diff --git a/src/main/java/ch/njol/skript/Skript.java b/src/main/java/ch/njol/skript/Skript.java index 216bc6687d8..117d05efb68 100644 --- a/src/main/java/ch/njol/skript/Skript.java +++ b/src/main/java/ch/njol/skript/Skript.java @@ -4,7 +4,6 @@ import ch.njol.skript.bukkitutil.BurgerHelper; import ch.njol.skript.classes.ClassInfo; import ch.njol.skript.classes.data.*; -import ch.njol.skript.command.Commands; import ch.njol.skript.doc.Documentation; import ch.njol.skript.events.EvtSkript; import ch.njol.skript.expressions.arithmetic.ExprArithmetic; @@ -44,15 +43,14 @@ import org.bukkit.event.Event; import org.bukkit.event.EventHandler; import org.bukkit.event.Listener; -import org.bukkit.event.player.PlayerCommandPreprocessEvent; import org.bukkit.event.player.PlayerJoinEvent; import org.bukkit.event.server.PluginDisableEvent; -import org.bukkit.event.server.ServerCommandEvent; import org.bukkit.event.server.ServerLoadEvent; import org.bukkit.plugin.Plugin; import org.bukkit.plugin.PluginDescriptionFile; import org.bukkit.plugin.java.JavaPlugin; import org.jetbrains.annotations.ApiStatus; +import org.jetbrains.annotations.NotNull; import org.jetbrains.annotations.Nullable; import org.jetbrains.annotations.UnknownNullability; import org.jetbrains.annotations.Unmodifiable; @@ -63,6 +61,7 @@ import org.skriptlang.skript.addon.AddonModule; import org.skriptlang.skript.bukkit.BukkitModule; import org.skriptlang.skript.bukkit.SkriptMetrics; +import org.skriptlang.skript.bukkit.command.elements.effects.EffCommand; import org.skriptlang.skript.bukkit.lang.eventvalue.EventValueRegistry; import org.skriptlang.skript.bukkit.log.runtime.BukkitRuntimeErrorConsumer; import org.skriptlang.skript.bukkit.registration.BukkitSyntaxInfos; @@ -80,7 +79,9 @@ import org.skriptlang.skript.lang.script.Script; import org.skriptlang.skript.lang.structure.Structure; import org.skriptlang.skript.lang.structure.StructureInfo; +import org.skriptlang.skript.log.runtime.ErrorSource; import org.skriptlang.skript.log.runtime.RuntimeErrorManager; +import org.skriptlang.skript.log.runtime.RuntimeErrorProducer; import org.skriptlang.skript.registration.DefaultSyntaxInfos; import org.skriptlang.skript.registration.SyntaxInfo; import org.skriptlang.skript.registration.SyntaxRegistry; @@ -576,8 +577,6 @@ public String name() { // todo: remove completely 2.11 or 2.12 CompletableFuture aliases = Aliases.loadAsync(); - Commands.registerListeners(); - if (logNormal()) info(" " + Language.get("skript.copyright")); @@ -1811,26 +1810,24 @@ public static void registerStructure( * @param sender * @param command * @return Whether the command was run + * @deprecated There is no replacement for this method. */ - public static boolean dispatchCommand(final CommandSender sender, final String command) { - try { - if (sender instanceof Player) { - final PlayerCommandPreprocessEvent e = new PlayerCommandPreprocessEvent((Player) sender, "/" + command); - Bukkit.getPluginManager().callEvent(e); - if (e.isCancelled() || !e.getMessage().startsWith("/")) - return false; - return Bukkit.dispatchCommand(e.getPlayer(), e.getMessage().substring(1)); - } else { - final ServerCommandEvent e = new ServerCommandEvent(sender, command); - Bukkit.getPluginManager().callEvent(e); - if (e.getCommand().isEmpty() || e.isCancelled()) - return false; - return Bukkit.dispatchCommand(e.getSender(), e.getCommand()); + @Deprecated(since = "INSERT VERSION", forRemoval = true) + public static boolean dispatchCommand(CommandSender sender, String command) { + return EffCommand.dispatchCommand(sender, command, new RuntimeErrorProducer() { + @Override + public @NotNull ErrorSource getErrorSource() { + throw new UnsupportedOperationException(); } - } catch (final Exception ex) { - ex.printStackTrace(); // just like Bukkit - return false; - } + @Override + public void error(String message) { } + @Override + public void error(String message, String highlight) { } + @Override + public void warning(String message) { } + @Override + public void warning(String message, String highlight) { } + }); } // ================ LOGGING ================ diff --git a/src/main/java/ch/njol/skript/bukkitutil/CommandReloader.java b/src/main/java/ch/njol/skript/bukkitutil/CommandReloader.java index aa730e1ad0c..86871293df9 100644 --- a/src/main/java/ch/njol/skript/bukkitutil/CommandReloader.java +++ b/src/main/java/ch/njol/skript/bukkitutil/CommandReloader.java @@ -10,7 +10,9 @@ /** * Utilizes CraftServer with reflection to re-send commands to clients. + * @deprecated This behavior should not be relied upon. There is no replacement. */ +@Deprecated(since = "INSERT VERSION", forRemoval = true) public class CommandReloader { @Nullable diff --git a/src/main/java/ch/njol/skript/command/Argument.java b/src/main/java/ch/njol/skript/command/Argument.java index 70ea5da9212..9c678b99d5f 100644 --- a/src/main/java/ch/njol/skript/command/Argument.java +++ b/src/main/java/ch/njol/skript/command/Argument.java @@ -22,7 +22,10 @@ * Represents an argument of a command * * @author Peter Güttinger + * @deprecated There is no direct replacement for this class. + * The closest alternative is {@link org.skriptlang.skript.bukkit.command.custom.ArgumentData}. */ +@Deprecated(since = "INSERT VERSION", forRemoval = true) public class Argument { @Nullable diff --git a/src/main/java/ch/njol/skript/command/CommandUsage.java b/src/main/java/ch/njol/skript/command/CommandUsage.java index d691257609a..ef93d0aee46 100644 --- a/src/main/java/ch/njol/skript/command/CommandUsage.java +++ b/src/main/java/ch/njol/skript/command/CommandUsage.java @@ -10,7 +10,9 @@ /** * Holds info about the usage of a command. * TODO: replace with record when java 17 + * @deprecated There is no direct replacement for this class. */ +@Deprecated(since = "INSERT VERSION", forRemoval = true) public class CommandUsage { /** diff --git a/src/main/java/ch/njol/skript/command/Commands.java b/src/main/java/ch/njol/skript/command/Commands.java index 120e16867e9..8c729d6c4ed 100644 --- a/src/main/java/ch/njol/skript/command/Commands.java +++ b/src/main/java/ch/njol/skript/command/Commands.java @@ -22,8 +22,6 @@ import org.bukkit.event.EventPriority; import org.bukkit.event.Listener; import org.bukkit.event.player.AsyncPlayerChatEvent; -import org.bukkit.event.player.PlayerCommandPreprocessEvent; -import org.bukkit.event.server.ServerCommandEvent; import org.bukkit.help.HelpMap; import org.bukkit.help.HelpTopic; import org.bukkit.plugin.SimplePluginManager; @@ -125,46 +123,6 @@ public static String unescape(String string) { return "" + unescape.matcher(string).replaceAll("$0"); } - private final static Listener commandListener = new Listener() { - - @EventHandler(priority = EventPriority.HIGHEST, ignoreCancelled = true) - public void onPlayerCommand(PlayerCommandPreprocessEvent event) { - // Spigot will simply report that the command doesn't exist if a player does not have permission to use it. - // This is good security but, well, it's a breaking change for Skript. So we need to check for permissions - // ourselves and handle those messages, for every command. - - // parse command, see if it's a skript command - String[] cmd = event.getMessage().substring(1).split("\\s+", 2); - String label = cmd[0].toLowerCase(Locale.ENGLISH); - String arguments = cmd.length == 1 ? "" : "" + cmd[1]; - ScriptCommand command = commands.get(label); - - // is it a skript command? - if (command != null) { - // if so, check permissions to handle ourselves - if (!command.checkPermissions(event.getPlayer(), label, arguments)) - event.setCancelled(true); - - // we can also handle case sensitivity here: - if (SkriptConfig.caseInsensitiveCommands.value()) { - cmd[0] = event.getMessage().charAt(0) + label; - event.setMessage(String.join(" ", cmd)); - } - } - } - - @SuppressWarnings("null") - @EventHandler(priority = EventPriority.HIGHEST) - public void onServerCommand(ServerCommandEvent event) { - if (event.getCommand().isEmpty() || event.isCancelled()) - return; - if ((Skript.testing() || SkriptConfig.enableEffectCommands.value()) && event.getCommand().startsWith(SkriptConfig.effectCommandToken.value())) { - if (handleEffectCommand(event.getSender(), event.getCommand())) - event.setCancelled(true); - } - } - }; - static boolean handleEffectCommand(CommandSender sender, String command) { if (!(Skript.testing() || sender instanceof ConsoleCommandSender || sender.hasPermission("skript.effectcommands") || SkriptConfig.allowOpsToUseEffectCommands.value() && sender.isOp())) return false; @@ -279,8 +237,6 @@ public static void unregisterCommand(ScriptCommand scriptCommand) { public static void registerListeners() { if (!registeredListeners) { - Bukkit.getPluginManager().registerEvents(commandListener, Skript.getInstance()); - Bukkit.getPluginManager().registerEvents(new Listener() { @EventHandler(priority = EventPriority.LOWEST, ignoreCancelled = true) public void onPlayerChat(AsyncPlayerChatEvent event) { diff --git a/src/main/java/ch/njol/skript/command/ScriptCommand.java b/src/main/java/ch/njol/skript/command/ScriptCommand.java index 61c3ba78c8b..2a27f8ac457 100644 --- a/src/main/java/ch/njol/skript/command/ScriptCommand.java +++ b/src/main/java/ch/njol/skript/command/ScriptCommand.java @@ -64,7 +64,10 @@ /** * This class is used for user-defined commands. + * @deprecated There is no direct replacement for this command. + * The closest alternative is {@link org.skriptlang.skript.bukkit.command.custom.ScriptBrigadierCommand}. */ +@Deprecated(since = "INSERT VERSION", forRemoval = true) public class ScriptCommand implements TabExecutor { public final static Message m_executable_by_players = new Message("commands.executable by players"); @@ -332,7 +335,7 @@ public boolean execute(final CommandSender sender, final String commandLabel, fi boolean execute2(final ScriptCommandEvent event, final CommandSender sender, final String commandLabel, final String rest) { final ParseLogHandler log = SkriptLogger.startParseLogHandler(); try { - final boolean ok = SkriptParser.parseArguments(rest, ScriptCommand.this, event); + final boolean ok = false; if (!ok) { final LogEntry e = log.getError(); if (e != null) diff --git a/src/main/java/ch/njol/skript/command/ScriptCommandEvent.java b/src/main/java/ch/njol/skript/command/ScriptCommandEvent.java index deed6c35be0..00c415b76ec 100644 --- a/src/main/java/ch/njol/skript/command/ScriptCommandEvent.java +++ b/src/main/java/ch/njol/skript/command/ScriptCommandEvent.java @@ -6,6 +6,11 @@ import org.bukkit.entity.Player; import org.bukkit.event.HandlerList; +/** + * @deprecated There is no direct replacement for this class. + * The closest alternative is {@link org.skriptlang.skript.bukkit.command.custom.ScriptCommandEvent}. + */ +@Deprecated(since = "INSERT VERSION", forRemoval = true) public class ScriptCommandEvent extends CommandEvent { private final ScriptCommand scriptCommand; diff --git a/src/main/java/ch/njol/skript/command/package-info.java b/src/main/java/ch/njol/skript/command/package-info.java index e18672cb830..58bc0fabf66 100644 --- a/src/main/java/ch/njol/skript/command/package-info.java +++ b/src/main/java/ch/njol/skript/command/package-info.java @@ -1,25 +1 @@ -/** - * This file is part of Skript. - * - * Skript is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * Skript is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with Skript. If not, see . - * - * Copyright Peter Güttinger, SkriptLang team and contributors - */ -/** - * Code related to handling commands, either Skript commands or custom script commands. - * - * @author Peter Güttinger - */ package ch.njol.skript.command; - diff --git a/src/main/java/ch/njol/skript/conditions/CondIsSkriptCommand.java b/src/main/java/ch/njol/skript/conditions/CondIsSkriptCommand.java deleted file mode 100644 index 3375a02bf80..00000000000 --- a/src/main/java/ch/njol/skript/conditions/CondIsSkriptCommand.java +++ /dev/null @@ -1,34 +0,0 @@ -package ch.njol.skript.conditions; - -import ch.njol.skript.conditions.base.PropertyCondition; -import ch.njol.skript.doc.Description; -import ch.njol.skript.doc.Example; -import ch.njol.skript.doc.Name; -import ch.njol.skript.doc.Since; - -import static ch.njol.skript.command.Commands.scriptCommandExists; - -@Name("Is a Skript command") -@Description("Checks whether a command/string is a custom Skript command.") -@Example(""" - on command: - command is a skript command - """) -@Since("2.6") -public class CondIsSkriptCommand extends PropertyCondition { - - static { - register(CondIsSkriptCommand.class, "[a] s(k|c)ript (command|cmd)", "string"); - } - - @Override - public boolean check(String cmd) { - return scriptCommandExists(cmd); - } - - @Override - protected String getPropertyName() { - return "skript command"; - } - -} diff --git a/src/main/java/ch/njol/skript/effects/EffCancelCooldown.java b/src/main/java/ch/njol/skript/effects/EffCancelCooldown.java deleted file mode 100644 index eb84feae65a..00000000000 --- a/src/main/java/ch/njol/skript/effects/EffCancelCooldown.java +++ /dev/null @@ -1,69 +0,0 @@ -package ch.njol.skript.effects; - -import ch.njol.skript.lang.EventRestrictedSyntax; -import ch.njol.util.coll.CollectionUtils; -import org.bukkit.event.Event; -import org.jetbrains.annotations.Nullable; - -import ch.njol.skript.Skript; -import ch.njol.skript.command.ScriptCommandEvent; -import ch.njol.skript.doc.Description; -import ch.njol.skript.doc.Example; -import ch.njol.skript.doc.Name; -import ch.njol.skript.doc.Since; -import ch.njol.skript.lang.Effect; -import ch.njol.skript.lang.Expression; -import ch.njol.skript.lang.SkriptParser; -import ch.njol.skript.log.ErrorQuality; -import ch.njol.util.Kleenean; - -@Name("Cancel Command Cooldown") -@Description({"Only usable in commands. Makes it so the current command usage isn't counted towards the cooldown."}) -@Example(""" - command /nick : - executable by: players - cooldown: 10 seconds - trigger: - if length of arg-1 is more than 16: - # Makes it so that invalid arguments don't make you wait for the cooldown again - cancel the cooldown - send "Your nickname may be at most 16 characters." - stop - set the player's display name to arg-1 - """) -@Since("2.2-dev34") -public class EffCancelCooldown extends Effect implements EventRestrictedSyntax { - - static { - Skript.registerEffect(EffCancelCooldown.class, - "(cancel|ignore) [the] [current] [command] cooldown", - "un(cancel|ignore) [the] [current] [command] cooldown"); - } - - private boolean cancel; - - @Override - public boolean init(Expression[] exprs, int matchedPattern, Kleenean isDelayed, SkriptParser.ParseResult parseResult) { - cancel = matchedPattern == 0; - return true; - } - - @Override - public Class[] supportedEvents() { - return CollectionUtils.array(ScriptCommandEvent.class); - } - - @Override - protected void execute(Event e) { - if (!(e instanceof ScriptCommandEvent)) - return; - - ((ScriptCommandEvent) e).setCooldownCancelled(cancel); - } - - @Override - public String toString(@Nullable Event e, boolean debug) { - return (cancel ? "" : "un") + "cancel the command cooldown"; - } - -} diff --git a/src/main/java/ch/njol/skript/effects/EffCommand.java b/src/main/java/ch/njol/skript/effects/EffCommand.java deleted file mode 100644 index 851a30aa710..00000000000 --- a/src/main/java/ch/njol/skript/effects/EffCommand.java +++ /dev/null @@ -1,95 +0,0 @@ -package ch.njol.skript.effects; - -import org.bukkit.Bukkit; -import org.bukkit.command.CommandSender; -import org.bukkit.entity.Player; -import org.bukkit.event.Event; -import org.jetbrains.annotations.Nullable; - -import ch.njol.skript.Skript; -import ch.njol.skript.doc.Description; -import ch.njol.skript.doc.Example; -import ch.njol.skript.doc.Name; -import ch.njol.skript.doc.Since; -import ch.njol.skript.lang.Effect; -import ch.njol.skript.lang.Expression; -import ch.njol.skript.lang.SkriptParser.ParseResult; -import ch.njol.skript.lang.VariableString; -import ch.njol.skript.util.StringMode; -import ch.njol.skript.util.Utils; -import ch.njol.util.Kleenean; - -@Name("Command") -@Description({ - "Executes a command. This can be useful to use other plugins in triggers.", - "If the command is a bungeecord side command, " + - "you can use the [bungeecord] option to execute command on the proxy." -}) -@Example("make player execute command \"/home\"") -@Example("execute console command \"/say Hello everyone!\"") -@Example("execute player bungeecord command \"/alert &6Testing Announcement!\"") -@Since("1.0, 2.8.0 (bungeecord command)") -public class EffCommand extends Effect { - - public static final String MESSAGE_CHANNEL = "Message"; - - static { - Skript.registerEffect(EffCommand.class, - "[execute] [the] [bungee:bungee[cord]] command[s] %strings% [by %-commandsenders%]", - "[execute] [the] %commandsenders% [bungee:bungee[cord]] command[s] %strings%", - "(let|make) %commandsenders% execute [[the] [bungee:bungee[cord]] command[s]] %strings%"); - } - - @Nullable - private Expression senders; - private Expression commands; - private boolean bungeecord; - - @Override - @SuppressWarnings("unchecked") - public boolean init(Expression[] exprs, int matchedPattern, Kleenean isDelayed, ParseResult parseResult) { - if (matchedPattern == 0) { - commands = (Expression) exprs[0]; - senders = (Expression) exprs[1]; - } else { - senders = (Expression) exprs[0]; - commands = (Expression) exprs[1]; - } - bungeecord = parseResult.hasTag("bungee"); - if (bungeecord && senders == null) { - Skript.error("The commandsenders expression cannot be omitted when using the bungeecord option"); - return false; - } - commands = VariableString.setStringMode(commands, StringMode.COMMAND); - return true; - } - - @Override - public void execute(Event event) { - for (String command : commands.getArray(event)) { - assert command != null; - if (command.startsWith("/")) - command = "" + command.substring(1); - if (senders != null) { - for (CommandSender sender : senders.getArray(event)) { - if (bungeecord) { - if (!(sender instanceof Player)) - continue; - Player player = (Player) sender; - Utils.sendPluginMessage(player, EffConnect.BUNGEE_CHANNEL, MESSAGE_CHANNEL, player.getName(), "/" + command); - continue; - } - Skript.dispatchCommand(sender, command); - } - } else { - Skript.dispatchCommand(Bukkit.getConsoleSender(), command); - } - } - } - - @Override - public String toString(@Nullable Event event, boolean debug) { - return "make " + (senders != null ? senders.toString(event, debug) : "the console") + " execute " + (bungeecord ? "bungeecord " : "") + "command " + commands.toString(event, debug); - } - -} diff --git a/src/main/java/ch/njol/skript/effects/EffScriptFile.java b/src/main/java/ch/njol/skript/effects/EffScriptFile.java index ea47fadab30..894ca1acd86 100644 --- a/src/main/java/ch/njol/skript/effects/EffScriptFile.java +++ b/src/main/java/ch/njol/skript/effects/EffScriptFile.java @@ -4,7 +4,6 @@ import ch.njol.skript.ScriptLoader.ScriptInfo; import ch.njol.skript.Skript; import ch.njol.skript.SkriptCommand; -import ch.njol.skript.command.ScriptCommand; import ch.njol.skript.doc.Description; import ch.njol.skript.doc.Example; import ch.njol.skript.doc.Name; diff --git a/src/main/java/ch/njol/skript/expressions/ExprAllCommands.java b/src/main/java/ch/njol/skript/expressions/ExprAllCommands.java deleted file mode 100644 index b44402b3767..00000000000 --- a/src/main/java/ch/njol/skript/expressions/ExprAllCommands.java +++ /dev/null @@ -1,69 +0,0 @@ -package ch.njol.skript.expressions; - -import org.bukkit.event.Event; -import org.jetbrains.annotations.Nullable; - -import ch.njol.skript.Skript; -import ch.njol.skript.command.Commands; -import ch.njol.skript.doc.Description; -import ch.njol.skript.doc.Example; -import ch.njol.skript.doc.Name; -import ch.njol.skript.doc.Since; -import ch.njol.skript.lang.Expression; -import ch.njol.skript.lang.ExpressionType; -import ch.njol.skript.lang.SkriptParser.ParseResult; -import ch.njol.skript.lang.util.SimpleExpression; -import ch.njol.util.Kleenean; - -@Name("All commands") -@Description("Returns all registered commands or all script commands.") -@Example("send \"Number of all commands: %size of all commands%\"") -@Example("send \"Number of all script commands: %size of all script commands%\"") -@Since("2.6") -public class ExprAllCommands extends SimpleExpression { - - static { - Skript.registerExpression(ExprAllCommands.class, String.class, ExpressionType.SIMPLE, "[(all|the|all [of] the)] [registered] [(1¦script)] commands"); - } - - private boolean scriptCommandsOnly; - - @Override - public boolean init(Expression[] exprs, int matchedPattern, Kleenean isDelayed, ParseResult parseResult) { - scriptCommandsOnly = parseResult.mark == 1; - return true; - } - - @Nullable - @Override - @SuppressWarnings("null") - protected String[] get(Event e) { - if (scriptCommandsOnly) { - return Commands.getScriptCommands().toArray(new String[0]); - } else { - if (Commands.getCommandMap() == null) - return null; - return Commands.getCommandMap() - .getCommands() - .parallelStream() - .map(command -> command.getLabel()) - .toArray(String[]::new); - } - } - - @Override - public boolean isSingle() { - return false; - } - - @Override - public Class getReturnType() { - return String.class; - } - - @Override - public String toString(@Nullable Event e, boolean debug) { - return "all " + (scriptCommandsOnly ? "script " : " ") + "commands"; - } - -} diff --git a/src/main/java/ch/njol/skript/expressions/ExprArgument.java b/src/main/java/ch/njol/skript/expressions/ExprArgument.java deleted file mode 100644 index d90388e426e..00000000000 --- a/src/main/java/ch/njol/skript/expressions/ExprArgument.java +++ /dev/null @@ -1,289 +0,0 @@ -package ch.njol.skript.expressions; - -import ch.njol.skript.Skript; -import ch.njol.skript.classes.ClassInfo; -import ch.njol.skript.command.Argument; -import ch.njol.skript.command.Commands; -import ch.njol.skript.command.ScriptCommandEvent; -import ch.njol.skript.doc.Description; -import ch.njol.skript.doc.Example; -import ch.njol.skript.doc.Name; -import ch.njol.skript.doc.Since; -import ch.njol.skript.lang.EventRestrictedSyntax; -import ch.njol.skript.lang.Expression; -import ch.njol.skript.lang.ExpressionType; -import ch.njol.skript.lang.Literal; -import ch.njol.skript.lang.SkriptParser.ParseResult; -import ch.njol.skript.lang.util.SimpleExpression; -import ch.njol.skript.log.ErrorQuality; -import ch.njol.skript.registrations.Classes; -import ch.njol.skript.util.Utils; -import ch.njol.util.Kleenean; -import ch.njol.util.StringUtils; -import ch.njol.util.coll.CollectionUtils; -import org.bukkit.event.Event; -import org.bukkit.event.player.PlayerCommandPreprocessEvent; -import org.bukkit.event.server.ServerCommandEvent; -import org.jetbrains.annotations.Nullable; - -import java.util.List; -import java.util.regex.MatchResult; - -@Name("Argument") -@Description({ - "Usable in script commands and command events. Holds the value of an argument given to the command, " + - "e.g. if the command \"/tell <player> <text>\" is used like \"/tell Njol Hello Njol!\" argument 1 is the player named \"Njol\" and argument 2 is \"Hello Njol!\".", - "One can also use the type of the argument instead of its index to address the argument, e.g. in the above example 'player-argument' is the same as 'argument 1'.", - "Please note that specifying the argument type is only supported in script commands." -}) -@Example("give the item-argument to the player-argument") -@Example("damage the player-argument by the number-argument") -@Example("give a diamond pickaxe to the argument") -@Example("add argument 1 to argument 2") -@Example("heal the last argument") -@Since("1.0, 2.7 (support for command events)") -public class ExprArgument extends SimpleExpression implements EventRestrictedSyntax { - - static { - Skript.registerExpression(ExprArgument.class, Object.class, ExpressionType.SIMPLE, - "[the] last arg[ument]", // LAST - "[the] arg[ument](-| )<(\\d+)>", // ORDINAL - "[the] <(\\d*1)st|(\\d*2)nd|(\\d*3)rd|(\\d*[4-90])th> arg[ument][s]", // ORDINAL - "[(all [[of] the]|the)] arg[ument][(1:s)]", // SINGLE OR ALL - "[the] %*classinfo%( |-)arg[ument][( |-)<\\d+>]", // CLASSINFO - "[the] arg[ument]( |-)%*classinfo%[( |-)<\\d+>]" // CLASSINFO - ); - } - - private static final int LAST = 0, ORDINAL = 1, SINGLE = 2, ALL = 3, CLASSINFO = 4; - private int what; - - @Nullable - private Argument argument; - - private int ordinal = -1; // Available in ORDINAL and sometimes CLASSINFO - - private boolean couldCauseArithmeticConfusion = false; - - @Override - @SuppressWarnings("unchecked") - public boolean init(Expression[] exprs, int matchedPattern, Kleenean isDelayed, ParseResult parseResult) { - boolean scriptCommand = getParser().isCurrentEvent(ScriptCommandEvent.class); - - switch (matchedPattern) { - case 0: - what = LAST; - break; - case 1: - case 2: - what = ORDINAL; - break; - case 3: - what = parseResult.mark == 1 ? ALL : SINGLE; - if (what == SINGLE && parseResult.expr.matches("(the )?arg(ument)?")) - couldCauseArithmeticConfusion = true; // 'arg-1' could be parsed as 'argument - 1' - break; - case 4: - case 5: - what = CLASSINFO; - break; - default: - assert false; - } - - if (!scriptCommand && what == CLASSINFO) { - Skript.error("Command event arguments are strings, meaning type specification is useless"); - return false; - } - - List> currentArguments = Commands.currentArguments; - if (scriptCommand && (currentArguments == null || currentArguments.isEmpty())) { - Skript.error("This command doesn't have any arguments", ErrorQuality.SEMANTIC_ERROR); - return false; - } - - if (what == ORDINAL) { - // Figure out in which format (1st, 2nd, 3rd, Nth) argument was given in - MatchResult regex = parseResult.regexes.get(0); - String argMatch = null; - for (int i = 1; i <= 4; i++) { - argMatch = regex.group(i); - if (argMatch != null) { - break; // Found format - } - } - assert argMatch != null; - ordinal = Utils.parseInt(argMatch); - if (scriptCommand && ordinal > currentArguments.size()) { // Only check if it's a script command as we know nothing of command event arguments - Skript.error("This command doesn't have a " + StringUtils.fancyOrderNumber(ordinal) + " argument", ErrorQuality.SEMANTIC_ERROR); - return false; - } - } - - if (scriptCommand) { // Handle before execution - switch (what) { - case LAST: - argument = currentArguments.get(currentArguments.size() - 1); - break; - case ORDINAL: - argument = currentArguments.get(ordinal - 1); - break; - case SINGLE: - if (currentArguments.size() == 1) { - argument = currentArguments.get(0); - } else { - Skript.error("This command has multiple arguments, meaning it is not possible to get the 'argument'. Use 'argument 1', 'argument 2', etc. instead", ErrorQuality.SEMANTIC_ERROR); - return false; - } - break; - case ALL: - Skript.error("'arguments' cannot be used for script commands. Use 'argument 1', 'argument 2', etc. instead", ErrorQuality.SEMANTIC_ERROR); - return false; - case CLASSINFO: - ClassInfo c = ((Literal>) exprs[0]).getSingle(); - if (parseResult.regexes.size() > 0) { - ordinal = Utils.parseInt(parseResult.regexes.get(0).group()); - if (ordinal > currentArguments.size()) { - Skript.error("This command doesn't have a " + StringUtils.fancyOrderNumber(ordinal) + " " + c + " argument", ErrorQuality.SEMANTIC_ERROR); - return false; - } - } - - Argument arg = null; - int argAmount = 0; - for (Argument a : currentArguments) { - if (!c.getC().isAssignableFrom(a.getType())) // This argument is not of the required type - continue; - - if (ordinal == -1 && argAmount == 2) { // The user said ' argument' without specifying which, and multiple arguments for the type exist - Skript.error("There are multiple " + c + " arguments in this command", ErrorQuality.SEMANTIC_ERROR); - return false; - } - - arg = a; - - argAmount++; - if (argAmount == ordinal) { // There is argNum argument for the required type (ex: "string argument 2" would exist) - break; - } - } - - if (argAmount == 0) { - Skript.error("There is no " + c + " argument in this command", ErrorQuality.SEMANTIC_ERROR); - return false; - } else if (ordinal > argAmount) { // The user wanted an argument number that didn't exist for the given type - if (argAmount == 1) { - Skript.error("There is only one " + c + " argument in this command", ErrorQuality.SEMANTIC_ERROR); - } else { - Skript.error("There are only " + argAmount + " " + c + " arguments in this command", ErrorQuality.SEMANTIC_ERROR); - } - return false; - } - - // 'arg' will never be null here - argument = arg; - break; - default: - assert false : what; - return false; - } - } - - return true; - } - - @Override - public Class[] supportedEvents() { - return CollectionUtils.array(ScriptCommandEvent.class, PlayerCommandPreprocessEvent.class, - ServerCommandEvent.class); - } - - @Override - @Nullable - protected Object[] get(final Event e) { - if (argument != null) { - return argument.getCurrent(e); - } - - String fullCommand; - if (e instanceof PlayerCommandPreprocessEvent) { - fullCommand = ((PlayerCommandPreprocessEvent) e).getMessage().substring(1).trim(); - } else if (e instanceof ServerCommandEvent) { // It's a ServerCommandEvent then - fullCommand = ((ServerCommandEvent) e).getCommand().trim(); - } else { - return new Object[0]; - } - - String[] arguments; - int firstSpace = fullCommand.indexOf(' '); - if (firstSpace != -1) { - fullCommand = fullCommand.substring(firstSpace + 1); - arguments = fullCommand.split(" "); - } else { // No arguments, just the command - return new String[0]; - } - - switch (what) { - case LAST: - if (arguments.length > 0) - return new String[]{arguments[arguments.length - 1]}; - break; - case ORDINAL: - if (arguments.length >= ordinal) - return new String[]{arguments[ordinal - 1]}; - break; - case SINGLE: - if (arguments.length == 1) - return new String[]{arguments[arguments.length - 1]}; - break; - case ALL: - return arguments; - } - - return new Object[0]; - } - - @Override - public boolean isSingle() { - return argument != null ? argument.isSingle() : what != ALL; - } - - /** - * @return whether the expression is 'arg', a single argument that could cause confusion with 'arg-1' being parsed as 'argument - 1'. - */ - public boolean couldCauseArithmeticConfusion() { - return couldCauseArithmeticConfusion; - } - - @Override - public Class getReturnType() { - return argument != null ? argument.getType() : String.class; - } - - @Override - public boolean isLoopOf(String s) { - return s.equalsIgnoreCase("argument"); - } - - @Override - public String toString(@Nullable Event e, boolean debug) { - switch (what) { - case LAST: - return "the last argument"; - case ORDINAL: - return "the " + StringUtils.fancyOrderNumber(ordinal) + " argument"; - case SINGLE: - return "the argument"; - case ALL: - return "the arguments"; - case CLASSINFO: - assert argument != null; - ClassInfo ci = Classes.getExactClassInfo(argument.getType()); - assert ci != null; // If it was null, that would be very bad - return "the " + ci + " argument " + (ordinal != -1 ? ordinal : ""); // Add the argument number if the user gave it before - default: - return "argument"; - } - } - -} diff --git a/src/main/java/ch/njol/skript/expressions/ExprCmdCooldownInfo.java b/src/main/java/ch/njol/skript/expressions/ExprCmdCooldownInfo.java deleted file mode 100644 index 9cb0eaf7882..00000000000 --- a/src/main/java/ch/njol/skript/expressions/ExprCmdCooldownInfo.java +++ /dev/null @@ -1,211 +0,0 @@ -package ch.njol.skript.expressions; - -import java.util.UUID; - -import ch.njol.skript.lang.EventRestrictedSyntax; -import ch.njol.util.coll.CollectionUtils; -import org.bukkit.command.CommandSender; -import org.bukkit.entity.Player; -import org.bukkit.event.Event; -import org.jetbrains.annotations.Nullable; - -import ch.njol.skript.Skript; -import ch.njol.skript.classes.Changer; -import ch.njol.skript.command.ScriptCommand; -import ch.njol.skript.command.ScriptCommandEvent; -import ch.njol.skript.doc.Description; -import ch.njol.skript.doc.Example; -import ch.njol.skript.doc.Name; -import ch.njol.skript.doc.Since; -import ch.njol.skript.lang.Expression; -import ch.njol.skript.lang.ExpressionType; -import ch.njol.skript.lang.SkriptParser; -import ch.njol.skript.lang.util.SimpleExpression; -import ch.njol.skript.log.ErrorQuality; -import ch.njol.skript.util.Date; -import ch.njol.skript.util.Timespan; -import ch.njol.util.Kleenean; - -@Name("Cooldown Time/Remaining Time/Elapsed Time/Last Usage/Bypass Permission") -@Description({"Only usable in command events. Represents the cooldown time, the remaining time, the elapsed time,", - "the last usage date, or the cooldown bypass permission."}) -@Example(""" - command /home: - cooldown: 10 seconds - cooldown message: You last teleported home %elapsed time% ago, you may teleport home again in %remaining time%. - trigger: - teleport player to {home::%player%} - """) -@Since("2.2-dev33") -public class ExprCmdCooldownInfo extends SimpleExpression implements EventRestrictedSyntax { - - static { - Skript.registerExpression(ExprCmdCooldownInfo.class, Object.class, ExpressionType.SIMPLE, - "[the] remaining [time] [of [the] (cooldown|wait) [(of|for) [the] [current] command]]", - "[the] elapsed [time] [of [the] (cooldown|wait) [(of|for) [the] [current] command]]", - "[the] ((cooldown|wait) time|[wait] time of [the] (cooldown|wait) [(of|for) [the] [current] command])", - "[the] last usage [date] [of [the] (cooldown|wait) [(of|for) [the] [current] command]]", - "[the] [cooldown] bypass perm[ission] [of [the] (cooldown|wait) [(of|for) [the] [current] command]]"); - } - - private int pattern; - - @Override - public boolean init(Expression[] exprs, int matchedPattern, Kleenean isDelayed, SkriptParser.ParseResult parseResult) { - pattern = matchedPattern; - return true; - } - - @Override - public Class[] supportedEvents() { - return CollectionUtils.array(ScriptCommandEvent.class); - } - - @Override - @Nullable - protected Object[] get(Event e) { - if (!(e instanceof ScriptCommandEvent)) - return null; - ScriptCommandEvent event = ((ScriptCommandEvent) e); - ScriptCommand scriptCommand = event.getScriptCommand(); - - CommandSender sender = event.getSender(); - if (scriptCommand.getCooldown() == null || !(sender instanceof Player)) - return null; - Player player = (Player) event.getSender(); - UUID uuid = player.getUniqueId(); - - switch (pattern) { - case 0: - case 1: - long ms = pattern != 1 - ? scriptCommand.getRemainingMilliseconds(uuid, event) - : scriptCommand.getElapsedMilliseconds(uuid, event); - return new Timespan[] { new Timespan(ms) }; - case 2: - return new Timespan[] { scriptCommand.getCooldown() }; - case 3: - return new Date[] { scriptCommand.getLastUsage(uuid, event) }; - case 4: - return new String[] { scriptCommand.getCooldownBypass() }; - } - - return null; - } - - @Nullable - @Override - public Class[] acceptChange(Changer.ChangeMode mode) { - switch (mode) { - case ADD: - case REMOVE: - if (pattern <= 1) - // remaining or elapsed time - return new Class[] { Timespan.class }; - case RESET: - case SET: - if (pattern <= 1) - // remaining or elapsed time - return new Class[] { Timespan.class }; - else if (pattern == 3) - // last usage date - return new Class[] { Date.class }; - } - return null; - } - - @Override - public void change(Event e, @Nullable Object[] delta, Changer.ChangeMode mode) { - if (!(e instanceof ScriptCommandEvent)) - return; - ScriptCommandEvent commandEvent = (ScriptCommandEvent) e; - ScriptCommand command = commandEvent.getScriptCommand(); - Timespan cooldown = command.getCooldown(); - CommandSender sender = commandEvent.getSender(); - if (cooldown == null || !(sender instanceof Player)) - return; - long cooldownMs = cooldown.getAs(Timespan.TimePeriod.MILLISECOND); - UUID uuid = ((Player) sender).getUniqueId(); - - if (pattern <= 1) { - Timespan timespan = delta == null ? new Timespan(0) : (Timespan) delta[0]; - switch (mode) { - case ADD: - case REMOVE: - long change = (mode == Changer.ChangeMode.ADD ? 1 : -1) * timespan.getAs(Timespan.TimePeriod.MILLISECOND); - if (pattern == 0) { - long remaining = command.getRemainingMilliseconds(uuid, commandEvent); - long changed = remaining + change; - if (changed < 0) - changed = 0; - command.setRemainingMilliseconds(uuid, commandEvent, changed); - } else { - long elapsed = command.getElapsedMilliseconds(uuid, commandEvent); - long changed = elapsed + change; - if (changed > cooldownMs) - changed = cooldownMs; - command.setElapsedMilliSeconds(uuid, commandEvent, changed); - } - break; - case RESET: - if (pattern == 0) - command.setRemainingMilliseconds(uuid, commandEvent, cooldownMs); - else - command.setElapsedMilliSeconds(uuid, commandEvent, 0); - break; - case SET: - if (pattern == 0) - command.setRemainingMilliseconds(uuid, commandEvent, timespan.getAs(Timespan.TimePeriod.MILLISECOND)); - else - command.setElapsedMilliSeconds(uuid, commandEvent, timespan.getAs(Timespan.TimePeriod.MILLISECOND)); - break; - } - } else if (pattern == 3) { - switch (mode) { - case REMOVE_ALL: - case RESET: - command.setLastUsage(uuid, commandEvent, null); - break; - case SET: - Date date = delta == null ? null : (Date) delta[0]; - command.setLastUsage(uuid, commandEvent, date); - break; - } - } - } - - @Override - public boolean isSingle() { - return true; - } - - @Override - public Class getReturnType() { - if (pattern <= 2) - return Timespan.class; - return pattern == 3 ? Date.class : String.class; - } - - @Override - public String toString(@Nullable Event e, boolean debug) { - return "the " + getExpressionName() + " of the cooldown"; - } - - @Nullable - private String getExpressionName() { - switch (pattern) { - case 0: - return "remaining time"; - case 1: - return "elapsed time"; - case 2: - return "cooldown time"; - case 3: - return "last usage date"; - case 4: - return "bypass permission"; - } - return null; - } - -} diff --git a/src/main/java/ch/njol/skript/expressions/ExprCommandInfo.java b/src/main/java/ch/njol/skript/expressions/ExprCommandInfo.java deleted file mode 100644 index 39ac41c8ccc..00000000000 --- a/src/main/java/ch/njol/skript/expressions/ExprCommandInfo.java +++ /dev/null @@ -1,181 +0,0 @@ -package ch.njol.skript.expressions; - -import java.util.ArrayList; -import java.util.List; -import java.util.Locale; -import java.util.Objects; -import java.util.function.Function; - -import ch.njol.skript.command.ScriptCommand; -import ch.njol.skript.command.ScriptCommandEvent; -import ch.njol.skript.util.Utils; -import org.bukkit.command.Command; -import org.bukkit.command.CommandMap; -import org.bukkit.command.PluginCommand; -import org.bukkit.command.defaults.BukkitCommand; -import org.bukkit.event.Event; -import org.bukkit.event.player.PlayerCommandPreprocessEvent; -import org.bukkit.event.server.ServerCommandEvent; -import org.jetbrains.annotations.Nullable; - -import ch.njol.skript.Skript; -import ch.njol.skript.command.Commands; -import ch.njol.skript.doc.Description; -import ch.njol.skript.doc.Example; -import ch.njol.skript.doc.Name; -import ch.njol.skript.doc.Since; -import ch.njol.skript.lang.Expression; -import ch.njol.skript.lang.ExpressionType; -import ch.njol.skript.lang.SkriptParser.ParseResult; -import ch.njol.skript.lang.util.SimpleExpression; -import ch.njol.util.Kleenean; - -@Name("Command Info") -@Description("Get information about a command.") -@Example("main command label of command \"skript\"") -@Example("description of command \"help\"") -@Example("label of command \"pl\"") -@Example("usage of command \"help\"") -@Example("aliases of command \"bukkit:help\"") -@Example("permission of command \"/op\"") -@Example("command \"op\"'s permission message") -@Example("command \"sk\"'s plugin owner") -@Example(""" - command /greet : - usage: /greet - trigger: - if arg-1 is sender: - send "&cYou can't greet yourself! Usage: %the usage%" - stop - send "%sender% greets you!" to arg-1 - send "You greeted %arg-1%!" - """) -@Since("2.6") -public class ExprCommandInfo extends SimpleExpression { - - private enum InfoType { - NAME(Command::getName), - DESCRIPTION(Command::getDescription), - LABEL(Command::getLabel), - USAGE(Command::getUsage), - ALIASES(null), // Handled differently - PERMISSION(Command::getPermission), - PERMISSION_MESSAGE(Command::getPermissionMessage), - PLUGIN(command -> { - if (command instanceof PluginCommand) { - return ((PluginCommand) command).getPlugin().getName(); - } else if (command instanceof BukkitCommand) { - return "Bukkit"; - } else if (command.getClass().getPackage().getName().startsWith("org.spigot")) { - return "Spigot"; - } else if (command.getClass().getPackage().getName().startsWith("com.destroystokyo.paper")) { - return "Paper"; - } - return "Unknown"; - }); - - private final @Nullable Function function; - - InfoType(@Nullable Function function) { - this.function = function; - } - - } - - static { - Skript.registerExpression(ExprCommandInfo.class, String.class, ExpressionType.PROPERTY, - "[the] main command [label|name] [of [[the] command[s] %-strings%]]", "command[s] %strings%'[s] main command [label|name]", - "[the] description [of [[the] command[s] %-strings%]]", "command[s] %strings%'[s] description", - "[the] label [of [[the] command[s] %-strings%]]", "command[s] %strings%'[s] label", - "[the] usage [of [[the] command[s] %-strings%]]", "command[s] %strings%'[s] usage", - "[(all|the|all [of] the)] aliases [of [[the] command[s] %-strings%]]", "command[s] %strings%'[s] aliases", - "[the] permission [of [[the] command[s] %-strings%]]", "command[s] %strings%'[s] permission", - "[the] permission message [of [[the] command[s] %-strings%]]", "command[s] %strings%'[s] permission message", - "[the] plugin [owner] [of [[the] command[s] %-strings%]]", "command[s] %strings%'[s] plugin [owner]"); - } - - @SuppressWarnings("NotNullFieldNotInitialized") - private InfoType type; - - @Nullable - private Expression commandName; - - @Override - @SuppressWarnings("unchecked") - public boolean init(Expression[] exprs, int matchedPattern, Kleenean isDelayed, ParseResult parseResult) { - commandName = (Expression) exprs[0]; - if (commandName == null && !getParser().isCurrentEvent(ScriptCommandEvent.class, PlayerCommandPreprocessEvent.class, ServerCommandEvent.class)) { - Skript.error("There's no command in " + Utils.a(getParser().getCurrentEventName()) + " event. Please provide a command"); - return false; - } - type = InfoType.values()[Math.floorDiv(matchedPattern, 2)]; - return true; - } - - @Nullable - @Override - protected String[] get(Event event) { - Command[] commands = getCommands(event); - if (commands == null) - return new String[0]; - if (type == InfoType.ALIASES) { - ArrayList result = new ArrayList<>(); - for (Command command : commands) - result.addAll(getAliases(command)); - return result.toArray(new String[0]); - } - String[] result = new String[commands.length]; - for (int i = 0; i < commands.length; i++) - result[i] = type.function.apply(commands[i]); - return result; - } - - @Override - public boolean isSingle() { - return type != InfoType.ALIASES && (commandName == null || commandName.isSingle()); - } - - @Override - public Class getReturnType() { - return String.class; - } - - @Override - public String toString(@Nullable Event event, boolean debug) { - return "the " + type.name().toLowerCase(Locale.ENGLISH).replace("_", " ") + - (commandName == null ? "" : " of command " + commandName.toString(event, debug)); - } - - @Nullable - private Command[] getCommands(Event event) { - if (event instanceof ScriptCommandEvent && commandName == null) - return new Command[] {((ScriptCommandEvent) event).getScriptCommand().getBukkitCommand()}; - - CommandMap map = Commands.getCommandMap(); - if (map == null) - return null; - - if (commandName != null) - return commandName.stream(event).map(map::getCommand).filter(Objects::nonNull).toArray(Command[]::new); - - String commandName; - if (event instanceof ServerCommandEvent) { - commandName = ((ServerCommandEvent) event).getCommand(); - } else if (event instanceof PlayerCommandPreprocessEvent) { - commandName = ((PlayerCommandPreprocessEvent) event).getMessage().substring(1); - } else { - return null; - } - commandName = commandName.split(":")[0]; - Command command = map.getCommand(commandName); - return command != null ? new Command[] {command} : null; - } - - private static List getAliases(Command command) { - if (!(command instanceof PluginCommand) || ((PluginCommand) command).getPlugin() != Skript.getInstance()) - return command.getAliases(); - ScriptCommand scriptCommand = Commands.getScriptCommand(command.getName()); - return scriptCommand == null ? command.getAliases() : scriptCommand.getAliases(); - } - -} diff --git a/src/main/java/ch/njol/skript/expressions/ExprCommandSender.java b/src/main/java/ch/njol/skript/expressions/ExprCommandSender.java deleted file mode 100644 index f3eb52b11ab..00000000000 --- a/src/main/java/ch/njol/skript/expressions/ExprCommandSender.java +++ /dev/null @@ -1,34 +0,0 @@ -package ch.njol.skript.expressions; - -import org.bukkit.command.CommandSender; - -import ch.njol.skript.doc.Description; -import ch.njol.skript.doc.Events; -import ch.njol.skript.doc.Example; -import ch.njol.skript.doc.Name; -import ch.njol.skript.doc.Since; -import ch.njol.skript.expressions.base.EventValueExpression; - -@Name("Command Sender") -@Description({ - "The player or the console who sent a command. Mostly useful in commands and command events.", - "If the command sender is a command block, its location can be retrieved by using %block's location%" -}) -@Example("make the command sender execute \"/say hi!\"") -@Example(""" - on command: - log "%executor% used command /%command% %arguments%" to "commands.log" - """) -@Since("2.0") -@Events("command") -public class ExprCommandSender extends EventValueExpression { - - static { - register(ExprCommandSender.class, CommandSender.class, "[command['s]] (sender|executor)"); - } - - public ExprCommandSender() { - super(CommandSender.class); - } - -} diff --git a/src/main/java/ch/njol/skript/expressions/arithmetic/ExprArithmetic.java b/src/main/java/ch/njol/skript/expressions/arithmetic/ExprArithmetic.java index 35851083e2f..473bc225aea 100644 --- a/src/main/java/ch/njol/skript/expressions/arithmetic/ExprArithmetic.java +++ b/src/main/java/ch/njol/skript/expressions/arithmetic/ExprArithmetic.java @@ -6,7 +6,7 @@ import ch.njol.skript.doc.Example; import ch.njol.skript.doc.Name; import ch.njol.skript.doc.Since; -import ch.njol.skript.expressions.ExprArgument; +import org.skriptlang.skript.bukkit.command.elements.expressions.ExprArgument; import ch.njol.skript.lang.Expression; import ch.njol.skript.lang.ExpressionType; import ch.njol.skript.lang.Literal; diff --git a/src/main/java/ch/njol/skript/lang/SkriptParser.java b/src/main/java/ch/njol/skript/lang/SkriptParser.java index 596b833ab6f..3de03caeefe 100644 --- a/src/main/java/ch/njol/skript/lang/SkriptParser.java +++ b/src/main/java/ch/njol/skript/lang/SkriptParser.java @@ -5,10 +5,7 @@ import ch.njol.skript.SkriptConfig; import ch.njol.skript.classes.ClassInfo; import ch.njol.skript.classes.Parser; -import ch.njol.skript.command.Argument; import ch.njol.skript.command.Commands; -import ch.njol.skript.command.ScriptCommand; -import ch.njol.skript.command.ScriptCommandEvent; import ch.njol.skript.expressions.ExprParse; import ch.njol.skript.lang.DefaultExpressionUtils.DefaultExpressionError; import ch.njol.skript.lang.function.ExprFunctionCall; @@ -1217,30 +1214,6 @@ public org.skriptlang.skript.common.function.FunctionReference parseFunct return new FunctionReference<>(newReference.name(), null, newReference.namespace(), types, expressions); } - /* - * Command parsing - */ - - /** - * Prints parse errors (i.e. must start a ParseLog before calling this method) - */ - public static boolean parseArguments(String args, ScriptCommand command, ScriptCommandEvent event) { - SkriptParser parser = new SkriptParser(args, PARSE_LITERALS, ParseContext.COMMAND); - ParseResult parseResult = parser.parse_i(command.getPattern()); - if (parseResult == null) - return false; - - List> arguments = command.getArguments(); - assert arguments.size() == parseResult.exprs.length; - for (int i = 0; i < parseResult.exprs.length; i++) { - if (parseResult.exprs[i] == null) - arguments.get(i).setToDefault(event); - else - arguments.get(i).set(event, parseResult.exprs[i].getArray(event)); - } - return true; - } - /* * Utility methods */ diff --git a/src/main/java/ch/njol/skript/structures/StructCommand.java b/src/main/java/ch/njol/skript/structures/StructCommand.java index 7dde39e5047..c6246124217 100644 --- a/src/main/java/ch/njol/skript/structures/StructCommand.java +++ b/src/main/java/ch/njol/skript/structures/StructCommand.java @@ -1,343 +1,33 @@ package ch.njol.skript.structures; -import ch.njol.skript.ScriptLoader; -import ch.njol.skript.Skript; -import ch.njol.skript.bukkitutil.CommandReloader; -import ch.njol.skript.classes.ClassInfo; -import ch.njol.skript.classes.Parser; -import ch.njol.skript.command.Argument; -import ch.njol.skript.command.CommandUsage; -import ch.njol.skript.command.Commands; -import ch.njol.skript.command.ScriptCommand; -import ch.njol.skript.command.ScriptCommandEvent; -import ch.njol.skript.config.SectionNode; -import ch.njol.skript.doc.Description; -import ch.njol.skript.doc.Example; -import ch.njol.skript.doc.Name; -import ch.njol.skript.doc.Since; import ch.njol.skript.lang.Literal; -import ch.njol.skript.lang.ParseContext; -import org.skriptlang.skript.lang.script.Script; -import ch.njol.skript.lang.SkriptParser; import ch.njol.skript.lang.SkriptParser.ParseResult; -import ch.njol.skript.lang.VariableString; import org.skriptlang.skript.lang.entry.EntryContainer; -import org.skriptlang.skript.lang.entry.KeyValueEntryData; import org.skriptlang.skript.lang.structure.Structure; -import org.skriptlang.skript.lang.entry.EntryValidator; -import org.skriptlang.skript.lang.entry.util.LiteralEntryData; -import org.skriptlang.skript.lang.entry.util.VariableStringEntryData; -import ch.njol.skript.registrations.Classes; -import ch.njol.skript.util.StringMode; -import ch.njol.skript.util.Timespan; -import ch.njol.skript.util.Utils; -import ch.njol.util.NonNullPair; -import ch.njol.util.StringUtils; -import org.bukkit.Bukkit; import org.bukkit.event.Event; import org.jetbrains.annotations.Nullable; -import java.util.ArrayList; -import java.util.Arrays; -import java.util.List; -import java.util.concurrent.atomic.AtomicBoolean; -import java.util.regex.Matcher; -import java.util.regex.Pattern; - -@Name("Command") -@Description("Used for registering custom commands.") -@Example(""" - command /broadcast : - usage: A command for broadcasting a message to all players. - permission: skript.command.broadcast - permission message: You don't have permission to broadcast messages - aliases: /bc - executable by: players and console - cooldown: 15 seconds - cooldown message: You last broadcast a message %elapsed time% ago. You can broadcast another message in %remaining time%. - cooldown bypass: skript.command.broadcast.admin - cooldown storage: {cooldown::%player%} - trigger: - broadcast the argument - """) -@Since("1.0") +/** + * @deprecated See {@link org.skriptlang.skript.bukkit.command.elements.structures.StructCommand}. + * Note that this replacement class is considered internal API. + */ +@Deprecated(since = "INSERT VERSION", forRemoval = true) public class StructCommand extends Structure { - // Paper versions with the new command system need a delay before syncing commands or a CME will occur. - private static final boolean DELAY_COMMAND_SYNCING = Skript.classExists("io.papermc.paper.command.brigadier.Commands"); - + /** + * @deprecated This field is no longer used internally and should not be relied upon for any behavior. + */ + @Deprecated(since = "INSERT VERSION", forRemoval = true) public static final Priority PRIORITY = new Priority(500); - private static final Pattern COMMAND_PATTERN = Pattern.compile("(?i)^command\\s+/?(\\S+)\\s*(\\s+(.+))?$"); - private static final Pattern ARGUMENT_PATTERN = Pattern.compile("<\\s*(?:([^>]+?)\\s*:\\s*)?(.+?)\\s*(?:=\\s*(" + SkriptParser.WILDCARD + "))?\\s*>"); - private static final Pattern DESCRIPTION_PATTERN = Pattern.compile("(?>("aliases", new ArrayList<>(), true) { - private final Pattern pattern = Pattern.compile("\\s*,\\s*/?"); - - @Override - protected List getValue(String value) { - List aliases = new ArrayList<>(Arrays.asList(pattern.split(value))); - if (aliases.get(0).startsWith("/")) { - aliases.set(0, aliases.get(0).substring(1)); - } else if (aliases.get(0).isEmpty()) { - aliases = new ArrayList<>(0); - } - return aliases; - } - }) - .addEntryData(new KeyValueEntryData("executable by", ScriptCommand.CONSOLE | ScriptCommand.PLAYERS, true) { - private final Pattern pattern = Pattern.compile("\\s*,\\s*|\\s+(and|or)\\s+"); - - @Override - @Nullable - protected Integer getValue(String value) { - int executableBy = 0; - for (String b : pattern.split(value)) { - if (b.equalsIgnoreCase("console") || b.equalsIgnoreCase("the console")) { - executableBy |= ScriptCommand.CONSOLE; - } else if (b.equalsIgnoreCase("players") || b.equalsIgnoreCase("player")) { - executableBy |= ScriptCommand.PLAYERS; - } else { - return null; - } - } - return executableBy; - } - }) - .addEntryData(new LiteralEntryData<>("cooldown", null, true, Timespan.class)) - .addEntryData(new VariableStringEntryData("cooldown message", null, true)) - .addEntry("cooldown bypass", null, true) - .addEntryData(new VariableStringEntryData("cooldown storage", null, true, StringMode.VARIABLE_NAME)) - .addSection("trigger", false) - .unexpectedEntryMessage(key -> - "Unexpected entry '" + key + "'. Check that it's spelled correctly, and ensure that you have put all code into a trigger." - ) - .build(), - "command <.+>" - ); - } - - @SuppressWarnings("NotNullFieldNotInitialized") - private EntryContainer entryContainer; - - @Nullable - private ScriptCommand scriptCommand; - @Override public boolean init(Literal[] args, int matchedPattern, ParseResult parseResult, @Nullable EntryContainer entryContainer) { - assert entryContainer != null; // cannot be null for non-simple structures - this.entryContainer = entryContainer; - return true; + return false; } @Override public boolean load() { - getParser().setCurrentEvent("command", ScriptCommandEvent.class); - - String fullCommand = entryContainer.getSource().getKey(); - assert fullCommand != null; - fullCommand = ScriptLoader.replaceOptions(fullCommand); - - int level = 0; - for (int i = 0; i < fullCommand.length(); i++) { - if (fullCommand.charAt(i) == '[') { - level++; - } else if (fullCommand.charAt(i) == ']') { - if (level == 0) { - Skript.error("Invalid placement of [optional brackets]"); - getParser().deleteCurrentEvent(); - return false; - } - level--; - } - } - if (level > 0) { - Skript.error("Invalid amount of [optional brackets]"); - getParser().deleteCurrentEvent(); - return false; - } - - Matcher matcher = COMMAND_PATTERN.matcher(fullCommand); - boolean matches = matcher.matches(); - if (!matches) { - Skript.error("Invalid command structure pattern"); - return false; - } - - String command = matcher.group(1).toLowerCase(); - ScriptCommand existingCommand = Commands.getScriptCommand(command); - if (existingCommand != null && existingCommand.getLabel().equals(command)) { - Script script = existingCommand.getScript(); - Skript.error("A command with the name /" + existingCommand.getName() + " is already defined" - + (script != null ? (" in " + script.getConfig().getFileName()) : "") - ); - getParser().deleteCurrentEvent(); - return false; - } - - String arguments = matcher.group(3) == null ? "" : matcher.group(3); - StringBuilder pattern = new StringBuilder(); - - List> currentArguments = Commands.currentArguments = new ArrayList<>(); //Mirre - matcher = ARGUMENT_PATTERN.matcher(arguments); - int lastEnd = 0; - int optionals = 0; - for (int i = 0; matcher.find(); i++) { - pattern.append(Commands.escape(arguments.substring(lastEnd, matcher.start()))); - optionals += StringUtils.count(arguments, '[', lastEnd, matcher.start()); - optionals -= StringUtils.count(arguments, ']', lastEnd, matcher.start()); - - lastEnd = matcher.end(); - - ClassInfo c; - c = Classes.getClassInfoFromUserInput(matcher.group(2)); - NonNullPair p = Utils.getEnglishPlural(matcher.group(2)); - if (c == null) - c = Classes.getClassInfoFromUserInput(p.getFirst()); - if (c == null) { - Skript.error("Unknown type '" + matcher.group(2) + "'"); - getParser().deleteCurrentEvent(); - return false; - } - Parser parser = c.getParser(); - if (parser == null || !parser.canParse(ParseContext.COMMAND)) { - Skript.error("Can't use " + c + " as argument of a command"); - getParser().deleteCurrentEvent(); - return false; - } - - Argument arg = Argument.newInstance(matcher.group(1), c, matcher.group(3), i, !p.getSecond(), optionals > 0); - if (arg == null) { - getParser().deleteCurrentEvent(); - return false; - } - currentArguments.add(arg); - - if (arg.isOptional() && optionals == 0) { - pattern.append('['); - optionals++; - } - pattern.append("%").append(arg.isOptional() ? "-" : "").append(Utils.toEnglishPlural(c.getCodeName(), p.getSecond())).append("%"); - } - - pattern.append(Commands.escape("" + arguments.substring(lastEnd))); - optionals += StringUtils.count(arguments, '[', lastEnd); - optionals -= StringUtils.count(arguments, ']', lastEnd); - for (int i = 0; i < optionals; i++) - pattern.append(']'); - - String desc = "/" + command + " "; - desc += StringUtils.replaceAll(pattern, DESCRIPTION_PATTERN, m1 -> { - assert m1 != null; - NonNullPair p = Utils.getEnglishPlural("" + m1.group(1)); - String s1 = p.getFirst(); - return "<" + Classes.getClassInfo(s1).getName().toString(p.getSecond()) + ">"; - }); - desc = Commands.unescape(desc).trim(); - - VariableString usageMessage = entryContainer.getOptional("usage", VariableString.class, false); - String defaultUsageMessage = Commands.m_correct_usage + " " + desc; - CommandUsage usage = new CommandUsage(usageMessage, defaultUsageMessage); - - String description = entryContainer.get("description", String.class, true); - String prefix = entryContainer.getOptional("prefix", String.class, false); - - String permission = entryContainer.get("permission", String.class, true); - VariableString permissionMessage = entryContainer.getOptional("permission message", VariableString.class, false); - if (permissionMessage != null && permission.isEmpty()) - Skript.warning("command /" + command + " has a permission message set, but not a permission"); - - List aliases = entryContainer.get("aliases", List.class,true); - int executableBy = entryContainer.get("executable by", Integer.class, true); - - Timespan cooldown = entryContainer.getOptional("cooldown", Timespan.class, false); - VariableString cooldownMessage = entryContainer.getOptional("cooldown message", VariableString.class, false); - if (cooldownMessage != null && cooldown == null) - Skript.warning("command /" + command + " has a cooldown message set, but not a cooldown"); - String cooldownBypass = entryContainer.getOptional("cooldown bypass", String.class, false); - if (cooldownBypass == null) { - cooldownBypass = ""; - } else if (cooldownBypass.isEmpty() && cooldown == null) { - Skript.warning("command /" + command + " has a cooldown bypass set, but not a cooldown"); - } - VariableString cooldownStorage = entryContainer.getOptional("cooldown storage", VariableString.class, false); - if (cooldownStorage != null && cooldown == null) - Skript.warning("command /" + command + " has a cooldown storage set, but not a cooldown"); - - SectionNode node = entryContainer.getSource(); - - if (Skript.debug() || node.debug()) - Skript.debug("command " + desc + ":"); - - Commands.currentArguments = currentArguments; - try { - scriptCommand = new ScriptCommand(getParser().getCurrentScript(), command, pattern.toString(), currentArguments, description, prefix, - usage, aliases, permission, permissionMessage, cooldown, cooldownMessage, cooldownBypass, cooldownStorage, - executableBy, entryContainer.get("trigger", SectionNode.class, false)); - } finally { - Commands.currentArguments = null; - } - - if (Skript.logVeryHigh() && !Skript.debug()) - Skript.info("Registered command " + desc); - - getParser().deleteCurrentEvent(); - - Commands.registerCommand(scriptCommand); - SYNC_COMMANDS.set(true); - - return true; - } - - @Override - public boolean postLoad() { - scheduleCommandSync(); - return true; - } - - @Override - public void unload() { - assert scriptCommand != null; // This method should never be called if one of the loading methods fail - Commands.unregisterCommand(scriptCommand); - SYNC_COMMANDS.set(true); - } - - @Override - public void postUnload() { - scheduleCommandSync(); - } - - private void scheduleCommandSync() { - if (SYNC_COMMANDS.get()) { - SYNC_COMMANDS.set(false); - if (DELAY_COMMAND_SYNCING) { - // if the plugin is disabled, the server is likely closing and delaying will cause an error. - if (Bukkit.getPluginManager().isPluginEnabled(Skript.getInstance())) - Bukkit.getScheduler().runTask(Skript.getInstance(), this::forceCommandSync); - } else { - forceCommandSync(); - } - } - } - - private void forceCommandSync() { - if (CommandReloader.syncCommands(Bukkit.getServer())) { - Skript.debug("Commands synced to clients"); - } else { - Skript.debug("Commands changed but not synced to clients (normal on 1.12 and older)"); - } + return false; } @Override diff --git a/src/main/java/ch/njol/skript/util/Patterns.java b/src/main/java/ch/njol/skript/util/Patterns.java index 5f2b9061a29..3489b222259 100644 --- a/src/main/java/ch/njol/skript/util/Patterns.java +++ b/src/main/java/ch/njol/skript/util/Patterns.java @@ -40,6 +40,23 @@ public Patterns(Object[][] info) { } } + /** + * Creates a new {@link Patterns} with the provided paired inputs. + * @param info Pattern-Value pairs, for example: + * + * new Patterns("pattern1", value1, "pattern2", value2); + * + */ + public Patterns(Object... info) { + patterns = new String[info.length / 2]; + types = new Object[info.length / 2]; + for (int i = 0; i < info.length; i += 2) { + patterns[i / 2] = (String) info[i]; + types[i / 2] = info[i + 1]; + matchedPatterns.computeIfAbsent(info[i + 1], list -> new ArrayList<>()).add(i / 2); + } + } + /** * Returns an array of the registered patterns. * @return An {@link java.lang.reflect.Array} of {@link String}s. diff --git a/src/main/java/ch/njol/util/coll/CollectionUtils.java b/src/main/java/ch/njol/util/coll/CollectionUtils.java index 4440bebd020..2af30352dba 100644 --- a/src/main/java/ch/njol/util/coll/CollectionUtils.java +++ b/src/main/java/ch/njol/util/coll/CollectionUtils.java @@ -478,4 +478,29 @@ public static Double[] wrap(double[] primitive) { return wrapped; } + /** + * Stringifies a collection as an "and" or "or" list. + * @param collection The collection to stringify. + * @param and Whether this is an "and" list or an "or" list. + * @return Stringification of {@code collection}. + * For example, {@code "x, y, and z"}. + */ + public static String toString(Collection collection, boolean and) { + StringBuilder builder = new StringBuilder(); + int i = 0; + int end = collection.size() - 1; + for (Object object : collection) { + if (i > 0) { + if (i == end) { + builder.append(and ? " and " : " or "); + } else { + builder.append(", "); + } + } + builder.append(object); + i++; + } + return builder.toString(); + } + } diff --git a/src/main/java/org/skriptlang/skript/bukkit/BukkitModule.java b/src/main/java/org/skriptlang/skript/bukkit/BukkitModule.java index ffa8d4bab03..38c8d925567 100644 --- a/src/main/java/org/skriptlang/skript/bukkit/BukkitModule.java +++ b/src/main/java/org/skriptlang/skript/bukkit/BukkitModule.java @@ -9,6 +9,7 @@ import org.skriptlang.skript.bukkit.bossbar.BossBarModule; import org.skriptlang.skript.bukkit.breeding.BreedingModule; import org.skriptlang.skript.bukkit.brewing.BrewingModule; +import org.skriptlang.skript.bukkit.command.CommandModule; import org.skriptlang.skript.bukkit.damagesource.DamageSourceModule; import org.skriptlang.skript.bukkit.enchantments.EnchantmentModule; import org.skriptlang.skript.bukkit.entity.EntityModule; @@ -42,6 +43,7 @@ public Iterable children() { new BossBarModule(this), new BreedingModule(this), new BrewingModule(this), + new CommandModule(this), new DamageSourceModule(this), new EnchantmentModule(this), new EntityModule(this), diff --git a/src/main/java/org/skriptlang/skript/bukkit/command/CommandModule.java b/src/main/java/org/skriptlang/skript/bukkit/command/CommandModule.java new file mode 100644 index 00000000000..f45c7f09211 --- /dev/null +++ b/src/main/java/org/skriptlang/skript/bukkit/command/CommandModule.java @@ -0,0 +1,50 @@ +package org.skriptlang.skript.bukkit.command; + +import ch.njol.skript.Skript; +import ch.njol.skript.command.Commands; +import org.skriptlang.skript.addon.AddonModule; +import org.skriptlang.skript.addon.HierarchicalAddonModule; +import org.skriptlang.skript.addon.SkriptAddon; +import org.skriptlang.skript.bukkit.command.custom.ScriptCommandRegistrar; +import org.skriptlang.skript.bukkit.command.elements.conditions.*; +import org.skriptlang.skript.bukkit.command.elements.effects.*; +import org.skriptlang.skript.bukkit.command.elements.expressions.*; +import org.skriptlang.skript.bukkit.command.elements.structures.*; + +public class CommandModule extends HierarchicalAddonModule { + + public CommandModule(AddonModule parentModule) { + super(parentModule); + } + + @Override + protected void initSelf(SkriptAddon addon) { + Commands.registerListeners(); + } + + @Override + protected void loadSelf(SkriptAddon addon) { + ScriptCommandRegistrar.init(Skript.getInstance()); + + register(addon, + CondIsScriptCommand::register, + EffCancelCooldown::register, + EffCommand::register, + ExprAllCommands::register, + ExprArgument::register, + ExprCmdCooldownInfo::register, + ExprCommand::register, + ExprCommandExecutor::register, + ExprCommandInfo::register, + ExprCommandSender::register, + ExprCommandSuggestions::register, + syntaxRegistry -> StructCommand.register(addon, syntaxRegistry) + ); + } + + @Override + public String name() { + return "command"; + } + +} diff --git a/src/main/java/org/skriptlang/skript/bukkit/command/custom/ArgumentData.java b/src/main/java/org/skriptlang/skript/bukkit/command/custom/ArgumentData.java new file mode 100644 index 00000000000..bcd44feda74 --- /dev/null +++ b/src/main/java/org/skriptlang/skript/bukkit/command/custom/ArgumentData.java @@ -0,0 +1,23 @@ +package org.skriptlang.skript.bukkit.command.custom; + +import ch.njol.skript.classes.ClassInfo; +import ch.njol.skript.lang.Expression; +import org.jetbrains.annotations.Nullable; + +/** + * Data describing a command argument. + * @param name The name of the argument. + * @param isAutomaticName Whether {@link #name} was automatically generated. + * @param type The type of the argument. + * @param defaultValue The default value of the argument, if specified. + * @param The type of the argument. + */ +public record ArgumentData( + String name, + boolean isAutomaticName, + ClassInfo type, + boolean isSingle, + @Nullable Expression defaultValue, + @Nullable T min, + @Nullable T max +) { } diff --git a/src/main/java/org/skriptlang/skript/bukkit/command/custom/CommandParsingData.java b/src/main/java/org/skriptlang/skript/bukkit/command/custom/CommandParsingData.java new file mode 100644 index 00000000000..3286371979d --- /dev/null +++ b/src/main/java/org/skriptlang/skript/bukkit/command/custom/CommandParsingData.java @@ -0,0 +1,104 @@ +package org.skriptlang.skript.bukkit.command.custom; + +import ch.njol.skript.lang.parser.ParserInstance; +import ch.njol.skript.lang.parser.ParserInstance.Data; +import org.bukkit.command.CommandSender; +import org.jetbrains.annotations.Nullable; + +import java.util.ArrayDeque; +import java.util.Deque; +import java.util.LinkedList; +import java.util.List; +import java.util.Set; +import java.util.function.Function; + +/** + * Parsing data holding command context. + */ +public final class CommandParsingData extends Data { + + /** + * @param executableBy Set describing what kinds of {@link CommandSender} can execute the command. + * @param cooldownManager Handles cooldown management for cooldown command entries. + */ + public record ExecutorData( + @Nullable Set executableBy, + @Nullable CooldownManager cooldownManager + ) { } + + private final LinkedList> arguments = new LinkedList<>(); + private final Deque indices = new ArrayDeque<>(4); + + private final Deque executorDatas = new ArrayDeque<>(4); + + public boolean isParsingCooldownEntry = false; + + public CommandParsingData(ParserInstance parserInstance) { + super(parserInstance); + } + + /** + * @return Whether this parsing data contains any data. + */ + public boolean isEmpty() { + return indices.isEmpty(); + } + + /** + * @return Arguments currently stored on this data, in declaration order. + */ + public List> getArguments() { + return List.copyOf(arguments); + } + + /** + * Pushes arguments to this data. + * @param arguments The arguments to push. + * @see #popArguments() + */ + public void pushArguments(List> arguments) { + indices.push(this.arguments.size()); + this.arguments.addAll(arguments); + } + + /** + * Removes the last pushed arguments from this data. + * @see #pushArguments(List) + */ + public void popArguments() { + arguments.subList(indices.pop(), arguments.size()).clear(); + } + + /** + * Pushes executor data to this data. + * @param executorData The data to push. + * @see #popExecutorData() + */ + public void pushExecutorData(ExecutorData executorData) { + executorDatas.push(executorData); + } + + /** + * Removes the last pushed executor data from this data. + * @see #pushExecutorData(ExecutorData) + */ + public void popExecutorData() { + executorDatas.pop(); + } + + /** + * Obtains the first non-null instance of a value from the pushed executors. + * @param function A function to obtain the desired value from the data. + * @return The value, or null if it was never set. + */ + public @Nullable T getExecutorData(Function function) { + for (ExecutorData executorData : executorDatas) { + T result = function.apply(executorData); + if (result != null) { + return result; + } + } + return null; + } + +} diff --git a/src/main/java/org/skriptlang/skript/bukkit/command/custom/CooldownManager.java b/src/main/java/org/skriptlang/skript/bukkit/command/custom/CooldownManager.java new file mode 100644 index 00000000000..916f0fe3f59 --- /dev/null +++ b/src/main/java/org/skriptlang/skript/bukkit/command/custom/CooldownManager.java @@ -0,0 +1,202 @@ +package org.skriptlang.skript.bukkit.command.custom; + +import ch.njol.skript.lang.Expression; +import ch.njol.skript.lang.VariableString; +import ch.njol.skript.localization.Message; +import ch.njol.skript.util.Date; +import ch.njol.skript.util.Timespan; +import ch.njol.skript.variables.Variables; +import net.kyori.adventure.text.Component; +import org.bukkit.command.CommandSender; +import org.bukkit.entity.Player; +import org.bukkit.event.Event; +import org.jetbrains.annotations.Nullable; +import org.skriptlang.skript.bukkit.text.TextComponentParser; +import org.skriptlang.skript.log.runtime.RuntimeErrorProducer; + +import java.util.Map; +import java.util.UUID; +import java.util.concurrent.ConcurrentHashMap; + +/** + * Used to manage cooldowns for an individual command. + */ +public class CooldownManager { + + private static final Message M_COOLDOWN_MESSAGE = new Message("commands.cooldown message"); + + private final Timespan cooldown; + private final @Nullable Expression cooldownMessage; + private final @Nullable String cooldownBypass; + private final CooldownStorage cooldownStorage; + + public CooldownManager(Timespan cooldown, @Nullable Expression cooldownMessage, + @Nullable String cooldownBypass, @Nullable VariableString cooldownStorage, + RuntimeErrorProducer errorProducer) { + this.cooldown = cooldown; + this.cooldownMessage = cooldownMessage; + this.cooldownBypass = cooldownBypass; + this.cooldownStorage = cooldownStorage == null ? + new MemoryCooldownStorage() : new VariableCooldownStorage(cooldownStorage, errorProducer); + } + + /** + * @return The cooldown this manager uses. + */ + public Timespan getCooldown() { + return cooldown; + } + + /** + * @return A permission, if set, to bypass this manager's cooldown. + */ + public @Nullable String getCooldownBypass() { + return cooldownBypass; + } + + /** + * Obtains the start date of {@code sender}'s most recent cooldown. + * + * @param context Event providing context. + * @param sender The sender to obtain the cooldown for. + * @return The start date of {@code sender}'s cooldown, or null if no cooldown is set. + * Note that this may return a value even if the cooldown is expired. + */ + public @Nullable Date getStartDate(Event context, CommandSender sender) { + if (sender instanceof Player player) { + Date startDate = cooldownStorage.getStartDate(context, player.getUniqueId()); + if (startDate == null) { + return null; + } + return new Date(startDate.getTime()); + } + return null; + } + + /** + * Sets the start date of {@code sender}'s cooldown. + * + * @param context Event providing context. + * @param sender The sender to set the cooldown for. + * @param startDate New cooldown start date, or null to remove any cooldown. + */ + public void setStartDate(Event context, CommandSender sender, @Nullable Date startDate) { + if (sender instanceof Player player) { + cooldownStorage.setCooldown(context, player.getUniqueId(), startDate); + } + } + + /** + * Checks whether {@code sender} has an active cooldown. + * If they do not, this method sets a new cooldown and returns {@code true}. + * + * @param event Event providing context. + * @param commandSender The sender to check. + * @return Whether command execution can proceed. + */ + public boolean checkExecution(Event event, CommandSender commandSender) { + if (!(commandSender instanceof Player player)) { + return true; + } + + UUID uuid = player.getUniqueId(); + + if (cooldownBypass != null && player.hasPermission(cooldownBypass)) { + // clear cooldown in case one was set + cooldownStorage.setCooldown(event, uuid, null); + return true; + } + + Date startDate = cooldownStorage.getStartDate(event, uuid); + Date now = Date.now(); + if (startDate == null || !startDate.plus(cooldown).after(now)) { + cooldownStorage.setCooldown(event, uuid, now); + return true; + } + + // send cooldown message + Component component = null; + if (cooldownMessage != null) { + component = cooldownMessage.getSingle(event); + } + if (component == null) { + component = TextComponentParser.instance() + .parse(M_COOLDOWN_MESSAGE.getValueOrDefault("This command is on cooldown!")); + } + player.sendMessage(component); + + return false; + } + + private interface CooldownStorage { + + @Nullable Date getStartDate(Event context, UUID uuid); + + void setCooldown(Event context, UUID uuid, @Nullable Date startDate); + + } + + private record MemoryCooldownStorage(Map cooldowns) implements CooldownStorage { + + public MemoryCooldownStorage() { + this(new ConcurrentHashMap<>()); + } + + @Override + public @Nullable Date getStartDate(Event context, UUID uuid) { + return cooldowns.get(uuid); + } + + @Override + public void setCooldown(Event context, UUID uuid, @Nullable Date startDate) { + if (startDate == null) { + cooldowns.remove(uuid); + } else { + cooldowns.put(uuid, startDate); + } + } + + } + + private record VariableCooldownStorage(VariableString cooldownStorage, RuntimeErrorProducer errorProducer) + implements CooldownStorage { + + @Override + public @Nullable Date getStartDate(Event context, UUID uuid) { + String name = getStorageVariableName(context); + if (name == null) { + return null; + } + Object variable = Variables.getVariable(name, null, false); + if (variable != null && !(variable instanceof Date)) { + errorProducer.warning("Variable {" + name + "} was not a date! You may be using this variable elsewhere." + + " This warning is letting you know that this variable is now overridden for the command storage."); + return null; + } + return (Date) variable; + } + + @Override + public void setCooldown(Event context, UUID uuid, @Nullable Date startDate) { + String name = getStorageVariableName(context); + if (name == null) { + return; + } + Variables.setVariable(name, startDate, null, false); + } + + private @Nullable String getStorageVariableName(Event context) { + String variableString = cooldownStorage.getSingle(context); + if (variableString == null) { + errorProducer.error("The cooldown storage variable defined is invalid! Cooldowns for this command will not work."); + return null; + } + if (variableString.startsWith("{")) { + variableString = variableString.substring(1, variableString.length() - 1); + } + return variableString; + } + + } + +} diff --git a/src/main/java/org/skriptlang/skript/bukkit/command/custom/ExecutableBy.java b/src/main/java/org/skriptlang/skript/bukkit/command/custom/ExecutableBy.java new file mode 100644 index 00000000000..741c18fafe0 --- /dev/null +++ b/src/main/java/org/skriptlang/skript/bukkit/command/custom/ExecutableBy.java @@ -0,0 +1,72 @@ +package org.skriptlang.skript.bukkit.command.custom; + +import com.google.common.collect.Sets; +import org.bukkit.command.BlockCommandSender; +import org.bukkit.command.CommandSender; +import org.bukkit.command.ConsoleCommandSender; +import org.bukkit.command.RemoteConsoleCommandSender; +import org.bukkit.entity.Player; + +import java.util.Set; +import java.util.function.Predicate; + +/** + * Enum describing the types of {@link CommandSender}s that can execute this command. + */ +public enum ExecutableBy { + + /** + * This command is only executable by players. + */ + PLAYERS(sender -> sender instanceof Player), + + /** + * This command is only executable by operators. + */ + OPERATORS(CommandSender::isOp), + + /** + * This command is only executable by console. + */ + CONSOLE(sender -> sender instanceof ConsoleCommandSender || sender instanceof RemoteConsoleCommandSender), + + /** + * This command is only executable by blocks (e.g, command blocks). + */ + BLOCKS(sender -> sender instanceof BlockCommandSender); + + private final Predicate predicate; + + ExecutableBy(Predicate predicate) { + this.predicate = predicate; + } + + /** + * @return A predicate to validate the behavior expected by this restriction. + */ + public Predicate predicate() { + return predicate; + } + + @Override + public String toString() { + return switch (this) { + case PLAYERS -> "players"; + case OPERATORS -> "operators"; + case CONSOLE -> "the console"; + case BLOCKS -> "blocks"; + }; + } + + /** + * Compares whether a set of {@link ExecutableBy}s is a superset of another. + * @param first The first set. + * @param second The second set. + * @return Whether the first set includes all {@link CommandSender}s covered by the second. + */ + public static boolean isSuperSet(Set first, Set second) { + Set difference = Sets.difference(second, first); + return difference.isEmpty() || difference.equals(Set.of(ExecutableBy.OPERATORS)); + } + +} diff --git a/src/main/java/org/skriptlang/skript/bukkit/command/custom/ScriptArgumentType.java b/src/main/java/org/skriptlang/skript/bukkit/command/custom/ScriptArgumentType.java new file mode 100644 index 00000000000..e87fe59e3d0 --- /dev/null +++ b/src/main/java/org/skriptlang/skript/bukkit/command/custom/ScriptArgumentType.java @@ -0,0 +1,206 @@ +package org.skriptlang.skript.bukkit.command.custom; + +import ch.njol.skript.Skript; +import ch.njol.skript.classes.ClassInfo; +import ch.njol.skript.classes.registry.RegistryClassInfo; +import ch.njol.skript.lang.Literal; +import ch.njol.skript.lang.ParseContext; +import ch.njol.skript.lang.SkriptParser; +import ch.njol.skript.log.ParseLogHandler; +import ch.njol.skript.registrations.Classes; +import com.mojang.brigadier.LiteralMessage; +import com.mojang.brigadier.arguments.ArgumentType; +import com.mojang.brigadier.arguments.BoolArgumentType; +import com.mojang.brigadier.arguments.DoubleArgumentType; +import com.mojang.brigadier.arguments.LongArgumentType; +import com.mojang.brigadier.arguments.StringArgumentType; +import com.mojang.brigadier.context.CommandContext; +import com.mojang.brigadier.exceptions.CommandSyntaxException; +import com.mojang.brigadier.exceptions.Dynamic2CommandExceptionType; +import com.mojang.brigadier.exceptions.DynamicCommandExceptionType; +import com.mojang.brigadier.suggestion.Suggestions; +import com.mojang.brigadier.suggestion.SuggestionsBuilder; +import io.papermc.paper.command.brigadier.argument.ArgumentTypes; +import io.papermc.paper.command.brigadier.argument.CustomArgumentType; +import io.papermc.paper.registry.RegistryKey; +import org.bukkit.GameMode; +import org.bukkit.World; +import org.bukkit.block.BlockState; +import org.bukkit.block.data.BlockData; +import org.bukkit.entity.Entity; +import org.bukkit.entity.Player; +import org.bukkit.inventory.ItemStack; +import org.jetbrains.annotations.ApiStatus; +import org.jetbrains.annotations.NotNull; +import org.jetbrains.annotations.Nullable; +import org.skriptlang.skript.bukkit.command.elements.structures.util.ScriptSuggestionProvider; + +import java.util.Iterator; +import java.util.Locale; +import java.util.Map; +import java.util.UUID; +import java.util.concurrent.CompletableFuture; +import java.util.function.Function; +import java.util.function.Supplier; + +/** + * A custom argument for wrapping {@link ch.njol.skript.classes.ClassInfo}s with a {@link ch.njol.skript.classes.Parser}. + * This is natively a {@link StringArgumentType} with conversion occurring during execution. + * @param The real type of the argument. + */ +@ApiStatus.Internal +public class ScriptArgumentType implements CustomArgumentType.Converted { + + /** + * Data about an available native argument type. + * A native argument type is one that is recognized by the client, providing enhanced validation features. + * @param supportsPlural Whether this argument type supports returning multiple values. + * @param supportsRange Whether this argument type supports minimum or maximum values. + * @param mapper A function for obtaining the native argument type from an argument data. + */ + public record NativeArgumentData( + boolean supportsPlural, + boolean supportsRange, + Function, ArgumentType> mapper + ) { + + public NativeArgumentData { + if (!supportsPlural) { + final var inputMapper = mapper; + mapper = data -> data.isSingle() ? inputMapper.apply(data) : null; + } + } + + public NativeArgumentData(Function, ArgumentType> mapper) { + this(false, false, mapper); + } + + } + + /** + * Pre-defined mappings of types that are acceptable to map to other native argument types. + */ + private static final Map, NativeArgumentData> ARGUMENT_TYPE_MAPPINGS = Map.of( + Boolean.class, new NativeArgumentData(ignored -> BoolArgumentType.bool()), + Long.class, new NativeArgumentData(false, true, data -> { + Long min = (Long) data.min(); + Long max = (Long) data.max(); + if (min == null) { + if (max == null) { + return LongArgumentType.longArg(); + } + return LongArgumentType.longArg(Long.MIN_VALUE, max); + } else if (max == null) { + return LongArgumentType.longArg(min); + } + return LongArgumentType.longArg(min, max); + }), + Number.class, new NativeArgumentData(false, true, data -> { + Number min = (Number) data.min(); + Number max = (Number) data.max(); + if (min == null) { + if (max == null) { + return DoubleArgumentType.doubleArg(); + } + return DoubleArgumentType.doubleArg(Double.MIN_VALUE, max.doubleValue()); + } else if (max == null) { + return DoubleArgumentType.doubleArg(min.doubleValue()); + } + return DoubleArgumentType.doubleArg(min.doubleValue(), max.doubleValue()); + }), + Player.class, new NativeArgumentData(true, false, data -> + data.isSingle() ? ArgumentTypes.player() : ArgumentTypes.players()), + Entity.class, new NativeArgumentData(true, false, data -> + data.isSingle() ? ArgumentTypes.entity() : ArgumentTypes.entities()), + GameMode.class, new NativeArgumentData(ignored -> ArgumentTypes.gameMode()), + World.class, new NativeArgumentData(ignored -> ArgumentTypes.world()), + UUID.class, new NativeArgumentData(ignored -> ArgumentTypes.uuid()), + BlockData.class, new NativeArgumentData(ignored -> new Converted() { + @Override + public @NotNull ArgumentType getNativeType() { + return ArgumentTypes.blockState(); + } + @Override + public @NotNull BlockData convert(@NotNull BlockState blockState) { + return blockState.getBlockData(); + } + }), + ItemStack.class, new NativeArgumentData(ignored -> ArgumentTypes.itemStack()) + ); + + public static @Nullable NativeArgumentData getNativeData(ClassInfo classInfo) { + NativeArgumentData nativeArgumentData = ARGUMENT_TYPE_MAPPINGS.get(classInfo.getC()); + if (nativeArgumentData != null) { + return nativeArgumentData; + } + if (classInfo instanceof RegistryClassInfo registryClassInfo) { + // TODO need to map RegistryClassInfo to RegistryKey + RegistryKey key = null; + if (key != null) { + return new NativeArgumentData(ignored -> ArgumentTypes.resource(key)); + } + } + return null; + } + + private static final DynamicCommandExceptionType ERROR_PARSER_ERROR = new DynamicCommandExceptionType( + input -> new LiteralMessage((String) input)); + + private static final Dynamic2CommandExceptionType ERROR_INVALID_INPUT = new Dynamic2CommandExceptionType( + (input, type) -> new LiteralMessage("'%s' is not a valid %s.".formatted(input, type))); + + private final ArgumentData argument; + private final StringArgumentType nativeType; + + public ScriptArgumentType(@NotNull ArgumentData argument, @NotNull StringArgumentType nativeType) { + this.argument = argument; + this.nativeType = nativeType; + } + + @Override + public @NotNull Object convert(@NotNull String input) throws CommandSyntaxException { + assert argument.type().getParser() != null; + input = input.replace('_', ' '); + + Literal result; + try (ParseLogHandler logHandler = new ParseLogHandler().start()) { + //noinspection unchecked + result = (Literal) new SkriptParser(input, SkriptParser.PARSE_LITERALS, ParseContext.COMMAND) + .parseExpression(argument.type().getC()); + if (result != null && argument.isSingle() && !result.canBeSingle()) { // provided many values but expected one + result = null; + Skript.error("Expected one " + argument.type().getName().getSingular() + " but got many."); + } + if (result == null) { + if (logHandler.hasError()) { + throw ERROR_PARSER_ERROR.create(logHandler.getError()); + } else { + throw ERROR_INVALID_INPUT.create(input, argument.type().getName().getSingular()); + } + } + } + + return argument.isSingle() ? result.getSingle() : result.getArray(); + } + + @Override + public @NotNull ArgumentType getNativeType() { + return nativeType; + } + + @Override + public @NotNull CompletableFuture listSuggestions(@NotNull CommandContext context, @NotNull SuggestionsBuilder builder) { + Supplier> supplier = argument.type().getSupplier(); + if (supplier == null) { + return Suggestions.empty(); + } + Iterator iterator = supplier.get(); + while (iterator.hasNext()) { + ScriptSuggestionProvider.suggest(builder, Classes.toString(iterator.next()).toLowerCase(Locale.ENGLISH) + .replace(' ', '_')); + } + + return builder.buildFuture(); + } + +} diff --git a/src/main/java/org/skriptlang/skript/bukkit/command/custom/ScriptBrigadierCommand.java b/src/main/java/org/skriptlang/skript/bukkit/command/custom/ScriptBrigadierCommand.java new file mode 100644 index 00000000000..33d0472d35b --- /dev/null +++ b/src/main/java/org/skriptlang/skript/bukkit/command/custom/ScriptBrigadierCommand.java @@ -0,0 +1,26 @@ +package org.skriptlang.skript.bukkit.command.custom; + +import com.mojang.brigadier.tree.LiteralCommandNode; +import io.papermc.paper.command.brigadier.CommandSourceStack; +import org.jetbrains.annotations.Nullable; +import org.skriptlang.skript.lang.script.Script; + +import java.util.Collection; + +/** + * @param script The script this command is defined in. + * @param node The command node providing functionality for this command. + * @param aliases Aliases used to reference the command. Can be empty. + * @param description String describing the command. + * @param namespace An alternative namespace the command is registered under. + * If null, this command is registered under the default namespace (typically {@code skript}). + */ +public record ScriptBrigadierCommand( + Script script, + LiteralCommandNode node, + Collection aliases, + @Nullable String description, + @Nullable String usage, + @Nullable String namespace, + @Nullable String permission +) { } diff --git a/src/main/java/org/skriptlang/skript/bukkit/command/custom/ScriptCommandEvent.java b/src/main/java/org/skriptlang/skript/bukkit/command/custom/ScriptCommandEvent.java new file mode 100644 index 00000000000..4551fdaf8ef --- /dev/null +++ b/src/main/java/org/skriptlang/skript/bukkit/command/custom/ScriptCommandEvent.java @@ -0,0 +1,104 @@ +package org.skriptlang.skript.bukkit.command.custom; + +import io.papermc.paper.command.brigadier.CommandSourceStack; +import org.bukkit.Location; +import org.bukkit.command.CommandSender; +import org.bukkit.entity.Entity; +import org.bukkit.event.Event; +import org.bukkit.event.HandlerList; +import org.jetbrains.annotations.NotNull; +import org.jetbrains.annotations.Nullable; + +import java.util.Collections; +import java.util.HashMap; +import java.util.Map; + +/** + * Event for executing {@link org.skriptlang.skript.bukkit.command.elements.structures.StructCommand} commands with. + */ +public class ScriptCommandEvent extends Event { + + private final String label; + private final String rawInput; + private final ScriptCommandExecutor commandExecutor; + private final CommandSender sender; + private final @Nullable Entity executor; + private final Location location; + + final Map arguments = new HashMap<>(); + + public ScriptCommandEvent(String label, String rawInput, ScriptCommandExecutor commandExecutor, CommandSourceStack source) { + this.label = label; + this.rawInput = rawInput; + this.commandExecutor = commandExecutor; + this.sender = source.getSender(); + this.executor = source.getExecutor(); + this.location = source.getLocation(); + } + + /** + * @return The label of the executed command. + */ + public String getLabel() { + return label; + } + + /** + * @return The full raw input being executed. + */ + public String getRawInput() { + return rawInput; + } + + /** + * @return The sender is the thing that initiated/triggered the execution of the command. + * It differs to {@link #getExecutor()} in that the executor can be changed by a command, e.g. {@code /execute}. + */ + public CommandSender getSender() { + return sender; + } + + /** + * @return The entity that executes the command. + * May not always be {@link #getSender()} as the executor of a command can be changed to a different entity than the one that triggered the command. + */ + public @Nullable Entity getExecutor() { + return executor; + } + + /** + * @return The location that the command is being executed at. + */ + public Location getLocation() { + return location; + } + + /** + * @return The command executor being used to execute this command (perform logic). + */ + public ScriptCommandExecutor getCommandExecutor() { + return commandExecutor; + } + + /** + * @return A map of all available arguments and their values. + */ + public Map getArguments() { + return Collections.unmodifiableMap(arguments); + } + + /** + * Obtains an argument by its name. + * @param name The name of the argument. + * @return The value of the argument. + */ + public Object getArgument(String name) { + return arguments.get(name); + } + + @Override + public @NotNull HandlerList getHandlers() { + throw new UnsupportedOperationException(); + } + +} diff --git a/src/main/java/org/skriptlang/skript/bukkit/command/custom/ScriptCommandExecutor.java b/src/main/java/org/skriptlang/skript/bukkit/command/custom/ScriptCommandExecutor.java new file mode 100644 index 00000000000..48a952ab472 --- /dev/null +++ b/src/main/java/org/skriptlang/skript/bukkit/command/custom/ScriptCommandExecutor.java @@ -0,0 +1,105 @@ +package org.skriptlang.skript.bukkit.command.custom; + +import ch.njol.skript.lang.Trigger; +import ch.njol.skript.variables.Variables; +import com.mojang.brigadier.Command; +import com.mojang.brigadier.context.CommandContext; +import com.mojang.brigadier.exceptions.CommandSyntaxException; +import com.mojang.brigadier.tree.ArgumentCommandNode; +import io.papermc.paper.command.brigadier.CommandSourceStack; +import io.papermc.paper.command.brigadier.argument.resolvers.ArgumentResolver; +import org.bukkit.command.CommandSender; +import org.bukkit.event.Event; +import org.jetbrains.annotations.ApiStatus; +import org.jetbrains.annotations.Nullable; + +import java.lang.reflect.Array; +import java.util.List; +import java.util.SequencedCollection; +import java.util.Set; +import java.util.stream.Collectors; + +/** + * An executor for Brigadier commands. + */ +@ApiStatus.Internal +public class ScriptCommandExecutor { + + private static void setVariable(String name, Object value, boolean isSingle, Event context) { + if (isSingle) { + Variables.setVariable(name, value, context, true); + } else { + Object[] values = ((Object[]) value); + int length = values.length; + for (int i = 0; i < length; i++) { + Variables.setVariable(name + "::" + (i + 1), values[i], context, true); + } + } + } + + private final Trigger trigger; + private final List> arguments; + private final @Nullable CooldownManager cooldownManager; + + public ScriptCommandExecutor(Trigger trigger, List> arguments, @Nullable CooldownManager cooldownManager) { + this.trigger = trigger; + this.arguments = arguments; + this.cooldownManager = cooldownManager; + } + + public @Nullable CooldownManager getCooldownManager() { + return cooldownManager; + } + + public int execute(CommandContext context) throws CommandSyntaxException { + CommandSourceStack source = context.getSource(); + ScriptCommandEvent commandEvent = + new ScriptCommandEvent(context.getNodes().getFirst().getNode().getName(), context.getInput(), this, source); + + // final validations + if (cooldownManager != null && !cooldownManager.checkExecution(commandEvent, source.getSender())) { + return Command.SINGLE_SUCCESS; + } + + // argument assembly + Set providedArgs = context.getNodes().stream() + .filter(node -> node.getNode() instanceof ArgumentCommandNode) + .map(node -> node.getNode().getName()) + .collect(Collectors.toSet()); + for (ArgumentData argument : arguments) { + Object value = null; + if (providedArgs.contains(argument.name())) { + value = context.getArgument(argument.name(), Object.class); // we manually handle type validation + if (value instanceof ArgumentResolver argumentResolver) { // native type needs resolved + value = argumentResolver.resolve(context.getSource()); + if (value instanceof SequencedCollection collection) { + if (argument.isSingle()) { // many single arguments are still provided as lists + value = collection.getFirst(); + } else { + value = collection.toArray((Object[]) Array.newInstance(argument.type().getC(), collection.size())); + } + } + } + } else if (argument.defaultValue() != null) { // fallback to default value + if (argument.isSingle()) { + value = argument.defaultValue().getSingle(commandEvent); + } else { + value = argument.defaultValue().getArray(commandEvent); + } + } + + if (value != null) { + commandEvent.arguments.put(argument.name(), value); + if (!argument.isAutomaticName()) { // store explicitly named arguments as variables + setVariable(argument.name(), value, argument.isSingle(), commandEvent); + } + } + } + + // execution + trigger.execute(commandEvent); + + return Command.SINGLE_SUCCESS; + } + +} diff --git a/src/main/java/org/skriptlang/skript/bukkit/command/custom/ScriptCommandRegistrar.java b/src/main/java/org/skriptlang/skript/bukkit/command/custom/ScriptCommandRegistrar.java new file mode 100644 index 00000000000..d8e722eaa66 --- /dev/null +++ b/src/main/java/org/skriptlang/skript/bukkit/command/custom/ScriptCommandRegistrar.java @@ -0,0 +1,336 @@ +package org.skriptlang.skript.bukkit.command.custom; + +import ch.njol.skript.Skript; +import io.papermc.paper.command.brigadier.Commands; +import io.papermc.paper.plugin.configuration.PluginMeta; +import io.papermc.paper.plugin.lifecycle.event.types.LifecycleEvents; +import org.bukkit.Bukkit; +import org.bukkit.command.Command; +import org.bukkit.command.CommandMap; +import org.bukkit.help.GenericCommandHelpTopic; +import org.bukkit.help.HelpMap; +import org.bukkit.help.HelpTopic; +import org.bukkit.help.HelpTopicComparator; +import org.bukkit.help.IndexHelpTopic; +import org.bukkit.plugin.java.JavaPlugin; +import org.jetbrains.annotations.ApiStatus; +import org.jetbrains.annotations.Nullable; + +import java.lang.invoke.MethodHandle; +import java.lang.invoke.MethodHandles; +import java.lang.invoke.MethodType; +import java.lang.reflect.InvocationHandler; +import java.lang.reflect.Method; +import java.lang.reflect.Proxy; +import java.util.List; +import java.util.Map; +import java.util.Set; +import java.util.TreeSet; +import java.util.concurrent.ConcurrentHashMap; +import java.util.stream.Stream; + +/** + * Utility class for registering Brigadier commands at runtime through reflection. + * This avoids a full reload via {@link Bukkit#reloadData()}. + * However, that approach will be used if the reflection-based approach fails to load. + */ +public final class ScriptCommandRegistrar { + + private static JavaPlugin plugin; + + private static Commands commandRegistrar; + private static final Map> REGISTERED_COMMANDS = new ConcurrentHashMap<>(); + private static final Set PENDING_REGISTRATIONS = ConcurrentHashMap.newKeySet(); + private static final Set PENDING_UNREGISTRATIONS = ConcurrentHashMap.newKeySet(); + + private static boolean useSafeReload; + private static @Nullable MethodHandle SET_VALID; + private static @Nullable MethodHandle INVALIDATE; + private static @Nullable MethodHandle REMOVE_COMMAND; + private static @Nullable MethodHandle SYNC_COMMANDS; + + private static final SkriptIndexHelpTopic indexHelpTopic = new SkriptIndexHelpTopic(); + + @ApiStatus.Internal + public static void init(JavaPlugin plugin) { + plugin.getLifecycleManager().registerEventHandler(LifecycleEvents.COMMANDS, commands -> { + commandRegistrar = commands.registrar(); + + if (!REGISTERED_COMMANDS.isEmpty() || !PENDING_REGISTRATIONS.isEmpty()) { + REGISTERED_COMMANDS.replaceAll((command, ignored) -> + commandRegistrar.register(command.node(), command.description(), command.aliases())); + processRegistrationSet(); + // once command registration has finished, and new help initialized, add in our custom entries + Bukkit.getScheduler().runTask(plugin, () -> { + HelpMap helpMap = Bukkit.getHelpMap(); + indexHelpTopic.clear(); + indexHelpTopic.replaceExisting(helpMap); + CommandMap commandMap = Bukkit.getCommandMap(); + REGISTERED_COMMANDS.forEach((command, labels) -> + registerHelp(helpMap, commandMap, command, labels)); + }); + return; + } + + if (ScriptCommandRegistrar.plugin != null) { + return; + } + ScriptCommandRegistrar.plugin = plugin; + + MethodHandles.Lookup lookup = MethodHandles.lookup(); + Class registrarClass = commandRegistrar.getClass(); + try { + SET_VALID = lookup.findVirtual(registrarClass, "setValid", MethodType.methodType(void.class)); + INVALIDATE = lookup.findVirtual(registrarClass, "invalidate", MethodType.methodType(void.class)); + Class rootClass = commandRegistrar.getDispatcher().getRoot().getClass(); + REMOVE_COMMAND = MethodHandles.privateLookupIn(rootClass, lookup) + .findVirtual(rootClass, "removeCommand", MethodType.methodType(void.class, String.class)); + SYNC_COMMANDS = lookup.findVirtual(Bukkit.getServer().getClass(), "syncCommands", MethodType.methodType(void.class)); + } catch (NoSuchMethodException | IllegalAccessException e) { + useSafeReload = true; + } + + // we have to delay replacing the existing help index entry since it does not yet exist + Bukkit.getScheduler().runTask(plugin, () -> indexHelpTopic.replaceExisting(Bukkit.getHelpMap())); + }); + } + + /** + * Adds the {@code command} to the registration queue. + * To finalize registration, the queue must be processed using {@link #processRegistrations()}. + * @param command The command to register. + * @see #processRegistrations() + */ + public static void register(ScriptBrigadierCommand command) { + PENDING_REGISTRATIONS.add(command); + } + + /** + * Processes all pending registrations, synchronizing them with the server's command dispatcher. + * @see #register(ScriptBrigadierCommand) + */ + public static void processRegistrations() { + if (!Bukkit.isPrimaryThread()) { + Bukkit.getScheduler().runTask(plugin, ScriptCommandRegistrar::processRegistrations); + return; + } + + if (!PENDING_UNREGISTRATIONS.isEmpty()) { + processUnregistrations(); + } + + if (useSafeReload) { + Bukkit.reloadData(); + return; + } + assert SET_VALID != null && INVALIDATE != null && SYNC_COMMANDS != null; + + try { + SET_VALID.invoke(commandRegistrar); + processRegistrationSet(); + INVALIDATE.invoke(commandRegistrar); + SYNC_COMMANDS.invoke(Bukkit.getServer()); + } catch (Throwable e) { + throw Skript.exception(e); + } + } + + private static void processRegistrationSet() { + PluginMeta pluginMeta = plugin.getPluginMeta(); + HelpMap helpMap = Bukkit.getHelpMap(); + CommandMap commandMap = Bukkit.getCommandMap(); + for (ScriptBrigadierCommand command : PENDING_REGISTRATIONS) { + if (command.namespace() == null) { + REGISTERED_COMMANDS.put(command, commandRegistrar.register(pluginMeta, command.node(), command.description(), command.aliases())); + } else { + TemporaryNamePluginMeta handler = new TemporaryNamePluginMeta(pluginMeta, command.namespace()); + PluginMeta meta = (PluginMeta) Proxy.newProxyInstance(pluginMeta.getClass().getClassLoader(), + new Class[]{PluginMeta.class}, handler); + REGISTERED_COMMANDS.put(command, commandRegistrar.register(meta, command.node(), command.description(), command.aliases())); + handler.useAlternativeName = false; + } + if (!useSafeReload) { // safe reloads process help differently at a later time + registerHelp(helpMap, commandMap, command, REGISTERED_COMMANDS.get(command)); + } + } + PENDING_REGISTRATIONS.clear(); + } + + private static class TemporaryNamePluginMeta implements InvocationHandler { + + final PluginMeta source; + final String name; + boolean useAlternativeName = true; + + public TemporaryNamePluginMeta(PluginMeta source, String name) { + this.source = source; + this.name = name; + } + + @Override + public Object invoke(Object proxy, Method method, Object[] args) throws Throwable { + if (useAlternativeName) { + String methodName = method.getName(); + if (methodName.equals("getName") || methodName.equals("namespace")) { + return name; + } + } + return method.invoke(source, args); + } + + } + + private static void registerHelp(HelpMap helpMap, CommandMap commandMap, ScriptBrigadierCommand command, Set labels) { + if (useSafeReload) { // remove existing topics, only needed for safe reload which is delayed + helpMap.getHelpTopics().removeAll(labels.stream() + .map(label -> { + // since these topics were created using GenericCommandHelpTopic, a slash is only added as a prefix + // if the label does not already start with one + if (label.charAt(0) != '/') { + label = '/' + label; + } + HelpTopic topic = helpMap.getHelpTopic(label); + if (topic == null && label.charAt(0) == '/') { + // in some cases, it *is* SkriptGenericCommandHelpTopic - only during shutdown? + topic = helpMap.getHelpTopic('/' + label); + } + return topic; + }) + .toList()); + } + + // register new help topics + for (String label : labels) { + Command bukkitCommand = commandMap.getCommand(label); + assert bukkitCommand != null; + if (command.usage() != null) { + bukkitCommand.setUsage(command.usage()); + } + + HelpTopic newTopic = new SkriptGenericCommandHelpTopic(bukkitCommand); + helpMap.addTopic(newTopic); + indexHelpTopic.add(newTopic); + } + } + + /** + * Adds the {@code command} to the unregistration queue. + * To finalize unregistration, the queue must be processed using {@link #processUnregistrations()}. + * @param command The command to unregister. + * @see #processUnregistrations() + */ + public static void unregister(ScriptBrigadierCommand command) { + PENDING_UNREGISTRATIONS.addAll(REGISTERED_COMMANDS.remove(command)); + } + + /** + * Processes all pending unregistrations, synchronizing them with the server's command dispatcher. + * @see #unregister(ScriptBrigadierCommand) + */ + public static void processUnregistrations() { + if (!Bukkit.isPrimaryThread()) { + Bukkit.getScheduler().runTask(plugin, ScriptCommandRegistrar::processUnregistrations); + return; + } + + if (useSafeReload) { + PENDING_UNREGISTRATIONS.clear(); + Bukkit.reloadData(); + return; + } + assert SET_VALID != null && INVALIDATE != null && REMOVE_COMMAND != null && SYNC_COMMANDS != null; + + try { + SET_VALID.invoke(commandRegistrar); + var root = commandRegistrar.getDispatcher().getRoot(); + for (String command : PENDING_UNREGISTRATIONS) { + REMOVE_COMMAND.invoke(root, command); + } + + // unregister help + HelpMap helpMap = Bukkit.getHelpMap(); + List topics = PENDING_UNREGISTRATIONS.stream() + .map(label -> helpMap.getHelpTopic("/" + label)) + .toList(); + helpMap.getHelpTopics().removeAll(topics); + indexHelpTopic.remove(topics); + + PENDING_UNREGISTRATIONS.clear(); + INVALIDATE.invoke(commandRegistrar); + SYNC_COMMANDS.invoke(Bukkit.getServer()); + } catch (Throwable e) { + throw Skript.exception(e); + } + } + + /** + * Obtains a script command by its name. + * @param command The name of the command. + * @return The script command named {@code command}, or null if no script command with that name exists. + */ + public static @Nullable ScriptBrigadierCommand getCommand(String command) { + return Stream.concat(PENDING_REGISTRATIONS.stream(), REGISTERED_COMMANDS.keySet().stream()) + .filter(registration -> registration.node().getLiteral().equals(command)) + .findFirst() + .orElse(null); + } + + /** + * @return All commands registered with this registrar. + */ + public static Set getCommands() { + return Set.copyOf(REGISTERED_COMMANDS.keySet()); + } + + private static class SkriptIndexHelpTopic extends IndexHelpTopic { + + public SkriptIndexHelpTopic() { + super("Skript", "All commands for Skript", null, + new TreeSet<>((lht, rht) -> { + // always force /skript to come first + if (lht.getName().equals("/skript")) { + return -1; + } else if (rht.getName().equals("/skript")) { + return 1; + } + return HelpTopicComparator.helpTopicComparatorInstance().compare(lht, rht); + }), + "Below is a list of all Skript commands:"); + } + + public void replaceExisting(HelpMap helpMap) { + // replace existing index entry + helpMap.getHelpTopics().remove(helpMap.getHelpTopic("Skript")); + helpMap.addTopic(this); + // add back topic for primary plugin command + add(helpMap.getHelpTopic("/skript")); + } + + public void add(HelpTopic topic) { + allTopics.add(topic); + } + + public void remove(List topics) { + allTopics.removeAll(topics); + } + + public void clear() { + allTopics.clear(); + } + + } + + private static class SkriptGenericCommandHelpTopic extends GenericCommandHelpTopic { + + public SkriptGenericCommandHelpTopic(Command command) { + super(command); + // we need to handle slashes intentionally included as part of the label + // essentially, prefix with a slash no matter what + if (command.getLabel().charAt(0) == '/') { + this.name = "/" + this.name; + } + } + + } + +} diff --git a/src/main/java/org/skriptlang/skript/bukkit/command/elements/conditions/CondIsScriptCommand.java b/src/main/java/org/skriptlang/skript/bukkit/command/elements/conditions/CondIsScriptCommand.java new file mode 100644 index 00000000000..44616b4bb8b --- /dev/null +++ b/src/main/java/org/skriptlang/skript/bukkit/command/elements/conditions/CondIsScriptCommand.java @@ -0,0 +1,38 @@ +package org.skriptlang.skript.bukkit.command.elements.conditions; + +import ch.njol.skript.conditions.base.PropertyCondition; +import ch.njol.skript.doc.Description; +import ch.njol.skript.doc.Example; +import ch.njol.skript.doc.Name; +import ch.njol.skript.doc.Since; +import org.skriptlang.skript.bukkit.command.custom.ScriptCommandRegistrar; +import org.skriptlang.skript.registration.SyntaxRegistry; + +@Name("Is a Script Command") +@Description("Checks whether a command (string label) is a script command (one registered in a script).") +@Example(""" + on command: + command is not a script command + # do something only for non-custom commands + """) +@Since("2.6") +public class CondIsScriptCommand extends PropertyCondition { + + public static void register(SyntaxRegistry syntaxRegistry) { + syntaxRegistry.register(SyntaxRegistry.CONDITION, + infoBuilder(CondIsScriptCommand.class, PropertyType.BE, "[a] s(c|k)ript (command|cmd)", "string") + .supplier(CondIsScriptCommand::new) + .build()); + } + + @Override + public boolean check(String command) { + return ScriptCommandRegistrar.getCommand(command) != null; + } + + @Override + protected String getPropertyName() { + return "script command"; + } + +} diff --git a/src/main/java/org/skriptlang/skript/bukkit/command/elements/effects/EffCancelCooldown.java b/src/main/java/org/skriptlang/skript/bukkit/command/elements/effects/EffCancelCooldown.java new file mode 100644 index 00000000000..15ecfdd0095 --- /dev/null +++ b/src/main/java/org/skriptlang/skript/bukkit/command/elements/effects/EffCancelCooldown.java @@ -0,0 +1,78 @@ +package org.skriptlang.skript.bukkit.command.elements.effects; + +import ch.njol.skript.Skript; +import ch.njol.skript.lang.EventRestrictedSyntax; +import ch.njol.skript.lang.SkriptParser.ParseResult; +import ch.njol.skript.util.Date; +import ch.njol.util.coll.CollectionUtils; +import org.bukkit.event.Event; +import org.jetbrains.annotations.Nullable; + +import ch.njol.skript.doc.Description; +import ch.njol.skript.doc.Example; +import ch.njol.skript.doc.Name; +import ch.njol.skript.doc.Since; +import ch.njol.skript.lang.Effect; +import ch.njol.skript.lang.Expression; +import ch.njol.util.Kleenean; +import org.skriptlang.skript.bukkit.command.custom.CommandParsingData; +import org.skriptlang.skript.bukkit.command.custom.CommandParsingData.ExecutorData; +import org.skriptlang.skript.bukkit.command.custom.ScriptCommandEvent; +import org.skriptlang.skript.registration.SyntaxInfo; +import org.skriptlang.skript.registration.SyntaxRegistry; + +@Name("Cancel Command Cooldown") +@Description("Only usable in commands. Makes it so the current command usage isn't counted towards the cooldown.") +@Example(""" + command /nick : + executable by: players + cooldown: 10 seconds + trigger: + if length of arg-1 is more than 16: + # Makes it so that invalid arguments don't make you wait for the cooldown again + cancel the cooldown + send "Your nickname may be at most 16 characters." + stop + set the player's display name to arg-1 + """) +@Since("2.2-dev34") +public class EffCancelCooldown extends Effect implements EventRestrictedSyntax { + + public static void register(SyntaxRegistry syntaxRegistry) { + syntaxRegistry.register(SyntaxRegistry.EFFECT, + SyntaxInfo.simple(EffCancelCooldown.class, EffCancelCooldown::new, + "[:un](cancel|ignore) [the] [current] [command] cooldown")); + } + + private boolean cancel; + + @Override + public boolean init(Expression[] exprs, int matchedPattern, Kleenean isDelayed, ParseResult parseResult) { + cancel = !parseResult.hasTag("un"); + if (getParser().getData(CommandParsingData.class).getExecutorData(ExecutorData::cooldownManager) == null) { + Skript.error("'" + toString(null, false) + "' can't be used because the command doesn't have a cooldown."); + return false; + } + return true; + } + + @Override + public Class[] supportedEvents() { + return CollectionUtils.array(ScriptCommandEvent.class); + } + + @Override + protected void execute(Event event) { + if (event instanceof ScriptCommandEvent commandEvent) { + assert commandEvent.getCommandExecutor().getCooldownManager() != null; + commandEvent.getCommandExecutor().getCooldownManager() + .setStartDate(event, commandEvent.getSender(), cancel ? null : Date.now()); + } + } + + @Override + public String toString(@Nullable Event event, boolean debug) { + return (cancel ? "" : "un") + "cancel the command cooldown"; + } + +} diff --git a/src/main/java/org/skriptlang/skript/bukkit/command/elements/effects/EffCommand.java b/src/main/java/org/skriptlang/skript/bukkit/command/elements/effects/EffCommand.java new file mode 100644 index 00000000000..1a61bc0d084 --- /dev/null +++ b/src/main/java/org/skriptlang/skript/bukkit/command/elements/effects/EffCommand.java @@ -0,0 +1,136 @@ +package org.skriptlang.skript.bukkit.command.elements.effects; + +import ch.njol.skript.effects.EffConnect; +import ch.njol.skript.lang.SyntaxStringBuilder; +import com.mojang.brigadier.exceptions.CommandSyntaxException; +import org.bukkit.Bukkit; +import org.bukkit.command.CommandException; +import org.bukkit.command.CommandSender; +import org.bukkit.entity.Player; +import org.bukkit.event.Event; +import org.bukkit.event.player.PlayerCommandPreprocessEvent; +import org.bukkit.event.server.ServerCommandEvent; +import org.jetbrains.annotations.ApiStatus; +import org.jetbrains.annotations.Nullable; + +import ch.njol.skript.Skript; +import ch.njol.skript.doc.Description; +import ch.njol.skript.doc.Example; +import ch.njol.skript.doc.Name; +import ch.njol.skript.doc.Since; +import ch.njol.skript.lang.Effect; +import ch.njol.skript.lang.Expression; +import ch.njol.skript.lang.SkriptParser.ParseResult; +import ch.njol.skript.lang.VariableString; +import ch.njol.skript.util.StringMode; +import ch.njol.skript.util.Utils; +import ch.njol.util.Kleenean; +import org.skriptlang.skript.log.runtime.RuntimeErrorProducer; +import org.skriptlang.skript.registration.SyntaxInfo; +import org.skriptlang.skript.registration.SyntaxRegistry; + +@Name("Command") +@Description(""" + Executes a command. \ + This can be useful to use other plugin in triggers. + If the command is a BungeeCord side command, \ + you can use the '[bungeecord]' keyword option to execute the command on the proxy. + """) +@Example("make player execute command \"/home\"") +@Example("execute console command \"/say Hello everyone!\"") +@Example("execute player bungeecord command \"/alert &6Testing Announcement!\"") +@Since("1.0, 2.8.0 (BungeeCord command)") +public class EffCommand extends Effect { + + private static final String MESSAGE_CHANNEL = "Message"; + + public static void register(SyntaxRegistry syntaxRegistry) { + syntaxRegistry.register(SyntaxRegistry.EFFECT, + SyntaxInfo.simple(EffCommand.class, EffCommand::new, + "execute [the] [bungee:bungee[cord]] command[s] %strings% [by %-commandsenders%]", + "execute [the] %commandsenders% [bungee:bungee[cord]] command[s] %strings%", + "(let|make) %commandsenders% execute [[the] [bungee:bungee[cord]] command[s]] %strings%")); + } + + private Expression commands; + private @Nullable Expression senders; + private boolean bungeecord; + + @Override + @SuppressWarnings("unchecked") + public boolean init(Expression[] exprs, int matchedPattern, Kleenean isDelayed, ParseResult parseResult) { + if (matchedPattern == 0) { + commands = (Expression) exprs[0]; + senders = (Expression) exprs[1]; + } else { + senders = (Expression) exprs[0]; + commands = (Expression) exprs[1]; + } + bungeecord = parseResult.hasTag("bungee"); + if (bungeecord && senders == null) { + Skript.error("You must specify a player command sender when executing a BungeeCord command."); + return false; + } + commands = VariableString.setStringMode(commands, StringMode.COMMAND); + return true; + } + + @Override + public void execute(Event event) { + CommandSender[] senders = this.senders == null ? new CommandSender[]{Bukkit.getConsoleSender()} : this.senders.getArray(event); + for (String command : commands.getArray(event)) { + if (command.startsWith("/")) { + command = command.substring(1); + } + for (CommandSender sender : senders) { + if (bungeecord) { + if (!(sender instanceof Player player)) { + continue; + } + Utils.sendPluginMessage(player, EffConnect.BUNGEE_CHANNEL, MESSAGE_CHANNEL, player.getName(), "/" + command); + } else { + dispatchCommand(sender, command, this); + } + } + } + } + + @Override + public String toString(@Nullable Event event, boolean debug) { + return new SyntaxStringBuilder(event, debug) + .append("execute the") + .appendIf(bungeecord, "bungeecord") + .append("command", commands) + .appendIf(senders != null, "by", senders) + .toString(); + } + + @ApiStatus.Internal + public static boolean dispatchCommand(CommandSender sender, String command, RuntimeErrorProducer errorProducer) { + try { + if (sender instanceof Player player) { + PlayerCommandPreprocessEvent event = new PlayerCommandPreprocessEvent(player, "/" + command); + Bukkit.getPluginManager().callEvent(event); + if (event.isCancelled() || !event.getMessage().startsWith("/")) { + return false; + } + return Bukkit.dispatchCommand(event.getPlayer(), event.getMessage().substring(1)); + } else { + ServerCommandEvent event = new ServerCommandEvent(sender, command); + Bukkit.getPluginManager().callEvent(event); + if (event.getCommand().isEmpty() || event.isCancelled()) { + return false; + } + return Bukkit.dispatchCommand(event.getSender(), event.getCommand()); + } + } catch (CommandException ex) { + if (ex.getCause() instanceof CommandSyntaxException commandSyntaxException) { + errorProducer.error("Failed to execute command: " + commandSyntaxException.getMessage()); + } else { + errorProducer.error("Failed to execute command: " + ex.getMessage()); + } + return false; + } + } + +} diff --git a/src/main/java/org/skriptlang/skript/bukkit/command/elements/expressions/ExprAllCommands.java b/src/main/java/org/skriptlang/skript/bukkit/command/elements/expressions/ExprAllCommands.java new file mode 100644 index 00000000000..703839212f2 --- /dev/null +++ b/src/main/java/org/skriptlang/skript/bukkit/command/elements/expressions/ExprAllCommands.java @@ -0,0 +1,65 @@ +package org.skriptlang.skript.bukkit.command.elements.expressions; + +import org.bukkit.Bukkit; +import org.bukkit.event.Event; +import org.jetbrains.annotations.Nullable; + +import ch.njol.skript.doc.Description; +import ch.njol.skript.doc.Example; +import ch.njol.skript.doc.Name; +import ch.njol.skript.doc.Since; +import ch.njol.skript.lang.Expression; +import ch.njol.skript.lang.SkriptParser.ParseResult; +import ch.njol.skript.lang.util.SimpleExpression; +import ch.njol.util.Kleenean; +import org.skriptlang.skript.bukkit.command.custom.ScriptCommandRegistrar; +import org.skriptlang.skript.registration.SyntaxInfo; +import org.skriptlang.skript.registration.SyntaxRegistry; + +@Name("All Commands") +@Description("Returns all registered commands or all script commands.") +@Example("send \"Number of all commands: %size of all commands%\"") +@Example("send \"Number of all script commands: %size of all script commands%\"") +@Since("2.6") +public class ExprAllCommands extends SimpleExpression { + + public static void register(SyntaxRegistry syntaxRegistry) { + syntaxRegistry.register(SyntaxRegistry.EXPRESSION, + SyntaxInfo.Expression.simple(ExprAllCommands.class, ExprAllCommands::new, String.class, + "[all [[of] the]|the] [registered] [:script] commands")); + } + + private boolean scriptCommandsOnly; + + @Override + public boolean init(Expression[] expressions, int matchedPattern, Kleenean isDelayed, ParseResult parseResult) { + scriptCommandsOnly = parseResult.hasTag("script"); + return true; + } + + @Override + protected String[] get(Event e) { + if (scriptCommandsOnly) { + return ScriptCommandRegistrar.getCommands().stream() + .map(command -> command.node().getLiteral()) + .toArray(String[]::new); + } + return Bukkit.getCommandMap().getKnownCommands().keySet().toArray(String[]::new); + } + + @Override + public boolean isSingle() { + return false; + } + + @Override + public Class getReturnType() { + return String.class; + } + + @Override + public String toString(@Nullable Event event, boolean debug) { + return "all " + (scriptCommandsOnly ? "script " : "") + "commands"; + } + +} diff --git a/src/main/java/org/skriptlang/skript/bukkit/command/elements/expressions/ExprArgument.java b/src/main/java/org/skriptlang/skript/bukkit/command/elements/expressions/ExprArgument.java new file mode 100644 index 00000000000..219c1c289d1 --- /dev/null +++ b/src/main/java/org/skriptlang/skript/bukkit/command/elements/expressions/ExprArgument.java @@ -0,0 +1,298 @@ +package org.skriptlang.skript.bukkit.command.elements.expressions; + +import ch.njol.skript.Skript; +import ch.njol.skript.classes.ClassInfo; +import ch.njol.skript.doc.Description; +import ch.njol.skript.doc.Example; +import ch.njol.skript.doc.Name; +import ch.njol.skript.doc.Since; +import ch.njol.skript.lang.EventRestrictedSyntax; +import ch.njol.skript.lang.Expression; +import ch.njol.skript.lang.Literal; +import ch.njol.skript.lang.SkriptParser.ParseResult; +import ch.njol.skript.lang.parser.ParserInstance; +import ch.njol.skript.lang.util.SimpleExpression; +import ch.njol.skript.log.ErrorQuality; +import ch.njol.skript.util.Utils; +import ch.njol.util.Kleenean; +import ch.njol.util.StringUtils; +import ch.njol.util.coll.CollectionUtils; +import org.bukkit.event.Event; +import org.bukkit.event.player.PlayerCommandPreprocessEvent; +import org.bukkit.event.server.ServerCommandEvent; +import org.jetbrains.annotations.Nullable; +import org.skriptlang.skript.bukkit.command.custom.ArgumentData; +import org.skriptlang.skript.bukkit.command.elements.structures.util.CommandSuggestionEvent; +import org.skriptlang.skript.bukkit.command.custom.ScriptCommandEvent; +import org.skriptlang.skript.bukkit.command.custom.CommandParsingData; +import org.skriptlang.skript.registration.SyntaxInfo; +import org.skriptlang.skript.registration.SyntaxRegistry; + +import java.lang.reflect.Array; +import java.util.List; +import java.util.regex.MatchResult; + +@Name("Argument") +@Description(""" + A user provided value in a command execution. + For example, if the command "/tell " is used like "/tell Njol Hello Njol!", 'argument 1' is the player named "Njol" and 'argument 2' is the text "Hello Njol!". + One can also use the type of the argument instead of its index to address the argument. + For example, in the command example above, 'player-argument' is the same as 'argument 1'. + Usable in script commands and command events. + Please note that specifying the argument type is only supported in script commands. + """) +@Example("give the item-argument to the player-argument") +@Example("damage the player-argument by the number-argument") +@Example("give a diamond pickaxe to the argument") +@Example("add argument 1 to argument 2") +@Example("heal the last argument") +@Since("1.0, 2.7 (support for command events)") +public class ExprArgument extends SimpleExpression implements EventRestrictedSyntax { + + public static void register(SyntaxRegistry syntaxRegistry) { + syntaxRegistry.register(SyntaxRegistry.EXPRESSION, SyntaxInfo.Expression.simple( + ExprArgument.class, ExprArgument::new, Object.class, + "[the] last arg[ument]", // LAST + "[the] arg[ument](-| )<(\\d+)>", // ORDINAL + "[the] <(\\d*1)st|(\\d*2)nd|(\\d*3)rd|(\\d*[4-90])th> arg[ument][s]", // ORDINAL + "[(all [[of] the]|the)] arg[ument][all:s]", // SINGLE OR ALL + "[the] %*classinfo%( |-)arg[ument][( |-)<\\d+>]", // CLASSINFO + "[the] arg[ument]( |-)%*classinfo%[( |-)<\\d+>]" // CLASSINFO + )); + } + + private enum ArgumentType { + + LAST, ORDINAL, SINGLE, ALL, CLASSINFO + + } + + private ArgumentType type; + private int ordinal = -1; // Available in ORDINAL and sometimes CLASSINFO + + @Nullable ArgumentData argument; + + private boolean couldCauseArithmeticConfusion = false; + + @Override + public boolean init(Expression[] exprs, int matchedPattern, Kleenean isDelayed, ParseResult parseResult) { + ParserInstance parser = getParser(); + boolean scriptCommand = parser.isCurrentEvent(ScriptCommandEvent.class, CommandSuggestionEvent.class); + + type = switch (matchedPattern) { + case 0 -> ArgumentType.LAST; + case 1, 2 -> ArgumentType.ORDINAL; + case 3 -> { + if (parseResult.hasTag("all")) { + yield ArgumentType.ALL; + } + if (parseResult.expr.matches("(the )?arg(ument)?")) { + couldCauseArithmeticConfusion = true; // 'arg-1' could be parsed as 'argument - 1' + } + yield ArgumentType.SINGLE; + } + case 4, 5 -> ArgumentType.CLASSINFO; + default -> throw new IllegalArgumentException("Unexpected matched pattern: " + matchedPattern); + }; + + if (!scriptCommand && type == ArgumentType.CLASSINFO) { + Skript.error("Command event arguments are strings, meaning type specification is useless"); + return false; + } + + List> currentArguments = null; + if (scriptCommand) { + currentArguments = parser.getData(CommandParsingData.class).getArguments(); + if (currentArguments.isEmpty()) { + Skript.error("This command doesn't have any arguments"); + return false; + } + } + + if (type == ArgumentType.ORDINAL) { + // Figure out in which format (1st, 2nd, 3rd, Nth) argument was given in + MatchResult regex = parseResult.regexes.getFirst(); + String argMatch = null; + for (int i = 1; i <= 4; i++) { + argMatch = regex.group(i); + if (argMatch != null) { + break; // Found format + } + } + assert argMatch != null; + ordinal = Utils.parseInt(argMatch); + if (scriptCommand && ordinal > currentArguments.size()) { // Only check if it's a script command as we know nothing of command event arguments + Skript.error("This command doesn't have a " + StringUtils.fancyOrderNumber(ordinal) + " argument", ErrorQuality.SEMANTIC_ERROR); + return false; + } + } + + if (scriptCommand) { // Handle before execution + switch (type) { + case LAST -> argument = currentArguments.getLast(); + case ORDINAL -> argument = currentArguments.get(ordinal - 1); + case SINGLE -> { + if (currentArguments.size() == 1) { + argument = currentArguments.getFirst(); + } else { + Skript.error("This command has multiple arguments, meaning it is not possible to get the 'argument'. Use 'argument 1', 'argument 2', etc. instead"); + return false; + } + } + case ALL -> Skript.error("'arguments' cannot be used for script commands. Use 'argument 1', 'argument 2', etc. instead"); + case CLASSINFO -> { + //noinspection unchecked + ClassInfo info = ((Literal>) exprs[0]).getSingle(); + if (!parseResult.regexes.isEmpty()) { + ordinal = Utils.parseInt(parseResult.regexes.getFirst().group()); + if (ordinal > currentArguments.size()) { + Skript.error("This command doesn't have a " + StringUtils.fancyOrderNumber(ordinal) + " " + info + " argument"); + return false; + } + } + + ArgumentData arg = null; + int argAmount = 0; + for (ArgumentData a : currentArguments) { + if (!info.getC().isAssignableFrom(a.type().getC())) // This argument is not of the required type + continue; + + if (ordinal == -1 && argAmount == 2) { // The user said ' argument' without specifying which, and multiple arguments for the type exist + Skript.error("There are multiple " + type + " arguments in this command", ErrorQuality.SEMANTIC_ERROR); + return false; + } + + arg = a; + + argAmount++; + if (argAmount == ordinal) { // There is argNum argument for the required type (ex: "string argument 2" would exist) + break; + } + } + + if (argAmount == 0) { + Skript.error("There is no " + type + " argument in this command"); + return false; + } else if (ordinal > argAmount) { // The user wanted an argument number that didn't exist for the given type + if (argAmount == 1) { + Skript.error("There is only one " + type + " argument in this command"); + } else { + Skript.error("There are only " + argAmount + " " + type + " arguments in this command"); + } + return false; + } + + // 'arg' will never be null here + argument = arg; + } + } + } + + return true; + } + + @Override + public Class[] supportedEvents() { + // important note: this expression is not actually evaluated for CommandSuggestionEvent + return CollectionUtils.array(ScriptCommandEvent.class, PlayerCommandPreprocessEvent.class, ServerCommandEvent.class, + CommandSuggestionEvent.class); + } + + @Override + protected Object @Nullable [] get(Event event) { + if (event instanceof CommandSuggestionEvent) { + error("Arguments cannot be obtained before the command is executed!"); + assert argument != null; + return (Object[]) Array.newInstance(argument.type().getC(), 0); + } + + if (argument != null) { + Object value = ((ScriptCommandEvent) event).getArgument(argument.name()); + if (argument.isSingle()) { + Object[] result = (Object[]) Array.newInstance(argument.type().getC(), 1); + result[0] = value; + return result; + } else { + return (Object[]) value; + } + } + + String fullCommand; + if (event instanceof PlayerCommandPreprocessEvent preprocessEvent) { + fullCommand = preprocessEvent.getMessage().substring(1).trim(); + } else if (event instanceof ServerCommandEvent serverCommandEvent) { + fullCommand = serverCommandEvent.getCommand().trim(); + } else { + return new String[0]; + } + + String[] arguments; + int firstSpace = fullCommand.indexOf(' '); + if (firstSpace != -1) { + fullCommand = fullCommand.substring(firstSpace + 1); + arguments = fullCommand.split(" "); + } else { // No arguments, just the command + return new String[0]; + } + + return switch (type) { + case LAST -> { + if (arguments.length > 0) { + yield new String[]{arguments[arguments.length - 1]}; + } + yield new String[0]; + } + case ORDINAL -> { + if (arguments.length >= ordinal) { + yield new String[]{arguments[ordinal - 1]}; + } + yield new String[0]; + } + case SINGLE -> { + if (arguments.length == 1) { + yield new String[]{arguments[arguments.length - 1]}; + } + yield new String[0]; + } + case ALL -> arguments; + default -> new String[0]; + }; + } + + @Override + public boolean isSingle() { + return argument == null ? type != ArgumentType.ALL : argument.isSingle(); + } + + @Override + public Class getReturnType() { + return argument == null ? String.class : argument.type().getC(); + } + + @Override + public boolean isLoopOf(String input) { + return input.equalsIgnoreCase("argument"); + } + + @Override + public String toString(@Nullable Event event, boolean debug) { + return switch (type) { + case LAST -> "the last argument"; + case ORDINAL -> "the " + StringUtils.fancyOrderNumber(ordinal) + " argument"; + case SINGLE -> "the argument"; + case ALL -> "the arguments"; + case CLASSINFO -> { + assert argument != null; + yield "the " + argument.type() + " argument " + (ordinal != -1 ? ordinal : ""); + } + }; + } + + /** + * @return whether the expression is 'arg', a single argument that could cause confusion with 'arg-1' being parsed as 'argument - 1'. + */ + public boolean couldCauseArithmeticConfusion() { + return couldCauseArithmeticConfusion; + } + +} diff --git a/src/main/java/org/skriptlang/skript/bukkit/command/elements/expressions/ExprCmdCooldownInfo.java b/src/main/java/org/skriptlang/skript/bukkit/command/elements/expressions/ExprCmdCooldownInfo.java new file mode 100644 index 00000000000..0c9cdc19017 --- /dev/null +++ b/src/main/java/org/skriptlang/skript/bukkit/command/elements/expressions/ExprCmdCooldownInfo.java @@ -0,0 +1,201 @@ +package org.skriptlang.skript.bukkit.command.elements.expressions; + +import java.lang.reflect.Array; + +import ch.njol.skript.Skript; +import ch.njol.skript.classes.Changer.ChangeMode; +import ch.njol.skript.doc.Keywords; +import ch.njol.skript.lang.EventRestrictedSyntax; +import ch.njol.skript.lang.SkriptParser.ParseResult; +import ch.njol.util.coll.CollectionUtils; +import org.bukkit.event.Event; +import org.jetbrains.annotations.Nullable; + +import ch.njol.skript.doc.Description; +import ch.njol.skript.doc.Example; +import ch.njol.skript.doc.Name; +import ch.njol.skript.doc.Since; +import ch.njol.skript.lang.Expression; +import ch.njol.skript.lang.util.SimpleExpression; +import ch.njol.skript.util.Date; +import ch.njol.skript.util.Timespan; +import ch.njol.util.Kleenean; +import org.skriptlang.skript.bukkit.command.custom.CommandParsingData; +import org.skriptlang.skript.bukkit.command.custom.CommandParsingData.ExecutorData; +import org.skriptlang.skript.bukkit.command.custom.CooldownManager; +import org.skriptlang.skript.bukkit.command.custom.ScriptCommandEvent; +import org.skriptlang.skript.registration.SyntaxInfo; +import org.skriptlang.skript.registration.SyntaxRegistry; + +@Name("Script Command Cooldown Information") +@Description(""" + Only usable in script commands. + Represents the cooldown time, the cooldown bypass permission, the remaining cooldown time, the elapsed cooldown time. + """) +@Example(""" + command /home: + cooldown: 10 seconds + cooldown message: You last teleported home %elapsed time% ago, you may teleport home again in %remaining time%. + trigger: + teleport player to {home::%player%} + """) +@Since("2.2-dev33") +@Keywords("") +public class ExprCmdCooldownInfo extends SimpleExpression implements EventRestrictedSyntax { + + public static void register(SyntaxRegistry syntaxRegistry) { + syntaxRegistry.register(SyntaxRegistry.EXPRESSION, + SyntaxInfo.Expression.simple(ExprCmdCooldownInfo.class, ExprCmdCooldownInfo::new, Object.class, + "[the] ((cooldown|wait) time|[wait] time of [the] (cooldown|wait) [(of|for) [the] [current] command])", + "[the] [cooldown] bypass perm[ission] [of [the] (cooldown|wait) [(of|for) [the] [current] command]]", + "[the] remaining [time] [of [the] (cooldown|wait) [(of|for) [the] [current] command]]", + "[the] elapsed [time] [of [the] (cooldown|wait) [(of|for) [the] [current] command]]")); + } + + private enum Type { + COOLDOWN_TIME, BYPASS_PERMISSION, REMAINING_TIME, ELAPSED_TIME + } + + private Type type; + + @Override + public boolean init(Expression[] exprs, int matchedPattern, Kleenean isDelayed, ParseResult parseResult) { + type = Type.values()[matchedPattern]; + CommandParsingData data = getParser().getData(CommandParsingData.class); + if (!data.isParsingCooldownEntry && data.getExecutorData(ExecutorData::cooldownManager) == null) { + Skript.error("'" + toString(null, false) + "' can't be used because the command doesn't have a cooldown."); + } + return true; + } + + @Override + public Class[] supportedEvents() { + return CollectionUtils.array(ScriptCommandEvent.class); + } + + @Override + protected Object[] get(Event event) { + if (!(event instanceof ScriptCommandEvent commandEvent)) { + return (Object[]) Array.newInstance(getReturnType(), 0); + } + + CooldownManager cooldownManager = commandEvent.getCommandExecutor().getCooldownManager(); + assert cooldownManager != null; + + return switch (type) { + case COOLDOWN_TIME -> new Timespan[]{cooldownManager.getCooldown()}; + case BYPASS_PERMISSION -> { + String bypass = cooldownManager.getCooldownBypass(); + yield bypass == null ? new String[0] : new String[]{bypass}; + } + case REMAINING_TIME -> { + Date startDate = cooldownManager.getStartDate(event, commandEvent.getSender()); + Date now = Date.now(); + Timespan remaining; + if (startDate == null) { + remaining = new Timespan(0); + } else { + remaining = cooldownManager.getCooldown().subtract(now.difference(startDate)); + } + yield new Timespan[]{remaining}; + } + case ELAPSED_TIME -> { + Date startDate = cooldownManager.getStartDate(event, commandEvent.getSender()); + Date now = Date.now(); + Timespan elapsed; + if (startDate == null) { + elapsed = new Timespan(0); + } else { + elapsed = now.difference(startDate); + } + yield new Timespan[]{elapsed}; + } + }; + } + + @Override + public Class @Nullable [] acceptChange(ChangeMode mode) { + return switch (type) { + case REMAINING_TIME, ELAPSED_TIME -> switch (mode) { + case ADD, SET, REMOVE, DELETE, RESET -> CollectionUtils.array(Timespan.class); + default -> null; + }; + default -> null; + }; + } + + @Override + public void change(Event event, Object @Nullable [] delta, ChangeMode mode) { + if (!(event instanceof ScriptCommandEvent commandEvent)) { + return; + } + + CooldownManager cooldownManager = commandEvent.getCommandExecutor().getCooldownManager(); + assert cooldownManager != null; + + Date startDate = cooldownManager.getStartDate(event, commandEvent.getSender()); + if (startDate == null) { + return; + } + + boolean isRemaining = type == Type.REMAINING_TIME; + + Timespan time; + Date now = Date.now(); + if (isRemaining) { + time = cooldownManager.getCooldown().subtract(now.difference(startDate)); + } else { + time = now.difference(startDate); + } + + time = switch (mode) { + case ADD -> { + assert delta != null; + yield time.add((Timespan) delta[0]); + } + case SET -> { + assert delta != null; + yield (Timespan) delta[0]; + } + case REMOVE -> { + assert delta != null; + yield time.subtract((Timespan) delta[0]); + } + case DELETE -> new Timespan(0); + case RESET -> isRemaining ? cooldownManager.getCooldown() : new Timespan(0); + default -> throw new IllegalStateException("Unexpected value: " + mode); + }; + + if (isRemaining) { + if (time.compareTo(cooldownManager.getCooldown()) > 0) { // cap remaining time at cooldown max + time = cooldownManager.getCooldown(); + } + // convert to elapsed time + time = cooldownManager.getCooldown().subtract(time); + } + + now.subtract(time); + cooldownManager.setStartDate(event, commandEvent.getSender(), now); + } + + @Override + public boolean isSingle() { + return true; + } + + @Override + public Class getReturnType() { + return type == Type.BYPASS_PERMISSION ? String.class : Timespan.class; + } + + @Override + public String toString(@Nullable Event event, boolean debug) { + return switch (type) { + case COOLDOWN_TIME -> "the cooldown time"; + case BYPASS_PERMISSION -> "the bypass permission of the cooldown"; + case REMAINING_TIME -> "the remaining time of the cooldown"; + case ELAPSED_TIME -> "the elapsed time of the cooldown"; + }; + } + +} diff --git a/src/main/java/ch/njol/skript/expressions/ExprCommand.java b/src/main/java/org/skriptlang/skript/bukkit/command/elements/expressions/ExprCommand.java similarity index 62% rename from src/main/java/ch/njol/skript/expressions/ExprCommand.java rename to src/main/java/org/skriptlang/skript/bukkit/command/elements/expressions/ExprCommand.java index e570413b451..a7e80557069 100644 --- a/src/main/java/ch/njol/skript/expressions/ExprCommand.java +++ b/src/main/java/org/skriptlang/skript/bukkit/command/elements/expressions/ExprCommand.java @@ -1,6 +1,5 @@ -package ch.njol.skript.expressions; +package org.skriptlang.skript.bukkit.command.elements.expressions; -import ch.njol.skript.command.ScriptCommandEvent; import ch.njol.skript.lang.EventRestrictedSyntax; import ch.njol.util.coll.CollectionUtils; import org.bukkit.event.Event; @@ -8,21 +7,19 @@ import org.bukkit.event.server.ServerCommandEvent; import org.jetbrains.annotations.Nullable; -import ch.njol.skript.Skript; import ch.njol.skript.doc.Description; import ch.njol.skript.doc.Events; import ch.njol.skript.doc.Example; import ch.njol.skript.doc.Name; import ch.njol.skript.doc.Since; import ch.njol.skript.lang.Expression; -import ch.njol.skript.lang.ExpressionType; import ch.njol.skript.lang.SkriptParser.ParseResult; import ch.njol.skript.lang.util.SimpleExpression; import ch.njol.util.Kleenean; +import org.skriptlang.skript.bukkit.command.custom.ScriptCommandEvent; +import org.skriptlang.skript.registration.SyntaxInfo; +import org.skriptlang.skript.registration.SyntaxRegistry; -/** - * @author Peter Güttinger - */ @Name("Command") @Description("The command that caused an 'on command' event (excluding the leading slash and all arguments)") @Example(""" @@ -37,15 +34,15 @@ @Events("command") public class ExprCommand extends SimpleExpression implements EventRestrictedSyntax { - static { - Skript.registerExpression(ExprCommand.class, String.class, ExpressionType.SIMPLE, + public static void register(SyntaxRegistry syntaxRegistry) { + syntaxRegistry.register(SyntaxRegistry.EXPRESSION, + SyntaxInfo.Expression.simple(ExprCommand.class, ExprCommand::new, String.class, "[the] (full|complete|whole) command", - "[the] command [(label|alias)]" - ); + "[the] command [label|alias]")); } private boolean fullCommand; - + @Override public boolean init(Expression[] exprs, int matchedPattern, Kleenean isDelayed, ParseResult parseResult) { fullCommand = matchedPattern == 0; @@ -56,41 +53,35 @@ public boolean init(Expression[] exprs, int matchedPattern, Kleenean isDelaye public Class[] supportedEvents() { return CollectionUtils.array(PlayerCommandPreprocessEvent.class, ServerCommandEvent.class, ScriptCommandEvent.class); } - - @Override - @Nullable - protected String[] get(final Event e) { - final String s; - - if (e instanceof PlayerCommandPreprocessEvent) { - s = ((PlayerCommandPreprocessEvent) e).getMessage().substring(1).trim(); - } else if (e instanceof ServerCommandEvent) { - s = ((ServerCommandEvent) e).getCommand().trim(); - } else { // It's a script command event - ScriptCommandEvent event = (ScriptCommandEvent) e; - s = event.getCommandLabel() + " " + event.getArgsString(); - } + @Override + protected String[] get(Event event) { + String input = switch (event) { + case PlayerCommandPreprocessEvent preprocessEvent -> preprocessEvent.getMessage().substring(1).trim(); + case ServerCommandEvent serverCommandEvent -> serverCommandEvent.getCommand().trim(); + case ScriptCommandEvent scriptCommandEvent -> scriptCommandEvent.getRawInput(); + default -> throw new IllegalStateException("Unexpected value: " + event); + }; if (fullCommand) { - return new String[]{s}; + return new String[]{input}; } else { - int c = s.indexOf(' '); - return new String[] {c == -1 ? s : s.substring(0, c)}; + int space = input.indexOf(' '); + return new String[] {space == -1 ? input : input.substring(0, space)}; } } - + @Override public boolean isSingle() { return true; } - + @Override public Class getReturnType() { return String.class; } - + @Override - public String toString(@Nullable Event e, boolean debug) { + public String toString(@Nullable Event event, boolean debug) { return fullCommand ? "the full command" : "the command"; } diff --git a/src/main/java/org/skriptlang/skript/bukkit/command/elements/expressions/ExprCommandExecutor.java b/src/main/java/org/skriptlang/skript/bukkit/command/elements/expressions/ExprCommandExecutor.java new file mode 100644 index 00000000000..fa08d4c11c0 --- /dev/null +++ b/src/main/java/org/skriptlang/skript/bukkit/command/elements/expressions/ExprCommandExecutor.java @@ -0,0 +1,75 @@ +package org.skriptlang.skript.bukkit.command.elements.expressions; + +import ch.njol.skript.doc.Description; +import ch.njol.skript.doc.Example; +import ch.njol.skript.doc.Name; +import ch.njol.skript.doc.Since; +import ch.njol.skript.lang.EventRestrictedSyntax; +import ch.njol.skript.lang.Expression; +import ch.njol.skript.lang.SkriptParser.ParseResult; +import ch.njol.skript.lang.util.SimpleExpression; +import ch.njol.util.Kleenean; +import org.bukkit.entity.Entity; +import org.bukkit.event.Event; +import org.jetbrains.annotations.Nullable; +import org.skriptlang.skript.bukkit.command.custom.ScriptCommandEvent; +import org.skriptlang.skript.registration.SyntaxInfo; +import org.skriptlang.skript.registration.SyntaxRegistry; + +@Name("Command Executor") +@Description(""" + The executor of a command. + This differs from the command sender in that the executor may not be the thing that triggered/initiated the command. + The executor of a command is often changed to be different from the command sender by using a vanilla command such as "/execute". + """) +@Example(""" + command /balance: + # This works if you do "/execute as run balance" + # It will send the output to the command sender, but it will be as if "" was the thing executing it. + send "Your balance is %{balance::%uuid of the executor%}%" to the sender + """) +@Since("INSERT VERSION") +public class ExprCommandExecutor extends SimpleExpression implements EventRestrictedSyntax { + + public static void register(SyntaxRegistry syntaxRegistry) { + syntaxRegistry.register(SyntaxRegistry.EXPRESSION, + SyntaxInfo.Expression.simple(ExprCommandExecutor.class, ExprCommandExecutor::new, Entity.class, + "[the] [command['s]] executor")); + } + + @Override + public boolean init(Expression[] expressions, int matchedPattern, Kleenean isDelayed, ParseResult parseResult) { + return true; + } + + @Override + public Class[] supportedEvents() { + //noinspection unchecked + return new Class[]{ScriptCommandEvent.class}; + } + + @Override + protected Entity[] get(Event event) { + Entity executor = null; + if (event instanceof ScriptCommandEvent scriptCommandEvent) { + executor = scriptCommandEvent.getExecutor(); + } + return executor == null ? new Entity[0] : new Entity[]{executor}; + } + + @Override + public boolean isSingle() { + return true; + } + + @Override + public Class getReturnType() { + return Entity.class; + } + + @Override + public String toString(@Nullable Event event, boolean debug) { + return "the command executor"; + } + +} diff --git a/src/main/java/org/skriptlang/skript/bukkit/command/elements/expressions/ExprCommandInfo.java b/src/main/java/org/skriptlang/skript/bukkit/command/elements/expressions/ExprCommandInfo.java new file mode 100644 index 00000000000..75c5a83443a --- /dev/null +++ b/src/main/java/org/skriptlang/skript/bukkit/command/elements/expressions/ExprCommandInfo.java @@ -0,0 +1,221 @@ +package org.skriptlang.skript.bukkit.command.elements.expressions; + +import java.util.Iterator; +import java.util.Locale; +import java.util.Objects; +import java.util.function.Function; +import java.util.stream.Stream; + +import ch.njol.skript.util.Patterns; +import ch.njol.skript.util.Utils; +import com.google.common.collect.Iterators; +import org.bukkit.Bukkit; +import org.bukkit.command.Command; +import org.bukkit.command.CommandMap; +import org.bukkit.command.PluginIdentifiableCommand; +import org.bukkit.command.defaults.BukkitCommand; +import org.bukkit.event.Event; +import org.bukkit.event.player.PlayerCommandPreprocessEvent; +import org.bukkit.event.server.ServerCommandEvent; +import org.jetbrains.annotations.Nullable; + +import ch.njol.skript.Skript; +import ch.njol.skript.doc.Description; +import ch.njol.skript.doc.Example; +import ch.njol.skript.doc.Name; +import ch.njol.skript.doc.Since; +import ch.njol.skript.lang.Expression; +import ch.njol.skript.lang.SkriptParser.ParseResult; +import ch.njol.skript.lang.util.SimpleExpression; +import ch.njol.util.Kleenean; +import org.skriptlang.skript.bukkit.command.custom.ScriptBrigadierCommand; +import org.skriptlang.skript.bukkit.command.custom.ScriptCommandEvent; +import org.skriptlang.skript.bukkit.command.custom.ScriptCommandRegistrar; +import org.skriptlang.skript.lang.script.ScriptWarning; +import org.skriptlang.skript.registration.SyntaxInfo; +import org.skriptlang.skript.registration.SyntaxRegistry; + +@Name("Command Info") +@Description("Get information about a command.") +@Example("main command label of command \"skript\"") +@Example("description of command \"help\"") +@Example("label of command \"pl\"") +@Example("usage of command \"help\"") +@Example("aliases of command \"bukkit:help\"") +@Example("permission of command \"/op\"") +@Example("command \"sk\"'s plugin owner") +@Example(""" + command /greet : + usage: /greet + trigger: + if arg-1 is sender: + send "&cYou can't greet yourself! Usage: %the usage%" + stop + send "%sender% greets you!" to arg-1 + send "You greeted %arg-1%!" + """) +@Since("2.6") +public class ExprCommandInfo extends SimpleExpression { + + private enum InfoType { + + NAME(stream -> stream.map(Command::getName)), + + DESCRIPTION(stream -> stream.map(Command::getDescription)), + + LABEL(stream -> stream.map(Command::getLabel)), + + USAGE(stream -> stream.map(command -> { + ScriptBrigadierCommand scriptCommand = ScriptCommandRegistrar.getCommand(command.getLabel()); + if (scriptCommand != null && scriptCommand.usage() != null) { + return scriptCommand.usage(); + } + return command.getUsage(); + })), + + ALIASES(stream -> stream.flatMap(command -> command.getAliases().stream())), + + PERMISSION(stream -> + stream.map(command -> { + String permission = command.getPermission(); + if (permission == null) { + ScriptBrigadierCommand scriptCommand = ScriptCommandRegistrar.getCommand(command.getLabel()); + if (scriptCommand != null) { + permission = scriptCommand.permission(); + } + } + return permission; + }) + .filter(Objects::nonNull)), + + PERMISSION_MESSAGE(stream -> stream.map(Command::getPermissionMessage) + .filter(Objects::nonNull)), + + PLUGIN(stream -> stream.map(command -> { + if (command instanceof PluginIdentifiableCommand pluginCommand) { + return pluginCommand.getPlugin().getName(); + } else if (command instanceof BukkitCommand) { + return "Bukkit"; + } + String packageName = command.getClass().getPackage().getName(); + if (packageName.startsWith("org.spigot")) { + return "Spigot"; + } else if (packageName.startsWith("io.papermc.paper") || packageName.startsWith("com.destroystokyo.paper")) { + return "Paper"; + } + return "Unknown"; + })); + + private final Function, Stream> function; + + InfoType(Function, Stream> function) { + this.function = function; + } + + } + + private static final Patterns PATTERNS; + + static { + String prefix = "command[s] %strings%'[s] "; + String suffix = " [of [[the] command[s]] %-strings%]"; + PATTERNS = new Patterns<>( + "[the] main command [label|name]" + suffix, InfoType.NAME, + prefix + "main command [label|name]", InfoType.NAME, + "[the] description" + suffix, InfoType.DESCRIPTION, + prefix + "description", InfoType.DESCRIPTION, + "[the] label" + suffix, InfoType.LABEL, + prefix + "label", InfoType.LABEL, + "[the] usage" + suffix, InfoType.USAGE, + prefix + "usage", InfoType.USAGE, + "[all [[of] the]|the] aliases" + suffix, InfoType.ALIASES, + prefix + "aliases", InfoType.ALIASES, + "[the] permission" + suffix, InfoType.PERMISSION, + prefix + "permission", InfoType.PERMISSION, + "[the] permission message" + suffix, InfoType.PERMISSION_MESSAGE, + prefix + "permission message", InfoType.PERMISSION_MESSAGE, + "[the] plugin [owner]" + suffix, InfoType.PLUGIN, + prefix + "plugin [owner]", InfoType.PLUGIN + ); + } + + public static void register(SyntaxRegistry syntaxRegistry) { + syntaxRegistry.register(SyntaxRegistry.EXPRESSION, + SyntaxInfo.Expression.simple(ExprCommandInfo.class, ExprCommandInfo::new, String.class, PATTERNS.getPatterns())); + } + + private InfoType type; + private @Nullable Expression commandName; + + @Override + public boolean init(Expression[] exprs, int matchedPattern, Kleenean isDelayed, ParseResult parseResult) { + //noinspection unchecked + commandName = (Expression) exprs[0]; + if (commandName == null && !getParser().isCurrentEvent(ScriptCommandEvent.class, PlayerCommandPreprocessEvent.class, ServerCommandEvent.class)) { + Skript.error("There's no command in " + Utils.a(getParser().getCurrentEventName()) + " event. Please provide a command"); + return false; + } + + type = PATTERNS.getInfo(matchedPattern); + if (type == InfoType.PERMISSION_MESSAGE) { + ScriptWarning.printDeprecationWarning("Permission messages are deprecated for player executed commands, " + + "as clients are not aware of commands that they cannot execute."); + } + + return true; + } + + @Override + protected String[] get(Event event) { + return stream(event).toArray(String[]::new); + } + + @Override + public @Nullable Iterator iterator(Event event) { + return Iterators.peekingIterator(stream(event).iterator()); + } + + @Override + public Stream stream(Event event) { + return type.function.apply(getCommands(event)); + } + + @Override + public boolean isSingle() { + return type != InfoType.ALIASES && (commandName == null || commandName.isSingle()); + } + + @Override + public Class getReturnType() { + return String.class; + } + + @Override + public String toString(@Nullable Event event, boolean debug) { + return "the " + type.name().toLowerCase(Locale.ENGLISH).replace("_", " ") + + (commandName == null ? "" : " of command " + commandName.toString(event, debug)); + } + + private Stream getCommands(Event event) { + CommandMap commandMap = Bukkit.getCommandMap(); + if (commandName != null) { + return commandName.stream(event) + .map(commandMap::getCommand) + .filter(Objects::nonNull); + } + + String fullCommand = switch (event) { + case ScriptCommandEvent scriptCommandEvent -> scriptCommandEvent.getLabel(); + case ServerCommandEvent serverCommandEvent -> serverCommandEvent.getCommand(); + case PlayerCommandPreprocessEvent preprocessEvent -> preprocessEvent.getMessage().substring(1); + default -> throw new IllegalStateException("Unexpected value: " + event); + }; + System.out.println("FULL COMMAND: " + fullCommand); + String label = fullCommand.split(":")[0]; + System.out.println("FULL LABEL: " + label); + + Command command = commandMap.getCommand(label); + return command == null ? Stream.empty() : Stream.of(command); + } + +} diff --git a/src/main/java/org/skriptlang/skript/bukkit/command/elements/expressions/ExprCommandSender.java b/src/main/java/org/skriptlang/skript/bukkit/command/elements/expressions/ExprCommandSender.java new file mode 100644 index 00000000000..9c88bf1548c --- /dev/null +++ b/src/main/java/org/skriptlang/skript/bukkit/command/elements/expressions/ExprCommandSender.java @@ -0,0 +1,39 @@ +package org.skriptlang.skript.bukkit.command.elements.expressions; + +import org.bukkit.command.CommandSender; + +import ch.njol.skript.doc.Description; +import ch.njol.skript.doc.Events; +import ch.njol.skript.doc.Example; +import ch.njol.skript.doc.Name; +import ch.njol.skript.doc.Since; +import ch.njol.skript.expressions.base.EventValueExpression; +import org.skriptlang.skript.registration.SyntaxRegistry; + +@Name("Command Sender") +@Description(""" + The sender of a command. \ + This differs from the command executor in that this is always the thing that originally triggered/initiated the command. \ + It cannot be changed by commands like "/execute". \ + """) +@Example("make the command sender execute \"/say hi!\"") +@Example(""" + on command: + log "%sender% used command /%command% %arguments%" to "commands.log" + """) +@Since("2.0") +@Events("command") +public class ExprCommandSender extends EventValueExpression { + + public static void register(SyntaxRegistry syntaxRegistry) { + syntaxRegistry.register(SyntaxRegistry.EXPRESSION, + infoBuilder(ExprCommandSender.class, CommandSender.class, "[command['s]] sender") + .supplier(ExprCommandSender::new) + .build()); + } + + public ExprCommandSender() { + super(CommandSender.class); + } + +} diff --git a/src/main/java/org/skriptlang/skript/bukkit/command/elements/expressions/ExprCommandSuggestions.java b/src/main/java/org/skriptlang/skript/bukkit/command/elements/expressions/ExprCommandSuggestions.java new file mode 100644 index 00000000000..ff09033d29d --- /dev/null +++ b/src/main/java/org/skriptlang/skript/bukkit/command/elements/expressions/ExprCommandSuggestions.java @@ -0,0 +1,137 @@ +package org.skriptlang.skript.bukkit.command.elements.expressions; + +import ch.njol.skript.Skript; +import ch.njol.skript.classes.Changer.ChangeMode; +import ch.njol.skript.doc.Description; +import ch.njol.skript.doc.Example; +import ch.njol.skript.doc.Name; +import ch.njol.skript.doc.Since; +import ch.njol.skript.expressions.base.PropertyExpression; +import ch.njol.skript.lang.EventRestrictedSyntax; +import ch.njol.skript.lang.Expression; +import ch.njol.skript.lang.SkriptParser.ParseResult; +import ch.njol.skript.lang.util.SimpleExpression; +import ch.njol.skript.registrations.Classes; +import ch.njol.util.Kleenean; +import org.bukkit.event.Event; +import org.jetbrains.annotations.Nullable; +import org.skriptlang.skript.bukkit.command.elements.structures.util.CommandSuggestionEvent; +import org.skriptlang.skript.registration.SyntaxRegistry; + +import java.util.ArrayList; +import java.util.List; + +@Name("Script Command Suggestions") +@Description(""" + Usable in a script command's "suggestions" entry. + Allows modifying the suggestions displayed while a command is being typed. + """) +@Example(""" + command /menu : + suggestions: + set the text argument's suggestions to "Appetizers", "Entrees", and "Desserts" + trigger: + send "Yum!" + """) +@Since("INSERT VERSION") +public class ExprCommandSuggestions extends SimpleExpression implements EventRestrictedSyntax { + + public static void register(SyntaxRegistry syntaxRegistry) { + syntaxRegistry.register(SyntaxRegistry.EXPRESSION, PropertyExpression.infoBuilder(ExprCommandSuggestions.class, String.class, + "[command] (suggestions|tab completions)", "objects", false) + .supplier(ExprCommandSuggestions::new) + .build()); + } + + private ExprArgument argument; + + @Override + public boolean init(Expression[] expressions, int matchedPattern, Kleenean isDelayed, ParseResult parseResult) { + if (!(expressions[0] instanceof ExprArgument exprArgument) || exprArgument.argument == null) { + Skript.error("It is only possible to obtain or change command suggestions of an argument!"); + return false; + } + argument = exprArgument; + return true; + } + + @Override + public Class[] supportedEvents() { + //noinspection unchecked + return new Class[]{CommandSuggestionEvent.class}; + } + + @Override + protected String @Nullable [] get(Event event) { + assert argument.argument != null; + List suggestions = ((CommandSuggestionEvent) event).suggestions.get(argument.argument.name()); + return suggestions == null ? new String[0] : suggestions.toArray(new String[0]); + } + + @Override + public Class @Nullable [] acceptChange(ChangeMode mode) { + return switch (mode) { + case ADD, SET, REMOVE, DELETE, RESET -> new Class[]{String[].class, argument.getReturnType().arrayType()}; + default -> null; + }; + } + + @Override + public void change(Event event, Object @Nullable [] delta, ChangeMode mode) { + assert argument.argument != null; + String argumentName = argument.argument.name(); + + if (mode == ChangeMode.RESET) { + ((CommandSuggestionEvent) event).suggestions.remove(argumentName); + return; + } + + List currentSuggestions = ((CommandSuggestionEvent) event).suggestions + .computeIfAbsent(argumentName, ignored -> new ArrayList<>()); + + switch (mode) { + case SET: + currentSuggestions.clear(); + //$FALL-THROUGH$ + case ADD: + assert delta != null; + for (Object object : delta) { + if (object instanceof String string) { + currentSuggestions.add(string); + } else { + currentSuggestions.add(Classes.toString(object)); + } + } + break; + case REMOVE: + assert delta != null; + for (Object object : delta) { + if (object instanceof String string) { + currentSuggestions.remove(string); + } else { + currentSuggestions.remove(Classes.toString(object)); + } + } + break; + case DELETE: + currentSuggestions.clear(); + break; + } + } + + @Override + public boolean isSingle() { + return false; + } + + @Override + public Class getReturnType() { + return String.class; + } + + @Override + public String toString(@Nullable Event event, boolean debug) { + return "the command suggestions of " + argument.toString(event, debug); + } + +} diff --git a/src/main/java/org/skriptlang/skript/bukkit/command/elements/structures/StructCommand.java b/src/main/java/org/skriptlang/skript/bukkit/command/elements/structures/StructCommand.java new file mode 100644 index 00000000000..6851d75852a --- /dev/null +++ b/src/main/java/org/skriptlang/skript/bukkit/command/elements/structures/StructCommand.java @@ -0,0 +1,120 @@ +package org.skriptlang.skript.bukkit.command.elements.structures; + +import ch.njol.skript.Skript; +import ch.njol.skript.config.SectionNode; +import ch.njol.skript.lang.Literal; +import ch.njol.skript.lang.SkriptParser.ParseResult; +import ch.njol.skript.registrations.Classes; +import com.mojang.brigadier.tree.LiteralCommandNode; +import io.papermc.paper.command.brigadier.CommandSourceStack; +import org.bukkit.Location; +import org.bukkit.World; +import org.bukkit.block.Block; +import org.bukkit.command.BlockCommandSender; +import org.bukkit.command.CommandSender; +import org.bukkit.event.Event; +import org.jetbrains.annotations.Nullable; +import org.skriptlang.skript.addon.SkriptAddon; +import org.skriptlang.skript.bukkit.command.custom.ScriptCommandRegistrar; +import org.skriptlang.skript.bukkit.command.custom.ScriptBrigadierCommand; +import org.skriptlang.skript.bukkit.command.custom.ScriptCommandEvent; +import org.skriptlang.skript.bukkit.command.elements.structures.util.SubCommandEntryData; +import org.skriptlang.skript.bukkit.lang.eventvalue.EventValue; +import org.skriptlang.skript.bukkit.lang.eventvalue.EventValueRegistry; +import org.skriptlang.skript.lang.entry.EntryContainer; +import org.skriptlang.skript.lang.structure.Structure; +import org.skriptlang.skript.registration.SyntaxInfo; +import org.skriptlang.skript.registration.SyntaxRegistry; + +import java.util.concurrent.atomic.AtomicBoolean; + +public class StructCommand extends Structure { + + public static void register(SkriptAddon addon, SyntaxRegistry syntaxRegistry) { + syntaxRegistry.register(SyntaxRegistry.STRUCTURE,SyntaxInfo.Structure.builder(StructCommand.class) + .supplier(StructCommand::new) + .addPattern("command <.+>") + .build()); + + EventValueRegistry evRegistry = addon.registry(EventValueRegistry.class); + evRegistry.register(EventValue.simple(ScriptCommandEvent.class, CommandSender.class, ScriptCommandEvent::getSender)); + evRegistry.register(EventValue.simple(ScriptCommandEvent.class, String[].class, + event -> event.getArguments().values().stream() + .map(Classes::toString) + .toArray(String[]::new))); + evRegistry.register(EventValue.simple(ScriptCommandEvent.class, Block.class, + event -> event.getSender() instanceof BlockCommandSender sender ? sender.getBlock() : null)); + evRegistry.register(EventValue.simple(ScriptCommandEvent.class, Location.class, ScriptCommandEvent::getLocation)); + evRegistry.register(EventValue.simple(ScriptCommandEvent.class, World.class, event -> event.getLocation().getWorld())); + } + + private static final SubCommandEntryData ROOT_ENTRY_DATA = + new SubCommandEntryData("command", false, false); + + private static final AtomicBoolean SYNC_COMMANDS = new AtomicBoolean(); + + private SectionNode rootNode; + private ScriptBrigadierCommand command; + + @Override + public boolean init(Literal[] args, int matchedPattern, ParseResult parseResult, EntryContainer entryContainer) { + rootNode = entryContainer.getSource(); + return true; + } + + @Override + public boolean load() { + // parsing + var result = ROOT_ENTRY_DATA.getValue(rootNode); + if (result == null) { // parsing failed, entry will have emitted a specific error message + return false; + } + if (result.arguments().size() != 1 || !(result.arguments().getFirst().build() instanceof LiteralCommandNode node)) { + Skript.error("A command must have a name."); + return false; + } + + // validation + ScriptBrigadierCommand existing = ScriptCommandRegistrar.getCommand(node.getLiteral()); + if (existing != null) { + Skript.error("A command with the name /" + node.getLiteral() + " is already defined in the script '" + + existing.script().nameAndPath() + ".sk'"); + return false; + } + + // registration + command = new ScriptBrigadierCommand(getParser().getCurrentScript(), node, result.aliases(), + result.description(), result.usage(), result.prefix(), result.permission()); + ScriptCommandRegistrar.register(command); + SYNC_COMMANDS.set(true); + + return true; + } + + @Override + public boolean postLoad() { + if (SYNC_COMMANDS.compareAndSet(true, false)) { + ScriptCommandRegistrar.processRegistrations(); + } + return true; + } + + @Override + public void unload() { + ScriptCommandRegistrar.unregister(command); + SYNC_COMMANDS.set(true); + } + + @Override + public void postUnload() { + if (SYNC_COMMANDS.compareAndSet(true, false)) { + ScriptCommandRegistrar.processUnregistrations(); + } + } + + @Override + public String toString(@Nullable Event event, boolean debug) { + return "command"; + } + +} diff --git a/src/main/java/org/skriptlang/skript/bukkit/command/elements/structures/util/CommandCompiler.java b/src/main/java/org/skriptlang/skript/bukkit/command/elements/structures/util/CommandCompiler.java new file mode 100644 index 00000000000..85ed4ba67c5 --- /dev/null +++ b/src/main/java/org/skriptlang/skript/bukkit/command/elements/structures/util/CommandCompiler.java @@ -0,0 +1,482 @@ +package org.skriptlang.skript.bukkit.command.elements.structures.util; + +import ch.njol.skript.Skript; +import ch.njol.skript.classes.ClassInfo; +import ch.njol.skript.lang.Expression; +import ch.njol.skript.lang.ParseContext; +import ch.njol.skript.lang.SkriptParser; +import ch.njol.skript.log.RetainingLogHandler; +import ch.njol.skript.registrations.Classes; +import ch.njol.skript.util.Utils; +import org.jetbrains.annotations.Nullable; +import org.skriptlang.skript.bukkit.command.custom.ArgumentData; +import org.skriptlang.skript.bukkit.command.custom.ScriptArgumentType; +import org.skriptlang.skript.bukkit.command.custom.ScriptArgumentType.NativeArgumentData; + +import java.util.ArrayList; +import java.util.Collection; +import java.util.HashSet; +import java.util.LinkedHashSet; +import java.util.List; +import java.util.Set; +import java.util.regex.Matcher; +import java.util.regex.Pattern; +import java.util.stream.Stream; + +/** + * Compiles a string command definition into a tree of literals ({@link LiteralCommandElement}) + * and arguments {@link ArgumentCommandElement}. + * @see #compile(String, List) + */ +final class CommandCompiler { + + /* + * Tree Structures + */ + + /** + * A node within a command tree. + */ + public static class CommandElement { + + protected final Set children; + + private CommandElement() { + this(new HashSet<>()); + } + + private CommandElement(Set children) { + this.children = children; + } + + /** + * @return Elements representing the branches of the command from this element. + * If there is at least one child, it may also contain {@code null}, + * indicating that command execution can occur at this element. + */ + public Collection children() { + return children; + } + + /** + * @return Whether command execution can occur at this element. + */ + public boolean isLeaf() { + return children.isEmpty() || children.contains(null); + } + + /** + * Appends elements to the end of the tree, from this element. + * If the {@link #children()} of an element contains a {@code null}, + * {@code elements} will also be appended to that element. + * @param elements Elements to append. + */ + public void append(Collection elements) { + boolean addToElement = children.isEmpty(); + for (CommandElement child : children) { + if (child == null) { // null child indicates that this element is a command edge + addToElement = true; + } else { + child.append(elements); + } + } + if (addToElement) { + children.remove(null); + for (CommandElement element : elements) { + // for certain nested commands, an element could potentially be appended to itself + // consider: + // A + // |- B + // |- D + // |- C + // |- D + // when appending "E", it will append to the first "D" under "B". + // this "D" is the same instance as the one under "C", so when this method moves + // to next append "E" to "C", it will have already been handled. + if (element != this) { + children.add(element); + } + } + } + } + + } + + /** + * Represents a literal (constant) argument of a command. + */ + public static class LiteralCommandElement extends CommandElement { + + private final String string; + + private LiteralCommandElement(String string) { + super(); + this.string = string; + } + + /** + * @return Literal argument this element represents. + */ + public String literal() { + return string; + } + + } + + /** + * Represents a dynamic argument of a command. + */ + public static class ArgumentCommandElement extends CommandElement { + + private final ArgumentData argument; + + private ArgumentCommandElement(ArgumentData argument) { + super(); + this.argument = argument; + } + + /** + * @return Data describing the argument this element represents. + */ + public ArgumentData argument() { + return argument; + } + + } + + /** + * Internal helper element for building choices during the compilation process. + * Unlike a regular {@link CommandElement}, when this element is appended to, only its last child is appended to. + */ + private static class ChoiceCommandElement extends CommandElement { + + /** + * Placeholder element representing a slot for the next element(s) to append onto. + */ + private static final CommandElement EMPTY = new CommandElement(null); + + private ChoiceCommandElement() { + // we use a linked hash set as ordering is now necessary + super(new LinkedHashSet<>()); + children.add(EMPTY); + } + + @Override + public void append(Collection elements) { + if (elements.isEmpty()) { + return; + } + LinkedHashSet children = (LinkedHashSet) this.children; + CommandElement last = children.getLast(); + if (last == EMPTY) { + children.removeLast(); + children.addAll(elements); + } else { + last.append(elements); + } + } + + public void appendEmpty() { + LinkedHashSet children = (LinkedHashSet) this.children; + if (children.getLast() == EMPTY) { // nothing was ever appended, meaning the previous choice was empty/blank + children.removeLast(); + children.add(null); // meaning this choice group is optional + } + children.add(EMPTY); + } + + @Override + public Collection children() { + LinkedHashSet children = (LinkedHashSet) this.children; + if (children.getLast() == EMPTY) { + children.removeLast(); + children.add(null); + } + return super.children(); + } + + } + + /* + * Compilation + */ + + /** + * + * @param root A plain {@link CommandElement} containing all children. + * For a regular input, such as {@code "heal "}, this element contains a single {@link LiteralCommandElement}. + * @param arguments Data of all arguments contained within the element tree. + */ + public record CompilationResult(CommandElement root, List> arguments) { } + + /** + * Compiles a string command definition into an element tree. + * @param pattern The command definition to compile. + * @param existingArguments Existing arguments to consider during compilation. + * @return A result of the compilation, or null if compilation failed. + */ + public static @Nullable CompilationResult compile(final String pattern, List> existingArguments) { + List> arguments = new ArrayList<>(); + CommandElement compiled = compile(pattern, existingArguments, arguments); + return compiled == null ? null : new CompilationResult(compiled, arguments); + } + + private static @Nullable CommandElement compile(final String pattern, + List> existingArguments, List> arguments) { + List pendingLiterals = new ArrayList<>(); + CommandElement first = new CommandElement(); + + int patternLength = pattern.length(); + for (int i = 0; i < patternLength; i++) { + char c = pattern.charAt(i); + if (c == '[' || c == '(') { // indicates optional choice or general grouping + boolean isOptional = c == '['; + int end = SkriptParser.nextBracket(pattern, isOptional ? ']' : ')', c, i + 1, true); + CommandElement commandElement = compile(pattern.substring(i + 1, end), existingArguments, arguments); + if (commandElement == null) { + return null; + } + + // determine elements to append + Collection toAppend; + if (isOptional) { + toAppend = new ArrayList<>(commandElement.children()); + toAppend.add(null); + } else { + toAppend = commandElement.children(); + } + + if (toAppend.stream().anyMatch(element -> element instanceof ArgumentCommandElement)) { + if (pendingLiterals.isEmpty()) { // [] is valid + first.append(toAppend); + } else { // argument placed attached to literals, e.g. 'lit' + Skript.error("Literals cannot be placed directly next to arguments. Separate them with a space."); + return null; + } + } else { + //noinspection rawtypes, unchecked + pendingLiterals = appendToLiterals(pendingLiterals, (Collection) toAppend); + } + + i = end; + } else if (c == '|') { // indicates the end of a single choice + if (!pendingLiterals.isEmpty()) { + first.append(pendingLiterals); + pendingLiterals.clear(); + } + + ChoiceCommandElement choiceElement; + if (first instanceof ChoiceCommandElement choiceCommandElement) { + choiceElement = choiceCommandElement; + } else { // indicates that we finished compiling the first choice + // thus, we create a new choice element with everything compiled so far as one of the choices + // the root element then becomes the choice element for further choice appending to occur + choiceElement = new ChoiceCommandElement(); + choiceElement.append(first.children()); + first = choiceElement; + } + // append an empty space for the following content to append to + choiceElement.appendEmpty(); + } else if (c == '<') { // indicates an argument + if (!pendingLiterals.isEmpty()) { // an argument cannot be legally placed here (ex. 'lit') + Skript.error("Literals cannot be placed directly next to arguments. Separate them with a space."); + return null; + } + + int end = SkriptParser.nextBracket(pattern, '>', c, i + 1, true); + ArgumentData argument = parseArgument(pattern.substring(i + 1, end), + Stream.concat(existingArguments.stream(), arguments.stream()).toList()); + if (argument == null) { + return null; + } + + arguments.add(argument); + first.append(List.of(new ArgumentCommandElement(argument))); + + i = end; + } else if (c == '\\' && i + 1 < patternLength) { // escaping + i++; + appendToLiterals(pendingLiterals, pattern.charAt(i)); + } else if (c == ' ') { // literal terminator + if (!pendingLiterals.isEmpty()) { + first.append(pendingLiterals); + pendingLiterals.clear(); + } + } else { + appendToLiterals(pendingLiterals, c); + } + } + + if (!pendingLiterals.isEmpty()) { // append any outstanding literal(s) + first.append(pendingLiterals); + } + + return first; + } + + /** + * Appends {@code character} to all elements of {@code literals}. + * @param literals The literals to append to. + * @param character The character to append. + */ + private static void appendToLiterals(List literals, char character) { + if (literals.isEmpty()) { + literals.add(new LiteralCommandElement(String.valueOf(character))); + return; + } + literals.replaceAll(element -> new LiteralCommandElement(element.literal() + character)); + } + + /** + * Appends every element in {@code elements} to every element in {@code literal}. + * Optional markers (null) are considered and preserved. + * @param literals The literals to append to. + * @param elements The elements to append. + * @return New list of literals, after appending. + */ + private static List appendToLiterals(Collection literals, + Collection elements) { + if (literals.isEmpty()) { + return new ArrayList<>(elements); + } + List newLiterals = new ArrayList<>(); + for (LiteralCommandElement literal : literals) { + if (literal == null) { // preserve optional marker (entire list is optional) + newLiterals.add(null); + continue; + } + for (LiteralCommandElement element : elements) { + if (element == null) { // null means the literal itself should still be valid + newLiterals.add(literal); + } else { + newLiterals.add(new LiteralCommandElement(literal.literal() + element.literal())); + } + } + } + return newLiterals; + } + + /* + * Argument Parsing + */ + + private static final Pattern ARGUMENT_PATTERN = + Pattern.compile("^\\s*(?:([^>]+?)\\s*:\\s*)?(.+?)\\s*(?:=\\s*(" + SkriptParser.WILDCARD + "))?\\s*$"); + + private static final Pattern TYPE_PATTERN = + Pattern.compile("^(.+?)\\s*(?: (?:from|above|between) (.+?))?(?:(?: (?:to|(?:and )?below|and) (.+?))?)?$"); + + private static @Nullable ArgumentData parseArgument(String argument, List> arguments) { + Matcher argumentMatcher = ARGUMENT_PATTERN.matcher(argument); + if (!argumentMatcher.find()) { + Skript.error("'" + argument + "' is not a properly formatted argument."); + return null; + } + + // first, parse the type + Matcher typeMatcher = TYPE_PATTERN.matcher(argumentMatcher.group(2)); + if (!typeMatcher.find()) { + Skript.error("'" + argumentMatcher.group(2) + "' is not a known type."); + return null; + } + String rawType = typeMatcher.group(1); + var plural = Utils.isPlural(rawType); + ClassInfo type = Classes.getClassInfoFromUserInput(plural.updated()); + if (type == null) { + Skript.error("'" + rawType + "' is not a known type."); + return null; + } else if (type.getParser() == null || !type.getParser().canParse(ParseContext.COMMAND)) { + Skript.error("The type '" + type.getName().getSingular() + "' cannot be used as a command argument."); + return null; + } + Object min = null; + Object max = null; + if (typeMatcher.group(2) != null) { // has min + min = type.getParser().parse(typeMatcher.group(2), ParseContext.COMMAND); + if (min == null) { + Skript.error("Invalid minimum range: " + min); + return null; + } + } + if (typeMatcher.group(3) != null) { // has max + max = type.getParser().parse(typeMatcher.group(3), ParseContext.COMMAND); + if (max == null) { + Skript.error("Invalid maximum range: " + max); + return null; + } + } + // type validation + NativeArgumentData nativeMapping = ScriptArgumentType.getNativeData(type); + if (min != null || max != null) { + if (nativeMapping == null || !nativeMapping.supportsRange()) { + String typeName = plural.plural() ? type.getName().getPlural() : type.getName().getSingular(); + Skript.error(typeName + " arguments do not support minimum or maximum values."); + return null; + } + if (!nativeMapping.supportsPlural() && plural.plural()) { + Skript.error("Only single " + type.getName().getSingular() + " arguments support minimum or maximum values."); + return null; + } + } + + // next, parse the name + String name = argumentMatcher.group(1); + boolean isAutomaticName = false; + if (name == null) { // user did not specify, manually create one + isAutomaticName = true; + name = type.getName().getSingular(); + String finalName = name; + // first, try just the classinfo name as the argument name (ex: 'number') + if (arguments.stream().anyMatch(arg -> arg.name().equals(finalName))) { + // otherwise, append an index (ex: 'number2') + int index = 2; + while (true) { + int finalIndex = index; + if (arguments.stream().anyMatch(arg -> arg.name().equals(finalName + finalIndex))) { + index++; + continue; + } + break; + } + name = name + index; + } + } else { + String finalName = name; + if (arguments.stream().anyMatch(arg -> arg.name().equals(finalName))) { + Skript.error("The argument name '" + finalName + "' was already used."); + return null; + } + } + + // finally, parse the default value + Expression defaultValue = null; + String rawDefaultValue = argumentMatcher.group(3); + if (rawDefaultValue != null) { + int parseType; + if (rawDefaultValue.startsWith("%") && rawDefaultValue.endsWith("%")) { + parseType = SkriptParser.PARSE_EXPRESSIONS; + rawDefaultValue = rawDefaultValue.substring(1, rawDefaultValue.length() - 1); + } else if (type.getC() == String.class && rawDefaultValue.startsWith("\"") && rawDefaultValue.endsWith("\"")) { + parseType = SkriptParser.PARSE_EXPRESSIONS; + } else { + parseType = SkriptParser.PARSE_LITERALS; + } + try (var logHandler = new RetainingLogHandler().start()) { + defaultValue = new SkriptParser(rawDefaultValue, parseType, ParseContext.COMMAND) + .parseExpression(type.getC()); + if (defaultValue == null) { + logHandler.printErrors("Can't understand this expression: '" + rawDefaultValue + "'." + + " The default value will be ignored for this argument."); + } else if (!plural.plural() && !defaultValue.canBeSingle()) { + logHandler.printErrors("Expected a single value but got many: " + defaultValue.toString(null, false)); + } else { + logHandler.printLog(); + } + } + } + + //noinspection unchecked, rawtypes + return new ArgumentData(name, isAutomaticName, type, !plural.plural(), defaultValue, min, max); + } + +} diff --git a/src/main/java/org/skriptlang/skript/bukkit/command/elements/structures/util/CommandSuggestionEvent.java b/src/main/java/org/skriptlang/skript/bukkit/command/elements/structures/util/CommandSuggestionEvent.java new file mode 100644 index 00000000000..8034216027b --- /dev/null +++ b/src/main/java/org/skriptlang/skript/bukkit/command/elements/structures/util/CommandSuggestionEvent.java @@ -0,0 +1,30 @@ +package org.skriptlang.skript.bukkit.command.elements.structures.util; + +import org.bukkit.event.Event; +import org.bukkit.event.HandlerList; +import org.jetbrains.annotations.ApiStatus; +import org.jetbrains.annotations.Contract; +import org.jetbrains.annotations.NotNull; + +import java.util.HashMap; +import java.util.List; +import java.util.Map; + +/** + * Internal event for managing context during "suggestions" entry evaluation in {@link SubCommandEntryData}. + */ +@ApiStatus.Internal +public class CommandSuggestionEvent extends Event { + + /** + * Map keyed by argument name. + */ + public Map> suggestions = new HashMap<>(); + + @Override + @Contract("-> fail") + public @NotNull HandlerList getHandlers() { + throw new UnsupportedOperationException(); + } + +} diff --git a/src/main/java/org/skriptlang/skript/bukkit/command/elements/structures/util/ScriptSuggestionProvider.java b/src/main/java/org/skriptlang/skript/bukkit/command/elements/structures/util/ScriptSuggestionProvider.java new file mode 100644 index 00000000000..b7525a6f9d3 --- /dev/null +++ b/src/main/java/org/skriptlang/skript/bukkit/command/elements/structures/util/ScriptSuggestionProvider.java @@ -0,0 +1,79 @@ +package org.skriptlang.skript.bukkit.command.elements.structures.util; + +import ch.njol.skript.lang.Trigger; +import com.mojang.brigadier.arguments.ArgumentType; +import com.mojang.brigadier.context.CommandContext; +import com.mojang.brigadier.suggestion.Suggestions; +import com.mojang.brigadier.suggestion.SuggestionsBuilder; +import io.papermc.paper.command.brigadier.CommandSourceStack; +import org.jetbrains.annotations.ApiStatus; +import org.jetbrains.annotations.NotNull; + +import java.util.List; +import java.util.Locale; +import java.util.concurrent.CompletableFuture; + +/** + * Utility class for managing {@link org.skriptlang.skript.bukkit.command.custom.ScriptBrigadierCommand} suggestions. + * Similar to {@link com.mojang.brigadier.suggestion.SuggestionProvider}. + */ +@ApiStatus.Internal +public class ScriptSuggestionProvider { + + private final Trigger suggestionsProvider; + + /** + * @param suggestionsProvider A trigger to execute using {@link CommandSuggestionEvent}. + * It is expected (though not required) that this trigger holds code to modify suggestions. + */ + public ScriptSuggestionProvider(Trigger suggestionsProvider) { + this.suggestionsProvider = suggestionsProvider; + } + + /** + * Obtains suggestions for a specific argument using this provider. + * @param argumentName The name of the argument suggestions are being obtained for. + * @param context Context around the command execution. + * @param builder The builder to add suggestions to + * @param argument The underlying argument suggestions are being obtained for. + * @return Future to obtain suggestions. + */ + public CompletableFuture getSuggestions(String argumentName, CommandContext context, + SuggestionsBuilder builder, ArgumentType argument) { + CommandSuggestionEvent suggestionEvent = new CommandSuggestionEvent(); + suggestionsProvider.execute(suggestionEvent); + List suggestions = suggestionEvent.suggestions.get(argumentName); + if (suggestions == null) { // nothing explicitly set, rely on argument's default suggestions + return argument.listSuggestions(context, builder); + } + for (String suggestion : suggestions) { + suggest(builder, suggestion); + } + return builder.buildFuture(); + } + + /** + * Attempts to add {@code suggestion} to {@code builder}. + * It expects that {@code suggestion} starts with (ignoring case) the currently entered input for the argument. + * @param builder The builder to add the suggestion to. + * @param suggestion The suggestion. + */ + public static void suggest(@NotNull SuggestionsBuilder builder, String suggestion) { + if (suggestion == null) { + return; + } + // treat as a valid match for + // treat <"foo b> as a valid match for + String remaining = builder.getRemainingLowerCase(); + if (!remaining.isEmpty() && remaining.charAt(0) == '"') { + remaining = remaining.substring(1); + } + if (remaining.contains(" ")) { + remaining = remaining.replace(' ', '_'); + } + if (suggestion.toLowerCase(Locale.ENGLISH).startsWith(remaining)) { + builder.suggest(suggestion); + } + } + +} diff --git a/src/main/java/org/skriptlang/skript/bukkit/command/elements/structures/util/SubCommandEntryData.java b/src/main/java/org/skriptlang/skript/bukkit/command/elements/structures/util/SubCommandEntryData.java new file mode 100644 index 00000000000..bdc046eed86 --- /dev/null +++ b/src/main/java/org/skriptlang/skript/bukkit/command/elements/structures/util/SubCommandEntryData.java @@ -0,0 +1,468 @@ +package org.skriptlang.skript.bukkit.command.elements.structures.util; + +import ch.njol.skript.ScriptLoader; +import ch.njol.skript.Skript; +import ch.njol.skript.config.Node; +import ch.njol.skript.config.SectionNode; +import ch.njol.skript.lang.Trigger; +import ch.njol.skript.lang.Variable; +import ch.njol.skript.lang.VariableString; +import ch.njol.skript.lang.parser.ParserInstance; +import ch.njol.skript.util.StringMode; +import ch.njol.skript.util.Timespan; +import ch.njol.skript.variables.HintManager; +import ch.njol.util.coll.CollectionUtils; +import com.mojang.brigadier.arguments.ArgumentType; +import com.mojang.brigadier.arguments.StringArgumentType; +import com.mojang.brigadier.builder.ArgumentBuilder; +import com.mojang.brigadier.builder.RequiredArgumentBuilder; +import io.papermc.paper.command.brigadier.CommandSourceStack; +import io.papermc.paper.command.brigadier.Commands; +import net.kyori.adventure.text.Component; +import org.bukkit.command.CommandSender; +import org.jetbrains.annotations.Nullable; +import org.skriptlang.skript.bukkit.command.custom.ArgumentData; +import org.skriptlang.skript.bukkit.command.custom.CommandParsingData; +import org.skriptlang.skript.bukkit.command.custom.CommandParsingData.ExecutorData; +import org.skriptlang.skript.bukkit.command.custom.CooldownManager; +import org.skriptlang.skript.bukkit.command.custom.ExecutableBy; +import org.skriptlang.skript.bukkit.command.custom.ScriptArgumentType.NativeArgumentData; +import org.skriptlang.skript.bukkit.command.custom.ScriptCommandEvent; +import org.skriptlang.skript.bukkit.command.elements.structures.util.CommandCompiler.ArgumentCommandElement; +import org.skriptlang.skript.bukkit.command.elements.structures.util.CommandCompiler.CommandElement; +import org.skriptlang.skript.bukkit.command.elements.structures.util.CommandCompiler.CompilationResult; +import org.skriptlang.skript.bukkit.command.elements.structures.util.CommandCompiler.LiteralCommandElement; +import org.skriptlang.skript.bukkit.command.custom.ScriptArgumentType; +import org.skriptlang.skript.bukkit.command.custom.ScriptCommandExecutor; +import org.skriptlang.skript.bukkit.command.elements.structures.util.SubCommandEntryData.Result; +import org.skriptlang.skript.lang.entry.EntryContainer; +import org.skriptlang.skript.lang.entry.EntryData; +import org.skriptlang.skript.lang.entry.EntryValidator; +import org.skriptlang.skript.lang.entry.KeyValueEntryData; +import org.skriptlang.skript.lang.entry.util.LiteralEntryData; +import org.skriptlang.skript.lang.entry.util.TriggerEntryData; +import org.skriptlang.skript.lang.entry.util.VariableStringEntryData; +import org.skriptlang.skript.lang.script.ScriptWarning; +import org.skriptlang.skript.log.runtime.ErrorSource; + +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Collection; +import java.util.EnumSet; +import java.util.List; +import java.util.Set; +import java.util.function.Predicate; +import java.util.regex.Matcher; +import java.util.regex.Pattern; + +public class SubCommandEntryData extends EntryData { + + public record Result( + List> arguments, + Collection aliases, + @Nullable String description, + @Nullable String usage, + @Nullable String prefix, + @Nullable String permission + ) { } + + private static final Predicate TRUE_PREDICATE = ignored -> true; + + private static final Pattern COMMAND_PATTERN = + Pattern.compile("(?i)^\\s*/?\\s*(.+)?$"); + + public static final EntryValidator SUB_COMMAND_VALIDATOR = EntryValidator.builder() + .addEntryData(new KeyValueEntryData>("aliases", null, true) { + private final Pattern pattern = Pattern.compile("\\s*,\\s*/?"); + + @Override + protected List getValue(String value) { + List aliases = new ArrayList<>(Arrays.asList(pattern.split(value))); + String first = aliases.getFirst(); + if (first.startsWith("/")) { // not caught by regex + aliases.set(0, first.substring(1)); + } else if (first.isEmpty()) { + Skript.error("Invalid aliases list: '" + value + "'. Aliases should be separated by commas."); + return List.of(); + } + return aliases; + } + }) + .addEntry("description", null, true) + .addEntryData(new VariableStringEntryData("usage", null, true)) + .addEntry("prefix", null, true) + .addEntryData(new TriggerEntryData("suggestions", null, true)) + .addEntry("permission", null, true) + .addEntryData(new KeyValueEntryData>("executable by", null, true) { + private final Pattern pattern = Pattern.compile("\\s*,(?:\\s+(?:and|or)\\s+)?\\s*|\\s+(?:and|or)\\s+"); + + @Override + protected Set getValue(String value) { + EnumSet executableBy = EnumSet.noneOf(ExecutableBy.class); + for (String type : pattern.split(value)) { + // "player" kept for compatibility + if (type.equalsIgnoreCase("players") || type.equalsIgnoreCase("player")) { + executableBy.add(ExecutableBy.PLAYERS); + } else if (type.equalsIgnoreCase("operators") || type.equalsIgnoreCase("ops")) { + executableBy.add(ExecutableBy.OPERATORS); + } else if (type.equalsIgnoreCase("console") || type.equalsIgnoreCase("the console")) { + executableBy.add(ExecutableBy.CONSOLE); + } else if (type.equalsIgnoreCase("blocks")) { + executableBy.add(ExecutableBy.BLOCKS); + } else { + Skript.error("Invalid command sender type: " + type); + return Set.of(); + } + } + return executableBy; + } + }) + .addEntryData(new LiteralEntryData<>("cooldown", null, true, Timespan.class)) + .addEntryData(new VariableStringEntryData("cooldown message", null, true)) + .addEntry("cooldown bypass", null, true) + .addEntryData(new VariableStringEntryData("cooldown storage", null, true, StringMode.VARIABLE_NAME)) + .addEntryData(new TriggerEntryData("trigger", null, true)) + .addEntryData(new SubCommandEntryData("subcommand", true, true)) + // deprecated entries + .addEntry("permission message", null, true) + .build(); + + static { + ParserInstance.registerData(CommandParsingData.class, CommandParsingData::new); + } + + public SubCommandEntryData(String key, boolean optional, boolean multiple) { + super(key, null, optional, multiple); + } + + @Override + public @Nullable Result getValue(Node node) { + assert node instanceof SectionNode; + + // validate section node structure + String key = node.getKey(); + if (key == null) + throw new IllegalArgumentException("EntryData#getValue() called with invalid node."); + String input = ScriptLoader.replaceOptions(key) + .substring(getKey().length() + 1); + Matcher commandMatcher = COMMAND_PATTERN.matcher(input); + boolean matches = commandMatcher.matches(); + if (!matches) { + Skript.error("Invalid command structure pattern"); + return null; + } + + // validate entries + EntryContainer entryContainer = SUB_COMMAND_VALIDATOR.validate((SectionNode) node); + if (entryContainer == null) { + return null; + } + + // set context for parsing + ParserInstance parser = ParserInstance.get(); + parser.setCurrentEvent("command", ScriptCommandEvent.class); + + // parse arguments + CommandParsingData parsingData = parser.getData(CommandParsingData.class); + boolean isRoot = parsingData.isEmpty(); + + CompilationResult compilationResult = CommandCompiler.compile(commandMatcher.group(1), parsingData.getArguments()); + if (compilationResult == null) { // failed for a reason reported by the compiler + return null; + } else if (compilationResult.root().isLeaf()) { + if (isRoot) { + Skript.error("A command must have a name."); + } else { + Skript.error("A subcommand must have at least one argument, literal or dynamic."); + } + return null; + } + + // prepare entries + // command aliases + List aliases = entryContainer.getOptional("aliases", List.class, false); + if (aliases == null) { + aliases = List.of(); + } else if (aliases.isEmpty()) { // parsing failed + return null; + } else if (!isRoot) { + Skript.error("Only the root of a command may have aliases."); + return null; + } + + // command description + String description = entryContainer.getOptional("description", String.class, false); + if (!isRoot && description != null) { + Skript.error("Only the root of a command may have a description."); + return null; + } + + // command usage + String usage = null; + VariableString variableUsage = entryContainer.getOptional("usage", VariableString.class, false); + if (variableUsage != null) { + if (!isRoot) { + Skript.error("Only the root of a command may have a usage."); + return null; + } + if (variableUsage.isSimple()) { + usage = variableUsage.toString(null); + } + } + + // command prefix (custom namespace) + String prefix = entryContainer.getOptional("prefix", String.class, false); + if (prefix != null) { + if (!isRoot) { + Skript.error("Only the root of a command may have a prefix."); + return null; + } + for (char c : prefix.toCharArray()) { + if (Character.isWhitespace(c)) { + Skript.error("Command prefixes must not have any whitespace."); + return null; + } else if (c == 167) { // char 167 is § + Skript.error("Command prefixes must not have any section symbols."); + return null; + } + } + } + + // command requirements + Predicate requires = TRUE_PREDICATE; + + // permission requirement + String permission = entryContainer.getOptional("permission", String.class, false); + if (permission != null) { + requires = requires.and(sender -> sender.hasPermission(permission)); + } + + // executable by requirement + Set executableBy = entryContainer.getOptional("executable by", Set.class, false); + if (executableBy != null) { + if (executableBy.isEmpty()) { // parsing failed + return null; + } + Set parent = parsingData.getExecutorData(ExecutorData::executableBy); + if (parent != null && !ExecutableBy.isSuperSet(parent, executableBy)) { + Skript.error("It is not possible to restrict execution to " + CollectionUtils.toString(executableBy, true) + + " as the parent command is only executable by " + CollectionUtils.toString(parent, true) + "."); + return null; + } + requires = requires.and(executableBy.stream() + .map(ExecutableBy::predicate) + .reduce(Predicate::or) + .orElseThrow()); + } + + // prepare final requirements predicate + Predicate commandRequires; + if (requires != TRUE_PREDICATE) { + final Predicate finalRequires = requires; + commandRequires = source -> finalRequires.test(source.getSender()); + } else { + commandRequires = null; + } + + // cooldowns + parsingData.isParsingCooldownEntry = true; + Timespan cooldown = entryContainer.getOptional("cooldown", Timespan.class, false); + VariableString cooldownMessage = entryContainer.getOptional("cooldown message", VariableString.class, false); + String cooldownBypass = entryContainer.getOptional("cooldown bypass", String.class, false); + VariableString cooldownStorage = entryContainer.getOptional("cooldown storage", VariableString.class, false); + parsingData.isParsingCooldownEntry = false; + CooldownManager cooldownManager; + if (cooldown == null) { + if (cooldownMessage != null) { + Skript.warning("There is a cooldown message set, but not a cooldown"); + } + if (cooldownBypass != null) { + Skript.warning("There is a cooldown bypass set, but not a cooldown"); + } + if (cooldownStorage != null) { + Skript.warning("There is a cooldown storage set, but not a cooldown"); + } + // inherit from parent command + cooldownManager = parsingData.getExecutorData(ExecutorData::cooldownManager); + } else { + assert parser.getCurrentStructure() != null; + ErrorSource errorSource = ErrorSource.fromNodeAndElement(node, parser.getCurrentStructure()); + //noinspection unchecked + cooldownManager = new CooldownManager(cooldown, + cooldownMessage == null ? null : cooldownMessage.getConvertedExpression(Component.class), + cooldownBypass, cooldownStorage, () -> errorSource); + } + + // handle deprecated entries + if (entryContainer.hasEntry("permission message")) { + ScriptWarning.printDeprecationWarning("The 'permission message' entry has been deprecated for removal in a future release." + + " Commands that a player does not have permission to execute are no longer sent to their client."); + } + + // prepare arguments + parsingData.pushArguments(compilationResult.arguments()); + List> allArguments = parsingData.getArguments(); + parsingData.pushExecutorData(new ExecutorData(executableBy, cooldownManager)); + + // parse execution trigger + HintManager hintManager = parser.getHintManager(); + Trigger execute; + try { + // set type hints + hintManager.enterScope(false); + for (ArgumentData argument : allArguments) { + String hintName = argument.name(); + if (argument.isAutomaticName()) { + continue; + } + if (!argument.isSingle()) { + hintName += Variable.SEPARATOR + "*"; + } + hintManager.set(hintName, argument.type().getC()); + } + + // parse trigger + execute = entryContainer.getOptional("trigger", Trigger.class, false); + } finally { + parser.deleteCurrentEvent(); + hintManager.exitScope(); + } + boolean hasExecute = execute != null; + + // parse suggestions trigger + parser.setCurrentEvent("command suggestions", CommandSuggestionEvent.class); + Trigger suggestionsTrigger = entryContainer.getOptional("suggestions", Trigger.class, false); + parser.deleteCurrentEvent(); + ScriptSuggestionProvider suggestionProvider; + if (suggestionsTrigger != null) { + suggestionProvider = new ScriptSuggestionProvider(suggestionsTrigger); + } else { + suggestionProvider = null; + } + + // setup executor + ScriptCommandExecutor executor; + if (hasExecute) { + executor = new ScriptCommandExecutor(execute, allArguments, cooldownManager); + } else { + executor = null; + } + + // attach subcommand pieces + List> subcommands = + entryContainer.getAll("subcommand", Result.class, false).stream() + .flatMap(result -> result.arguments().stream()) + .toList(); + if (subcommands.isEmpty() && !hasExecute) { + Skript.error("You must have a 'trigger' entry if there are no subcommands!"); + return null; + } + + //noinspection rawtypes + var result = (List) compilationResult.root().children().stream() + .map(child -> parse(child, executor, commandRequires, suggestionProvider, subcommands)) + .toList(); + + parsingData.popExecutorData(); + parsingData.popArguments(); + + //noinspection unchecked + return new Result(result, aliases, description, usage, prefix, permission); + } + + @Override + public boolean canCreateWith(Node node) { + if (!(node instanceof SectionNode)) { + return false; + } + String key = node.getKey(); + if (key == null) + return false; + key = ScriptLoader.replaceOptions(key); + String prefix = getKey() + " "; + return key.regionMatches(true, 0, prefix, 0, prefix.length()); + } + + /** + * Parses a {@link CommandElement} structure into a Brigadier equivalent. + * @param commandElement The root element to start building from. + * @param executor Executor to execute the command at any leaf elements. + * @param requires Predicate testing whether the command can be used. + * @param suggestionProvider Provider for custom suggestions. + * @param subcommands Subcommands to attach to any leaf elements. + * @return Builder representing the completed command tree from the root. + */ + private static ArgumentBuilder parse( + CommandElement commandElement, + @Nullable ScriptCommandExecutor executor, + @Nullable Predicate requires, + @Nullable ScriptSuggestionProvider suggestionProvider, + Collection> subcommands + ) { + Collection children = commandElement.children(); + + ArgumentBuilder argument; + if (commandElement instanceof LiteralCommandElement literalCommandElement) { + argument = Commands.literal(literalCommandElement.literal()); + } else { // ArgumentCommandElement + ArgumentData data = ((ArgumentCommandElement) commandElement).argument(); + + // determine Brigadier ArgumentType + // prefer native types, but fallback to generic argument for any type if invalid + ArgumentType nativeType = null; + NativeArgumentData nativeMapping = ScriptArgumentType.getNativeData(data.type()); + if (nativeMapping != null) { // native argument type may be available + nativeType = nativeMapping.mapper().apply(data); + } + if (nativeType == null) { + if (children.isEmpty() && subcommands.isEmpty()) { // last argument can be greedy + nativeType = StringArgumentType.greedyString(); + } else { + nativeType = StringArgumentType.string(); + } + nativeType = new ScriptArgumentType<>(data, (StringArgumentType) nativeType); + } + + String name = data.name(); + argument = Commands.argument(name, nativeType); + + // attach suggestion provider to argument if available + if (suggestionProvider != null) { + ArgumentType finalNativeType = nativeType; + //noinspection unchecked + ((RequiredArgumentBuilder) argument).suggests( + (context, builder) -> + suggestionProvider.getSuggestions(name, context, builder, finalNativeType)); + } + } + + if (requires != null) { + argument.requires(requires); + } + + // we parse and append the children elements to this argument + for (CommandElement element : children) { + if (element == null) { + continue; + } + // we don't need to pass requirements down to children. just on the root is good enough. + argument.then(parse(element, executor, null, suggestionProvider, subcommands)); + } + + // this is intentionally placed AFTER iterating over the children + // for conflicting command arguments, the argument at this level should be preferred over a subcommand's argument + // it is not guaranteed to conflict if two arguments are at the same level + // e.g. Player-then-Number conflicts but Number-then-Player doesn't + if (commandElement.isLeaf()) { + for (var subcommand : subcommands) { + argument.then(subcommand); + } + if (executor != null) { + argument.executes(executor::execute); + } + } + + return argument; + } + +} diff --git a/src/test/skript/tests/bukkit/command/CondIsScriptCommand.sk b/src/test/skript/tests/bukkit/command/CondIsScriptCommand.sk new file mode 100644 index 00000000000..8304d45f8e0 --- /dev/null +++ b/src/test/skript/tests/bukkit/command/CondIsScriptCommand.sk @@ -0,0 +1,7 @@ +command /is-script-command: + trigger: + stop + +test "CondIsScriptCommand": + assert "is-script-command" is a script command with "should be a script command but is not" + assert "help" is not a script command with "should not be a script command but is" diff --git a/src/test/skript/tests/bukkit/command/ExprAllCommands.sk b/src/test/skript/tests/bukkit/command/ExprAllCommands.sk new file mode 100644 index 00000000000..2ec795beb5f --- /dev/null +++ b/src/test/skript/tests/bukkit/command/ExprAllCommands.sk @@ -0,0 +1,9 @@ +command /exprallcommands: + trigger: + stop + +test "ExprAllCommands": + assert all commands contains "exprallcommands" with "script command not part of all commands" + assert all script commands contains "exprallcommands" with "script command not part of all script commands" + assert all commands contains "help" with "help command not part of all commands" + assert all script commands does not contain "help" with "help command part of all script commands" diff --git a/src/test/skript/tests/bukkit/command/ExprCommand.sk b/src/test/skript/tests/bukkit/command/ExprCommand.sk new file mode 100644 index 00000000000..5cb9a46af40 --- /dev/null +++ b/src/test/skript/tests/bukkit/command/ExprCommand.sk @@ -0,0 +1,9 @@ +command /exprcommand : + trigger: + set {-exprcommand::full} to the full command + set {-exprcommand::command} to the command + +test "ExprCommand": + execute console command "/exprcommand skript is cool" + assert {-exprcommand::full} is "exprcommand skript is cool" with "full command did not match" + assert {-exprcommand::command} is "exprcommand" with "command did not match" diff --git a/src/test/skript/tests/bukkit/command/ExprCommandInfo.sk b/src/test/skript/tests/bukkit/command/ExprCommandInfo.sk new file mode 100644 index 00000000000..1e0ac94e8b1 --- /dev/null +++ b/src/test/skript/tests/bukkit/command/ExprCommandInfo.sk @@ -0,0 +1,16 @@ +command /exprcommandinfo: + aliases: /exprcommandinfo2 + description: This is the description + usage: This is the usage + permission: skript.exprcommandinfo + trigger: + stop + +test "ExprCommandInfo": + assert the main command name of the command "exprcommandinfo" is "exprcommandinfo" + assert the description of the command "exprcommandinfo" is "This is the description" + assert the label of the command "exprcommandinfo" is "exprcommandinfo" + assert the usage of the command "exprcommandinfo" is "This is the usage" + assert the aliases of the command "exprcommandinfo" are "exprcommandinfo2" and "skript:exprcommandinfo2" + assert the permission of the command "exprcommandinfo" is "skript.exprcommandinfo" + assert the plugin of the command "exprcommandinfo" is "Skript" diff --git a/src/test/skript/tests/bukkit/command/StructCommand.sk b/src/test/skript/tests/bukkit/command/StructCommand.sk new file mode 100644 index 00000000000..002869fa49d --- /dev/null +++ b/src/test/skript/tests/bukkit/command/StructCommand.sk @@ -0,0 +1,171 @@ +using local variable type hints +using error catching + +options: + command: skriptcommand + +# expect error for missing trigger given no subcommands +parse: + results: {-StructCommand::entries::1::*} + code: + command /entries_test_1: + +# expect error for invalid executable by for subcommand +parse: + results: {-StructCommand::entries::2::*} + code: + command /entries_test_2: + executable by: players + trigger: + stop + subcommand test: + executable by: console + trigger: + stop + +# expect error for invalid subcommand and subsequently missing trigger (given no valid subcommands) +parse: + results: {-StructCommand::entries::3::*} + code: + command /entries_test_3: + subcommand test: + +# expect operators to be allowed below players (or other kinds) +parse: + results: {-StructCommand::entries::4::*} + code: + command /entries_test_4: + executable by: players + trigger: + stop + subcommand test: + executable by: operators + trigger: + stop + +# expect error for duplicate argument names +parse: + results: {-StructCommand::args::1::*} + code: + command /args_test_1 : + trigger: + stop + +# expect error for type not supporting range +parse: + results: {-StructCommand::args::2::*} + code: + command /args_test_2 : + trigger: + stop + +# expect no errors +parse: + results: {-StructCommand::args::3::*} + code: + command /args_test_3 : + trigger: + stop + +# expect error that only single number arguments support ranged +parse: + results: {-StructCommand::args::4::*} + code: + command /args_test_4 : + trigger: + stop + +# expect error that {_x} is not a text +parse: + results: {-StructCommand::hints::1::*} + code: + command /hint_test_1 : + trigger: + set {_a} to {_x} in lowercase + +# expect error that {_x::*} is not a text +parse: + results: {-StructCommand::hints::2::*} + code: + command /hint_test_2 : + trigger: + set {_a::*} to {_x::*} in lowercase + +command /test-subcommand : + subcommand : + trigger: + set {-test-subcommand} to argument 1 - argument 2 + trigger: + set {-test-subcommand} to arg-1 + +command /test-args-plural : + trigger: + set {-test-args-plural::*} to {_x::*} + +command /test-args-ranged : + trigger: + set {-test-args-ranged::*} to {_x} + +command {@command} [] []: + trigger: + set {-test-{@command}::arg1} to arg-1 + set {-test-{@command}::arg2} to arg-2 + set {-test-{@command}::arg3} to arg-3 + +command //somecommand []: + trigger: + set {-test-somecommand::arg1} to arg-1 + +# see https://github.com/SkriptLang/Skript/pull/6286 +command /commandtest : + trigger: + stop + +test "StructCommand": + # parsing + set {_error_missing_trigger} to "You must have a 'trigger' entry if there are no subcommands!" + assert {-StructCommand::entries::1::*} is {_error_missing_trigger} + assert {-StructCommand::entries::2::*} is "It is not possible to restrict execution to the console as the parent command is only executable by players." + assert {-StructCommand::entries::3::*} is {_error_missing_trigger} and {_error_missing_trigger} + assert {-StructCommand::entries::4::*} is not set + assert {-StructCommand::args::1::*} is "The argument name 'x' was already used." + assert {-StructCommand::args::2::*} is "text arguments do not support minimum or maximum values." + assert {-StructCommand::args::3::*} is not set + assert {-StructCommand::args::4::*} is "Only single number arguments support minimum or maximum values." + # parsing type hints + assert {-StructCommand::hints::1::*} is "Expected variable '{_x}' to be a text, but it is a number" with "Hint failed (%{StructCommand::hints::1::*}%)" + assert {-StructCommand::hints::2::*} is "Expected variable '{_x::*}' to be a text, but it is a number" with "Hint failed (%{StructCommand::hints::2::*}%)" + + # execution + execute command "test-subcommand 10" + assert {-test-subcommand} is 10 with "root command execution failed" + execute command "test-subcommand 10 5" + assert {-test-subcommand} is 5 with "subcommand command execution failed" + + execute command "test-args-plural 10" + assert {-test-args-plural::*} is 10 with "single input to plural args did not work" + execute command "test-args-plural 10 and 2" + assert {-test-args-plural::*} is 10 and 2 with "plural input to plural args did not work" + + execute command "test-args-ranged 6" + assert {-test-args-ranged::*} is 6 with "argument in range was not accepted" + catch runtime errors: + execute command "test-args-ranged 4" + if running minecraft "1.21.6": + assert last caught runtime errors start with "Failed to execute command: " + assert {-test-args-ranged::*} is 6 with "argument below range was accepted" + catch runtime errors: + execute command "test-args-ranged 11" + if running minecraft "1.21.6": + assert last caught runtime errors start with "Failed to execute command: " + assert {-test-args-ranged::*} is 6 with "argument above range was accepted" + + execute command "{@command} taco" + assert {-test-{@command}::arg1} is "taco" with "arg-1 test failed" + assert {-test-{@command}::arg2} is not set with "arg-2 test failed" + assert {-test-{@command}::arg3} is dirt named "steve" with "arg-3 test failed" + + # On some older versions (1.21.4, TBD), '//somecommand' does not work through console. + # This appears to be some sort of server issue. + execute command "skript:/somecommand burrito is tasty" + assert {-test-somecommand::arg1} is "burrito is tasty" with "arg-1 is 'burrito is tasty' test failed" diff --git a/src/test/skript/tests/misc/timespans.sk b/src/test/skript/tests/misc/timespans.sk index e6a81c60918..904a88bb32f 100644 --- a/src/test/skript/tests/misc/timespans.sk +++ b/src/test/skript/tests/misc/timespans.sk @@ -40,8 +40,7 @@ test "timespans": execute command "/testtimespan 2.5h" assert {timespan} is "2 hours and 30 minutes" parsed as timespan with "%{timespan}% doesn't equal 2 hours and 30 minutes!" - execute command "/testtimespan invalidinput" - assert {timespan} is "invalid" parsed as timespan to fail with "%{timespan}% was incorrectly parsed!" + assert "invalidinput" parsed as timespan is not set with "%{timespan}% was incorrectly parsed!" execute command "/testtimespan 1mo 2w 3d 4h 5m 6s" assert {timespan} is "1 month, 2 weeks, 3 days, 4 hours, 5 minutes and 6 seconds" parsed as timespan with "%{timespan}% doesn't equal 1 month, 2 weeks, 3 days, 4 hours, 5 minutes and 6 seconds!" diff --git a/src/test/skript/tests/regressions/6936-single quotes in commands.sk b/src/test/skript/tests/regressions/6936-single quotes in commands.sk index b8d8c554e64..b9444d58685 100644 --- a/src/test/skript/tests/regressions/6936-single quotes in commands.sk +++ b/src/test/skript/tests/regressions/6936-single quotes in commands.sk @@ -1,9 +1,9 @@ -command single-quotes-in-commands [""]: +command single-quotes-in-commands [" "]: trigger: set {sqic::output} to arg test "single-quotes-in-commands": - execute console command "single-quotes-in-commands ""success 1""" + execute console command "single-quotes-in-commands "" ""success 1"" """ assert {sqic::output} is "success 1" with "failed to parse arg in quotes" execute console command "single-quotes-in-commands" assert {sqic::output} is "success 2" with "failed to use default arg" diff --git a/src/test/skript/tests/syntaxes/structures/StructCommand.sk b/src/test/skript/tests/syntaxes/structures/StructCommand.sk deleted file mode 100644 index b53600dd4f5..00000000000 --- a/src/test/skript/tests/syntaxes/structures/StructCommand.sk +++ /dev/null @@ -1,47 +0,0 @@ -options: - command: skriptcommand - -command {@command} [] []: - trigger: - set {_arg1} to arg-1 - assert {_arg1} is "taco" with "arg-1 test failed (got '%{_arg1}%')" - set {_arg2} to arg-2 - assert {_arg2} is not set with "arg-2 test failed (got '%{_arg2}%')" - set {_arg3} to arg-3 - assert {_arg3} is dirt named "steve" with "arg-3 test failed (got '%{_arg3}%')" - -command //somecommand []: - trigger: - set {_arg1} to arg-1 - if {_arg1} is set: - assert {_arg1} is "burrito is tasty" with "arg-1 is 'burrito is tasty' test failed (got '%{_arg1}%')" - -# see https://github.com/SkriptLang/Skript/pull/6286 -command /commandtest : - trigger: - stop - -test "commands": - execute command "skriptcommand taco" - execute command "//somecommand burrito is tasty" - -using local variable type hints - -parse: - results: {StructCommand::hints::1::*} - code: - command /hint_test_1 : - trigger: - set {_a} to {_x} in lowercase - -parse: - results: {StructCommand::hints::2::*} - code: - command /hint_test_2 : - trigger: - set {_a::*} to {_x::*} in lowercase - -test "command structure type hints": - assert {StructCommand::hints::1::*} is "Expected variable '{_x}' to be a text, but it is a number" with "Hint failed (%{StructCommand::hints::1::*}%)" - assert {StructCommand::hints::2::*} is "Expected variable '{_x::*}' to be a text, but it is a number" with "Hint failed (%{StructCommand::hints::2::*}%)" -