From ac98b453af5da29d7aaa27e5d6520c2aab738b22 Mon Sep 17 00:00:00 2001 From: Patrick Miller Date: Mon, 22 Jun 2026 23:10:51 -0400 Subject: [PATCH 01/41] Add basic Brigadier-based command structure --- .../skript/bukkit/BukkitModule.java | 2 + .../skript/bukkit/command/CommandModule.java | 26 ++ .../elements/structures/StructCommand.java | 250 ++++++++++++++++++ .../structures/util/ArgumentData.java | 13 + .../structures/util/CommandEvent.java | 25 ++ .../util/SkriptBrigadierArgument.java | 96 +++++++ .../util/SkriptCommandExecutor.java | 43 +++ 7 files changed, 455 insertions(+) create mode 100644 src/main/java/org/skriptlang/skript/bukkit/command/CommandModule.java create mode 100644 src/main/java/org/skriptlang/skript/bukkit/command/elements/structures/StructCommand.java create mode 100644 src/main/java/org/skriptlang/skript/bukkit/command/elements/structures/util/ArgumentData.java create mode 100644 src/main/java/org/skriptlang/skript/bukkit/command/elements/structures/util/CommandEvent.java create mode 100644 src/main/java/org/skriptlang/skript/bukkit/command/elements/structures/util/SkriptBrigadierArgument.java create mode 100644 src/main/java/org/skriptlang/skript/bukkit/command/elements/structures/util/SkriptCommandExecutor.java diff --git a/src/main/java/org/skriptlang/skript/bukkit/BukkitModule.java b/src/main/java/org/skriptlang/skript/bukkit/BukkitModule.java index b1e42eadc10..a8608b1d7f2 100644 --- a/src/main/java/org/skriptlang/skript/bukkit/BukkitModule.java +++ b/src/main/java/org/skriptlang/skript/bukkit/BukkitModule.java @@ -8,6 +8,7 @@ import org.skriptlang.skript.bukkit.block.BlockModule; 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.entity.EntityModule; import org.skriptlang.skript.bukkit.fishing.FishingModule; @@ -38,6 +39,7 @@ public Iterable children() { new BlockModule(this), new BreedingModule(this), new BrewingModule(this), + new CommandModule(this), new DamageSourceModule(this), new EntityModule(this), new FishingModule(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..12fb68786b9 --- /dev/null +++ b/src/main/java/org/skriptlang/skript/bukkit/command/CommandModule.java @@ -0,0 +1,26 @@ +package org.skriptlang.skript.bukkit.command; + +import org.skriptlang.skript.addon.AddonModule; +import org.skriptlang.skript.addon.HierarchicalAddonModule; +import org.skriptlang.skript.addon.SkriptAddon; +import org.skriptlang.skript.bukkit.command.elements.structures.StructCommand; + +public class CommandModule extends HierarchicalAddonModule { + + public CommandModule(AddonModule parentModule) { + super(parentModule); + } + + @Override + protected void loadSelf(SkriptAddon addon) { + register(addon, + syntaxRegistry -> StructCommand.register(addon, syntaxRegistry) + ); + } + + @Override + public String name() { + return "command"; + } + +} 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..681a1455ca8 --- /dev/null +++ b/src/main/java/org/skriptlang/skript/bukkit/command/elements/structures/StructCommand.java @@ -0,0 +1,250 @@ +package org.skriptlang.skript.bukkit.command.elements.structures; + +import ch.njol.skript.Skript; +import ch.njol.skript.classes.ClassInfo; +import ch.njol.skript.lang.Expression; +import ch.njol.skript.lang.Literal; +import ch.njol.skript.lang.ParseContext; +import ch.njol.skript.lang.SkriptParser; +import ch.njol.skript.lang.SkriptParser.ParseResult; +import ch.njol.skript.lang.Trigger; +import ch.njol.skript.lang.parser.ParserInstance; +import ch.njol.skript.log.RetainingLogHandler; +import ch.njol.skript.registrations.Classes; +import ch.njol.skript.util.Utils; +import com.mojang.brigadier.builder.ArgumentBuilder; +import com.mojang.brigadier.builder.RequiredArgumentBuilder; +import com.mojang.brigadier.tree.LiteralCommandNode; +import io.papermc.paper.command.brigadier.CommandSourceStack; +import io.papermc.paper.command.brigadier.Commands; +import io.papermc.paper.plugin.lifecycle.event.types.LifecycleEvents; +import org.bukkit.Bukkit; +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.elements.structures.util.ArgumentData; +import org.skriptlang.skript.bukkit.command.elements.structures.util.CommandEvent; +import org.skriptlang.skript.bukkit.command.elements.structures.util.SkriptBrigadierArgument; +import org.skriptlang.skript.bukkit.command.elements.structures.util.SkriptCommandExecutor; +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.entry.EntryValidator; +import org.skriptlang.skript.lang.entry.util.TriggerEntryData; +import org.skriptlang.skript.lang.structure.Structure; +import org.skriptlang.skript.registration.SyntaxInfo; +import org.skriptlang.skript.registration.SyntaxRegistry; + +import java.util.ArrayList; +import java.util.LinkedList; +import java.util.List; +import java.util.Set; +import java.util.concurrent.ConcurrentHashMap; +import java.util.concurrent.atomic.AtomicBoolean; +import java.util.regex.Matcher; +import java.util.regex.Pattern; + +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 [/]<\\S+> [<.+>]") + .entryValidator(EntryValidator.builder() + .addEntryData(new TriggerEntryData("trigger", null, false)) + .build()) + .build()); + + EventValueRegistry evRegistry = addon.registry(EventValueRegistry.class); + evRegistry.register(EventValue.simple(CommandEvent.class, CommandSender.class, CommandEvent::getSender)); + + Skript.getInstance().getLifecycleManager().registerEventHandler(LifecycleEvents.COMMANDS,commands -> { + var registrar = commands.registrar(); + COMMANDS.forEach(registrar::register); + }); + } + + private static final Set> COMMANDS = ConcurrentHashMap.newKeySet(); + private static final AtomicBoolean SYNC_COMMANDS = new AtomicBoolean(); + + private static void performSync() { + if (SYNC_COMMANDS.get()) { + SYNC_COMMANDS.set(false); + Bukkit.reloadData(); + } + } + + /** + * Pattern for parsing arguments in the form: {@code }. + *
+ * Where {@code name}, {@code type}, and {@code defaultValue} are groups {@code 1}, {@code 2}, and {@code 3}, respectively. + */ + private static final Pattern ARGUMENT_PATTERN = + Pattern.compile("<\\s*(?:([^>]+?)\\s*:\\s*)?(.+?)\\s*(?:=\\s*(" + SkriptParser.WILDCARD + "))?\\s*>"); + + private String name; + private EntryContainer entryContainer; + private final List> arguments = new ArrayList<>(); + + private LiteralCommandNode command; + + @Override + public boolean init(Literal[] args, int matchedPattern, ParseResult parseResult, EntryContainer entryContainer) { + name = parseResult.regexes.getFirst().group(); + this.entryContainer = entryContainer; + + if (parseResult.regexes.size() == 1) { // no arguments specified + return true; + } + + // parse arguments + Matcher argumentMatcher = ARGUMENT_PATTERN.matcher(parseResult.regexes.getLast().group()); + boolean optional = false; + while (argumentMatcher.find()) { + // first, parse the type + String rawType = argumentMatcher.group(2); + var plural = Utils.isPlural(rawType); + ClassInfo type = Classes.getClassInfoFromUserInput(plural.updated()); + if (type == null) { + Skript.error("'" + rawType + "' is not a known type."); + return false; + } 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 false; + } + + // 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; + if (arguments.stream().anyMatch(data -> data.name().equals(finalName))) { // already taken + // try to generate a name like 'number2', 'number3', etc + int i = 2; + baseLoop: while (true) { + for (var argument : arguments) { + if (argument.name().equals(name + i)) { + i++; + continue baseLoop; + } + } + break; + } + name += i; + } + } + + // 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 { + 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 { + logHandler.printLog(); + } + } + } + optional |= defaultValue != null; + + //noinspection unchecked, rawtypes + arguments.add(new ArgumentData(name, isAutomaticName, type, defaultValue, optional)); + } + + return true; + } + + @Override + public boolean load() { + ParserInstance parser = getParser(); + parser.setCurrentEvent("command execution trigger", CommandEvent.class); + Trigger execute = entryContainer.get("trigger", Trigger.class, false); + parser.deleteCurrentEvent(); + + var builder = Commands.literal(name); + + List> pieces = new LinkedList<>(); + pieces.addFirst(builder); + for (int i = 0; i < arguments.size(); i++) { + ArgumentData argument = arguments.get(i); + SkriptBrigadierArgument brigadierArgument; + if (i == arguments.size() - 1) { + // TODO maybe not desirable for numbers + brigadierArgument = new SkriptBrigadierArgument.GreedyArgument<>(argument); + } else { + brigadierArgument = new SkriptBrigadierArgument<>(argument); + } + pieces.addFirst(Commands.argument(argument.name(), brigadierArgument)); + } + + // setup base + ArgumentBuilder base = pieces.removeFirst(); + SkriptCommandExecutor executor = new SkriptCommandExecutor(execute, arguments); + int argCount = arguments.size(); + final int finalBaseArgCount = argCount; + base.executes(context -> executor.execute(context, finalBaseArgCount)); + + // attach rest of argument pieces + var iterator = pieces.iterator(); + // wasOptional is so that we place an executes on the last, non-optional argument + boolean wasOptional = arguments.getLast().optional(); + while (iterator.hasNext()) { + argCount--; + final int finalArgCount = argCount; + base = iterator.next().then(base); + if (base instanceof RequiredArgumentBuilder requiredArgument && + ((SkriptBrigadierArgument) requiredArgument.getType()).getArgument().optional()) { + base.executes(context -> executor.execute(context, finalArgCount)); + wasOptional = true; + } else if (wasOptional) { + base.executes(context -> executor.execute(context, finalArgCount)); + wasOptional = false; + } + iterator.remove(); + } + + command = builder.build(); + + COMMANDS.add(command); + SYNC_COMMANDS.set(true); + + return true; + } + + @Override + public boolean postLoad() { + performSync(); + return true; + } + + @Override + public void unload() { + COMMANDS.remove(command); + SYNC_COMMANDS.set(true); + } + + @Override + public void postUnload() { + performSync(); + } + + @Override + public String toString(@Nullable Event event, boolean debug) { + return "command /" + name; + } + +} diff --git a/src/main/java/org/skriptlang/skript/bukkit/command/elements/structures/util/ArgumentData.java b/src/main/java/org/skriptlang/skript/bukkit/command/elements/structures/util/ArgumentData.java new file mode 100644 index 00000000000..562b5bb843f --- /dev/null +++ b/src/main/java/org/skriptlang/skript/bukkit/command/elements/structures/util/ArgumentData.java @@ -0,0 +1,13 @@ +package org.skriptlang.skript.bukkit.command.elements.structures.util; + +import ch.njol.skript.classes.ClassInfo; +import ch.njol.skript.lang.Expression; +import org.jetbrains.annotations.Nullable; + +public record ArgumentData( + String name, + boolean isAutomaticName, + ClassInfo type, + @Nullable Expression defaultValue, + boolean optional +) { } diff --git a/src/main/java/org/skriptlang/skript/bukkit/command/elements/structures/util/CommandEvent.java b/src/main/java/org/skriptlang/skript/bukkit/command/elements/structures/util/CommandEvent.java new file mode 100644 index 00000000000..fcf6db8194d --- /dev/null +++ b/src/main/java/org/skriptlang/skript/bukkit/command/elements/structures/util/CommandEvent.java @@ -0,0 +1,25 @@ +package org.skriptlang.skript.bukkit.command.elements.structures.util; + +import org.bukkit.command.CommandSender; +import org.bukkit.event.Event; +import org.bukkit.event.HandlerList; +import org.jetbrains.annotations.NotNull; + +public class CommandEvent extends Event { + + public final CommandSender sender; + + public CommandEvent(CommandSender sender) { + this.sender = sender; + } + + public CommandSender getSender() { + return sender; + } + + @Override + public @NotNull HandlerList getHandlers() { + throw new UnsupportedOperationException(); + } + +} diff --git a/src/main/java/org/skriptlang/skript/bukkit/command/elements/structures/util/SkriptBrigadierArgument.java b/src/main/java/org/skriptlang/skript/bukkit/command/elements/structures/util/SkriptBrigadierArgument.java new file mode 100644 index 00000000000..9b2bfb75248 --- /dev/null +++ b/src/main/java/org/skriptlang/skript/bukkit/command/elements/structures/util/SkriptBrigadierArgument.java @@ -0,0 +1,96 @@ +package org.skriptlang.skript.bukkit.command.elements.structures.util; + +import ch.njol.skript.lang.ParseContext; +import ch.njol.skript.lang.util.ContextlessEvent; +import ch.njol.skript.registrations.Classes; +import com.mojang.brigadier.LiteralMessage; +import com.mojang.brigadier.arguments.ArgumentType; +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.suggestion.Suggestions; +import com.mojang.brigadier.suggestion.SuggestionsBuilder; +import io.papermc.paper.command.brigadier.argument.CustomArgumentType; +import org.jetbrains.annotations.NotNull; + +import java.util.Iterator; +import java.util.Locale; +import java.util.concurrent.CompletableFuture; +import java.util.function.Supplier; + +public class SkriptBrigadierArgument implements CustomArgumentType.Converted { + + 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; + + public SkriptBrigadierArgument(@NotNull ArgumentData argument) { + this.argument = argument; + } + + public ArgumentData getArgument() { + return argument; + } + + @Override + public @NotNull T convert(@NotNull String input) throws CommandSyntaxException { + assert argument.type().getParser() != null; + input = input.replace('_', ' '); + T result = argument.type().getParser().parse(input, ParseContext.COMMAND); + if (result == null) { + if (argument.defaultValue() != null) { // attempt default value + result = argument.defaultValue().getSingle(ContextlessEvent.get()); + } + if (result == null) { + throw ERROR_INVALID_INPUT.create(input, argument.type().getName().getSingular()); + } + } + return result; + } + + @Override + public @NotNull ArgumentType getNativeType() { + return StringArgumentType.string(); + } + + @Override + public @NotNull CompletableFuture listSuggestions(@NotNull CommandContext context, @NotNull SuggestionsBuilder builder) { + Supplier> supplier = argument.type().getSupplier(); + if (supplier == null) { + return Suggestions.empty(); + } + + supplier.get().forEachRemaining(value -> { + String name = Classes.toString(value).toLowerCase(Locale.ENGLISH) + .replace(' ', '_'); + String remaining = builder.getRemainingLowerCase(); + if (!remaining.isEmpty() && remaining.charAt(0) == '"') { + remaining = remaining.substring(1); + } + if (remaining.contains(" ")) { + remaining = remaining.replace(' ', '_'); + } + if (name.startsWith(remaining)) { + builder.suggest(name); + } + }); + + return builder.buildFuture(); + } + + public static final class GreedyArgument extends SkriptBrigadierArgument { + + public GreedyArgument(@NotNull ArgumentData argument) { + super(argument); + } + + @Override + public @NotNull ArgumentType getNativeType() { + return StringArgumentType.greedyString(); + } + + } + +} diff --git a/src/main/java/org/skriptlang/skript/bukkit/command/elements/structures/util/SkriptCommandExecutor.java b/src/main/java/org/skriptlang/skript/bukkit/command/elements/structures/util/SkriptCommandExecutor.java new file mode 100644 index 00000000000..e09e5c79f47 --- /dev/null +++ b/src/main/java/org/skriptlang/skript/bukkit/command/elements/structures/util/SkriptCommandExecutor.java @@ -0,0 +1,43 @@ +package org.skriptlang.skript.bukkit.command.elements.structures.util; + +import ch.njol.skript.lang.Trigger; +import ch.njol.skript.variables.Variables; +import com.mojang.brigadier.Command; +import com.mojang.brigadier.context.CommandContext; +import io.papermc.paper.command.brigadier.CommandSourceStack; + +import java.util.List; + +public class SkriptCommandExecutor { + + private final Trigger trigger; + private final List> arguments; + + public SkriptCommandExecutor(Trigger trigger, List> arguments) { + this.trigger = trigger; + this.arguments = arguments; + } + + public int execute(CommandContext context, int argCount) { + CommandEvent commandEvent = new CommandEvent(context.getSource().getSender()); + + int i = 0; + for (ArgumentData argument : arguments) { + if (argument.isAutomaticName()) { + continue; + } + Object value = null; + if (i < argCount) { + value = context.getArgument(argument.name(), argument.type().getC()); + } else if (argument.defaultValue() != null) { + value = argument.defaultValue().getSingle(commandEvent); + } + Variables.setVariable(argument.name(), value, commandEvent, true); + i++; + } + trigger.execute(commandEvent); + + return Command.SINGLE_SUCCESS; + } + +} From 15533d8c631f0d8c71cd6afd6764be3868641e62 Mon Sep 17 00:00:00 2001 From: Patrick Miller Date: Tue, 23 Jun 2026 10:14:09 -0400 Subject: [PATCH 02/41] Switch to entry-data based approach --- .../elements/structures/StructCommand.java | 175 +------------ .../structures/util/SubCommandEntryData.java | 239 ++++++++++++++++++ 2 files changed, 253 insertions(+), 161 deletions(-) create mode 100644 src/main/java/org/skriptlang/skript/bukkit/command/elements/structures/util/SubCommandEntryData.java 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 index 681a1455ca8..8c32ccbf0a0 100644 --- 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 @@ -1,59 +1,36 @@ package org.skriptlang.skript.bukkit.command.elements.structures; import ch.njol.skript.Skript; -import ch.njol.skript.classes.ClassInfo; -import ch.njol.skript.lang.Expression; +import ch.njol.skript.config.SectionNode; import ch.njol.skript.lang.Literal; -import ch.njol.skript.lang.ParseContext; -import ch.njol.skript.lang.SkriptParser; import ch.njol.skript.lang.SkriptParser.ParseResult; -import ch.njol.skript.lang.Trigger; -import ch.njol.skript.lang.parser.ParserInstance; -import ch.njol.skript.log.RetainingLogHandler; -import ch.njol.skript.registrations.Classes; -import ch.njol.skript.util.Utils; -import com.mojang.brigadier.builder.ArgumentBuilder; -import com.mojang.brigadier.builder.RequiredArgumentBuilder; import com.mojang.brigadier.tree.LiteralCommandNode; import io.papermc.paper.command.brigadier.CommandSourceStack; -import io.papermc.paper.command.brigadier.Commands; import io.papermc.paper.plugin.lifecycle.event.types.LifecycleEvents; import org.bukkit.Bukkit; 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.elements.structures.util.ArgumentData; import org.skriptlang.skript.bukkit.command.elements.structures.util.CommandEvent; -import org.skriptlang.skript.bukkit.command.elements.structures.util.SkriptBrigadierArgument; -import org.skriptlang.skript.bukkit.command.elements.structures.util.SkriptCommandExecutor; +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.entry.EntryValidator; -import org.skriptlang.skript.lang.entry.util.TriggerEntryData; import org.skriptlang.skript.lang.structure.Structure; import org.skriptlang.skript.registration.SyntaxInfo; import org.skriptlang.skript.registration.SyntaxRegistry; -import java.util.ArrayList; -import java.util.LinkedList; -import java.util.List; import java.util.Set; import java.util.concurrent.ConcurrentHashMap; import java.util.concurrent.atomic.AtomicBoolean; -import java.util.regex.Matcher; -import java.util.regex.Pattern; 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 [/]<\\S+> [<.+>]") - .entryValidator(EntryValidator.builder() - .addEntryData(new TriggerEntryData("trigger", null, false)) - .build()) + .addPattern("command <.+>") .build()); EventValueRegistry evRegistry = addon.registry(EventValueRegistry.class); @@ -65,6 +42,9 @@ public static void register(SkriptAddon addon, SyntaxRegistry syntaxRegistry) { }); } + private static final SubCommandEntryData ROOT_ENTRY_DATA = + new SubCommandEntryData("command", false, false); + private static final Set> COMMANDS = ConcurrentHashMap.newKeySet(); private static final AtomicBoolean SYNC_COMMANDS = new AtomicBoolean(); @@ -75,151 +55,24 @@ private static void performSync() { } } - /** - * Pattern for parsing arguments in the form: {@code }. - *
- * Where {@code name}, {@code type}, and {@code defaultValue} are groups {@code 1}, {@code 2}, and {@code 3}, respectively. - */ - private static final Pattern ARGUMENT_PATTERN = - Pattern.compile("<\\s*(?:([^>]+?)\\s*:\\s*)?(.+?)\\s*(?:=\\s*(" + SkriptParser.WILDCARD + "))?\\s*>"); - - private String name; - private EntryContainer entryContainer; - private final List> arguments = new ArrayList<>(); - + private SectionNode rootNode; private LiteralCommandNode command; @Override public boolean init(Literal[] args, int matchedPattern, ParseResult parseResult, EntryContainer entryContainer) { - name = parseResult.regexes.getFirst().group(); - this.entryContainer = entryContainer; - - if (parseResult.regexes.size() == 1) { // no arguments specified - return true; - } - - // parse arguments - Matcher argumentMatcher = ARGUMENT_PATTERN.matcher(parseResult.regexes.getLast().group()); - boolean optional = false; - while (argumentMatcher.find()) { - // first, parse the type - String rawType = argumentMatcher.group(2); - var plural = Utils.isPlural(rawType); - ClassInfo type = Classes.getClassInfoFromUserInput(plural.updated()); - if (type == null) { - Skript.error("'" + rawType + "' is not a known type."); - return false; - } 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 false; - } - - // 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; - if (arguments.stream().anyMatch(data -> data.name().equals(finalName))) { // already taken - // try to generate a name like 'number2', 'number3', etc - int i = 2; - baseLoop: while (true) { - for (var argument : arguments) { - if (argument.name().equals(name + i)) { - i++; - continue baseLoop; - } - } - break; - } - name += i; - } - } - - // 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 { - 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 { - logHandler.printLog(); - } - } - } - optional |= defaultValue != null; - - //noinspection unchecked, rawtypes - arguments.add(new ArgumentData(name, isAutomaticName, type, defaultValue, optional)); - } - + rootNode = entryContainer.getSource(); return true; } @Override public boolean load() { - ParserInstance parser = getParser(); - parser.setCurrentEvent("command execution trigger", CommandEvent.class); - Trigger execute = entryContainer.get("trigger", Trigger.class, false); - parser.deleteCurrentEvent(); - - var builder = Commands.literal(name); - - List> pieces = new LinkedList<>(); - pieces.addFirst(builder); - for (int i = 0; i < arguments.size(); i++) { - ArgumentData argument = arguments.get(i); - SkriptBrigadierArgument brigadierArgument; - if (i == arguments.size() - 1) { - // TODO maybe not desirable for numbers - brigadierArgument = new SkriptBrigadierArgument.GreedyArgument<>(argument); - } else { - brigadierArgument = new SkriptBrigadierArgument<>(argument); - } - pieces.addFirst(Commands.argument(argument.name(), brigadierArgument)); + var command = ROOT_ENTRY_DATA.getValue(rootNode); + if (command == null) { + return false; } + this.command = (LiteralCommandNode) command.build(); - // setup base - ArgumentBuilder base = pieces.removeFirst(); - SkriptCommandExecutor executor = new SkriptCommandExecutor(execute, arguments); - int argCount = arguments.size(); - final int finalBaseArgCount = argCount; - base.executes(context -> executor.execute(context, finalBaseArgCount)); - - // attach rest of argument pieces - var iterator = pieces.iterator(); - // wasOptional is so that we place an executes on the last, non-optional argument - boolean wasOptional = arguments.getLast().optional(); - while (iterator.hasNext()) { - argCount--; - final int finalArgCount = argCount; - base = iterator.next().then(base); - if (base instanceof RequiredArgumentBuilder requiredArgument && - ((SkriptBrigadierArgument) requiredArgument.getType()).getArgument().optional()) { - base.executes(context -> executor.execute(context, finalArgCount)); - wasOptional = true; - } else if (wasOptional) { - base.executes(context -> executor.execute(context, finalArgCount)); - wasOptional = false; - } - iterator.remove(); - } - - command = builder.build(); - - COMMANDS.add(command); + COMMANDS.add(this.command); SYNC_COMMANDS.set(true); return true; @@ -244,7 +97,7 @@ public void postUnload() { @Override public String toString(@Nullable Event event, boolean debug) { - return "command /" + name; + return "command"; } } 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..a0cd2258ff9 --- /dev/null +++ b/src/main/java/org/skriptlang/skript/bukkit/command/elements/structures/util/SubCommandEntryData.java @@ -0,0 +1,239 @@ +package org.skriptlang.skript.bukkit.command.elements.structures.util; + +import ch.njol.skript.ScriptLoader; +import ch.njol.skript.Skript; +import ch.njol.skript.classes.ClassInfo; +import ch.njol.skript.config.Node; +import ch.njol.skript.config.SectionNode; +import ch.njol.skript.lang.Expression; +import ch.njol.skript.lang.ParseContext; +import ch.njol.skript.lang.SkriptParser; +import ch.njol.skript.lang.Trigger; +import ch.njol.skript.lang.parser.ParserInstance; +import ch.njol.skript.log.RetainingLogHandler; +import ch.njol.skript.registrations.Classes; +import ch.njol.skript.util.Utils; +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 org.jetbrains.annotations.Nullable; +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.util.TriggerEntryData; + +import java.util.ArrayList; +import java.util.LinkedList; +import java.util.List; +import java.util.regex.Matcher; +import java.util.regex.Pattern; + +public class SubCommandEntryData extends EntryData> { + + private static final Pattern COMMAND_PATTERN = + Pattern.compile("(?i)^\\s*/?(\\S+)\\s*(?:\\s+(.+))?$"); + + public static final EntryValidator SUB_COMMAND_VALIDATOR = org.skriptlang.skript.lang.entry.EntryValidator.builder() + .addEntryData(new TriggerEntryData("trigger", null, false)) + .addEntryData(new SubCommandEntryData("subcommand", true, true)) + .build(); + + public SubCommandEntryData(String key, boolean optional, boolean multiple) { + super(key, null, optional, multiple); + } + + @Override + public @Nullable ArgumentBuilder 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; + } + + String name = commandMatcher.group(1).toLowerCase(); + // TODO validate whether command already exists + + // validate entries + EntryContainer entryContainer = SUB_COMMAND_VALIDATOR.validate((SectionNode) node); + if (entryContainer == null) { + return null; + } + + // parse arguments + List> arguments = parseArguments(commandMatcher.group(2)); + if (arguments == null) { + return null; + } + + // parse execution trigger + ParserInstance parser = ParserInstance.get(); + parser.setCurrentEvent("command execution trigger", CommandEvent.class); + Trigger execute = entryContainer.get("trigger", Trigger.class, false); + parser.deleteCurrentEvent(); + + // build the brigadier command + var builder = Commands.literal(name); + + List> pieces = new LinkedList<>(); + pieces.addFirst(builder); + for (int i = 0; i < arguments.size(); i++) { + ArgumentData argument = arguments.get(i); + SkriptBrigadierArgument brigadierArgument; + if (i == arguments.size() - 1) { + // TODO maybe not desirable for numbers + brigadierArgument = new SkriptBrigadierArgument.GreedyArgument<>(argument); + } else { + brigadierArgument = new SkriptBrigadierArgument<>(argument); + } + pieces.addFirst(Commands.argument(argument.name(), brigadierArgument)); + } + + // setup base + ArgumentBuilder base = pieces.removeFirst(); + SkriptCommandExecutor executor = new SkriptCommandExecutor(execute, arguments); + int argCount = arguments.size(); + final int finalBaseArgCount = argCount; + base.executes(context -> executor.execute(context, finalBaseArgCount)); + + // attach subcommand pieces + // TODO verify error behavior... + // TODO need to use parser instance data to push forward current command argument information + List> subcommands = + entryContainer.getAll("subcommand", ArgumentBuilder.class, false); + for (var subcommand : subcommands) { + base.then(subcommand); + } + + // attach rest of argument pieces + var iterator = pieces.iterator(); + // wasOptional is so that we place an executes on the last, non-optional argument + boolean wasOptional = !arguments.isEmpty() && arguments.getLast().optional(); + while (iterator.hasNext()) { + argCount--; + final int finalArgCount = argCount; + base = iterator.next().then(base); + if (base instanceof RequiredArgumentBuilder requiredArgument && + ((SkriptBrigadierArgument) requiredArgument.getType()).getArgument().optional()) { + base.executes(context -> executor.execute(context, finalArgCount)); + wasOptional = true; + } else if (wasOptional) { + base.executes(context -> executor.execute(context, finalArgCount)); + wasOptional = false; + } + iterator.remove(); + } + + return builder; + } + + @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()); + } + + /* + * Argument Parsing + */ + + /** + * Pattern for parsing arguments in the form: {@code }. + *
+ * Where {@code name}, {@code type}, and {@code defaultValue} are groups {@code 1}, {@code 2}, and {@code 3}, respectively. + */ + // TODO optional brackets + private static final Pattern ARGUMENT_PATTERN = + Pattern.compile("<\\s*(?:([^>]+?)\\s*:\\s*)?(.+?)\\s*(?:=\\s*(" + SkriptParser.WILDCARD + "))?\\s*>"); + + private static @Nullable List> parseArguments(@Nullable String input) { + if (input == null) { + return List.of(); + } + input = input.toLowerCase(); + List> arguments = new ArrayList<>(); + Matcher argumentMatcher = ARGUMENT_PATTERN.matcher(input); + boolean optional = false; + while (argumentMatcher.find()) { + // first, parse the type + String rawType = argumentMatcher.group(2); + 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; + } + + // 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; + if (arguments.stream().anyMatch(data -> data.name().equals(finalName))) { // already taken + // try to generate a name like 'number2', 'number3', etc + int i = 2; + baseLoop: while (true) { + for (var argument : arguments) { + if (argument.name().equals(name + i)) { + i++; + continue baseLoop; + } + } + break; + } + name += i; + } + } + + // 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 { + 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 { + logHandler.printLog(); + } + } + } + optional |= defaultValue != null; + + //noinspection unchecked, rawtypes + arguments.add(new ArgumentData(name, isAutomaticName, type, defaultValue, optional)); + } + return arguments; + } + +} From 92bef1012007b29e3124c30e7db3cc4758692782 Mon Sep 17 00:00:00 2001 From: Patrick Miller Date: Tue, 23 Jun 2026 11:51:56 -0400 Subject: [PATCH 03/41] Handle argument forwarding, better default string argument type --- .../util/SkriptBrigadierArgument.java | 19 ++------- .../structures/util/SubCommandEntryData.java | 40 ++++++++++++++----- 2 files changed, 35 insertions(+), 24 deletions(-) diff --git a/src/main/java/org/skriptlang/skript/bukkit/command/elements/structures/util/SkriptBrigadierArgument.java b/src/main/java/org/skriptlang/skript/bukkit/command/elements/structures/util/SkriptBrigadierArgument.java index 9b2bfb75248..924e475c986 100644 --- a/src/main/java/org/skriptlang/skript/bukkit/command/elements/structures/util/SkriptBrigadierArgument.java +++ b/src/main/java/org/skriptlang/skript/bukkit/command/elements/structures/util/SkriptBrigadierArgument.java @@ -25,9 +25,11 @@ public class SkriptBrigadierArgument implements CustomArgumentType.Converted< (input, type) -> new LiteralMessage("'%s' is not a valid %s.".formatted(input, type))); private final ArgumentData argument; + private final StringArgumentType nativeType; - public SkriptBrigadierArgument(@NotNull ArgumentData argument) { + public SkriptBrigadierArgument(@NotNull ArgumentData argument, @NotNull StringArgumentType nativeType) { this.argument = argument; + this.nativeType = nativeType; } public ArgumentData getArgument() { @@ -52,7 +54,7 @@ public ArgumentData getArgument() { @Override public @NotNull ArgumentType getNativeType() { - return StringArgumentType.string(); + return nativeType; } @Override @@ -80,17 +82,4 @@ public ArgumentData getArgument() { return builder.buildFuture(); } - public static final class GreedyArgument extends SkriptBrigadierArgument { - - public GreedyArgument(@NotNull ArgumentData argument) { - super(argument); - } - - @Override - public @NotNull ArgumentType getNativeType() { - return StringArgumentType.greedyString(); - } - - } - } 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 index a0cd2258ff9..cb3f7ba5d4f 100644 --- 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 @@ -13,6 +13,7 @@ import ch.njol.skript.log.RetainingLogHandler; import ch.njol.skript.registrations.Classes; import ch.njol.skript.util.Utils; +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; @@ -31,6 +32,16 @@ public class SubCommandEntryData extends EntryData> { + private static final class CommandParsingData extends ParserInstance.Data { + + public List>> arguments = new LinkedList<>(); + + public CommandParsingData(ParserInstance parserInstance) { + super(parserInstance); + } + + } + private static final Pattern COMMAND_PATTERN = Pattern.compile("(?i)^\\s*/?(\\S+)\\s*(?:\\s+(.+))?$"); @@ -39,6 +50,10 @@ public class SubCommandEntryData extends EntryData argument = arguments.get(i); - SkriptBrigadierArgument brigadierArgument; - if (i == arguments.size() - 1) { - // TODO maybe not desirable for numbers - brigadierArgument = new SkriptBrigadierArgument.GreedyArgument<>(argument); + StringArgumentType nativeType; + if (Number.class.isAssignableFrom(argument.type().getC())) { + nativeType = StringArgumentType.word(); + } else if (i == arguments.size() - 1) { + nativeType = StringArgumentType.greedyString(); } else { - brigadierArgument = new SkriptBrigadierArgument<>(argument); + nativeType = StringArgumentType.string(); } - pieces.addFirst(Commands.argument(argument.name(), brigadierArgument)); + pieces.addFirst(Commands.argument(argument.name(), new SkriptBrigadierArgument<>(argument, nativeType))); } + CommandParsingData parsingData = parser.getData(CommandParsingData.class); + parsingData.arguments.addLast(arguments); + // setup base ArgumentBuilder base = pieces.removeFirst(); - SkriptCommandExecutor executor = new SkriptCommandExecutor(execute, arguments); - int argCount = arguments.size(); + List> allArguments = parsingData.arguments.stream() + .flatMap(List::stream) + .toList(); + SkriptCommandExecutor executor = new SkriptCommandExecutor(execute, allArguments); + int argCount = allArguments.size(); final int finalBaseArgCount = argCount; base.executes(context -> executor.execute(context, finalBaseArgCount)); // attach subcommand pieces // TODO verify error behavior... - // TODO need to use parser instance data to push forward current command argument information List> subcommands = entryContainer.getAll("subcommand", ArgumentBuilder.class, false); for (var subcommand : subcommands) { base.then(subcommand); } + parsingData.arguments.removeLast(); // attach rest of argument pieces var iterator = pieces.iterator(); From 72d682df91b3c772fe59e467ab6c95b0064a23bb Mon Sep 17 00:00:00 2001 From: Patrick Miller Date: Tue, 23 Jun 2026 12:59:54 -0400 Subject: [PATCH 04/41] Proper native type support --- .../util/SkriptBrigadierArgument.java | 17 +++++++++++++++++ .../structures/util/SkriptCommandExecutor.java | 13 +++++++++++-- .../structures/util/SubCommandEntryData.java | 18 ++++++++++-------- 3 files changed, 38 insertions(+), 10 deletions(-) diff --git a/src/main/java/org/skriptlang/skript/bukkit/command/elements/structures/util/SkriptBrigadierArgument.java b/src/main/java/org/skriptlang/skript/bukkit/command/elements/structures/util/SkriptBrigadierArgument.java index 924e475c986..a748e7949ea 100644 --- a/src/main/java/org/skriptlang/skript/bukkit/command/elements/structures/util/SkriptBrigadierArgument.java +++ b/src/main/java/org/skriptlang/skript/bukkit/command/elements/structures/util/SkriptBrigadierArgument.java @@ -5,22 +5,39 @@ 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.SimpleCommandExceptionType; 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 org.bukkit.entity.Player; import org.jetbrains.annotations.NotNull; import java.util.Iterator; import java.util.Locale; +import java.util.Map; import java.util.concurrent.CompletableFuture; import java.util.function.Supplier; public class SkriptBrigadierArgument implements CustomArgumentType.Converted { + public static final Map, ArgumentType> ARGUMENT_TYPE_MAPPINGS = Map.ofEntries( + Map.entry(Boolean.class, BoolArgumentType.bool()), + Map.entry(Long.class, LongArgumentType.longArg()), + Map.entry(Number.class, DoubleArgumentType.doubleArg()), + Map.entry(Player.class, ArgumentTypes.player()) + ); + + private static final SimpleCommandExceptionType ERROR_BAD_SOURCE = new SimpleCommandExceptionType( + new LiteralMessage("The source needs to be a CommandSourceStack!")); + private static final Dynamic2CommandExceptionType ERROR_INVALID_INPUT = new Dynamic2CommandExceptionType( (input, type) -> new LiteralMessage("'%s' is not a valid %s.".formatted(input, type))); diff --git a/src/main/java/org/skriptlang/skript/bukkit/command/elements/structures/util/SkriptCommandExecutor.java b/src/main/java/org/skriptlang/skript/bukkit/command/elements/structures/util/SkriptCommandExecutor.java index e09e5c79f47..e39f7f5685b 100644 --- a/src/main/java/org/skriptlang/skript/bukkit/command/elements/structures/util/SkriptCommandExecutor.java +++ b/src/main/java/org/skriptlang/skript/bukkit/command/elements/structures/util/SkriptCommandExecutor.java @@ -4,9 +4,12 @@ import ch.njol.skript.variables.Variables; import com.mojang.brigadier.Command; import com.mojang.brigadier.context.CommandContext; +import com.mojang.brigadier.exceptions.CommandSyntaxException; import io.papermc.paper.command.brigadier.CommandSourceStack; +import io.papermc.paper.command.brigadier.argument.resolvers.ArgumentResolver; import java.util.List; +import java.util.SequencedCollection; public class SkriptCommandExecutor { @@ -18,7 +21,7 @@ public SkriptCommandExecutor(Trigger trigger, List> arguments) { this.arguments = arguments; } - public int execute(CommandContext context, int argCount) { + public int execute(CommandContext context, int argCount) throws CommandSyntaxException { CommandEvent commandEvent = new CommandEvent(context.getSource().getSender()); int i = 0; @@ -28,7 +31,13 @@ public int execute(CommandContext context, int argCount) { } Object value = null; if (i < argCount) { - value = context.getArgument(argument.name(), argument.type().getC()); + value = context.getArgument(argument.name(), Object.class); + if (value instanceof ArgumentResolver argumentResolver) { + value = argumentResolver.resolve(context.getSource()); + if (value instanceof SequencedCollection collection) { + value = collection.getFirst(); + } + } } else if (argument.defaultValue() != null) { value = argument.defaultValue().getSingle(commandEvent); } 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 index cb3f7ba5d4f..ad877035bef 100644 --- 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 @@ -13,6 +13,7 @@ import ch.njol.skript.log.RetainingLogHandler; import ch.njol.skript.registrations.Classes; import ch.njol.skript.util.Utils; +import com.mojang.brigadier.arguments.ArgumentType; import com.mojang.brigadier.arguments.StringArgumentType; import com.mojang.brigadier.builder.ArgumentBuilder; import com.mojang.brigadier.builder.RequiredArgumentBuilder; @@ -103,15 +104,16 @@ public SubCommandEntryData(String key, boolean optional, boolean multiple) { pieces.addFirst(builder); for (int i = 0; i < arguments.size(); i++) { ArgumentData argument = arguments.get(i); - StringArgumentType nativeType; - if (Number.class.isAssignableFrom(argument.type().getC())) { - nativeType = StringArgumentType.word(); - } else if (i == arguments.size() - 1) { - nativeType = StringArgumentType.greedyString(); - } else { - nativeType = StringArgumentType.string(); + ArgumentType nativeType = SkriptBrigadierArgument.ARGUMENT_TYPE_MAPPINGS.get(argument.type().getC()); + if (nativeType == null) { + if (i == arguments.size() - 1) { + nativeType = StringArgumentType.greedyString(); + } else { + nativeType = StringArgumentType.string(); + } + nativeType = new SkriptBrigadierArgument<>(argument, (StringArgumentType) nativeType); } - pieces.addFirst(Commands.argument(argument.name(), new SkriptBrigadierArgument<>(argument, nativeType))); + pieces.addFirst(Commands.argument(argument.name(), nativeType)); } CommandParsingData parsingData = parser.getData(CommandParsingData.class); From 90f7e3577e4d440fc08c7bba8a8ba309429144e7 Mon Sep 17 00:00:00 2001 From: Patrick Miller Date: Tue, 23 Jun 2026 13:31:40 -0400 Subject: [PATCH 05/41] Don't require a trigger when subcommands are present --- .../structures/util/SubCommandEntryData.java | 49 ++++++++++++------- 1 file changed, 30 insertions(+), 19 deletions(-) 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 index ad877035bef..0923c191047 100644 --- 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 @@ -47,7 +47,7 @@ public CommandParsingData(ParserInstance parserInstance) { Pattern.compile("(?i)^\\s*/?(\\S+)\\s*(?:\\s+(.+))?$"); public static final EntryValidator SUB_COMMAND_VALIDATOR = org.skriptlang.skript.lang.entry.EntryValidator.builder() - .addEntryData(new TriggerEntryData("trigger", null, false)) + .addEntryData(new TriggerEntryData("trigger", null, true)) .addEntryData(new SubCommandEntryData("subcommand", true, true)) .build(); @@ -94,7 +94,8 @@ public SubCommandEntryData(String key, boolean optional, boolean multiple) { // parse execution trigger ParserInstance parser = ParserInstance.get(); parser.setCurrentEvent("command execution trigger", CommandEvent.class); - Trigger execute = entryContainer.get("trigger", Trigger.class, false); + Trigger execute = entryContainer.getOptional("trigger", Trigger.class, false); + boolean hasExecute = execute != null; parser.deleteCurrentEvent(); // build the brigadier command @@ -119,20 +120,30 @@ public SubCommandEntryData(String key, boolean optional, boolean multiple) { CommandParsingData parsingData = parser.getData(CommandParsingData.class); parsingData.arguments.addLast(arguments); - // setup base + // setup executor and base ArgumentBuilder base = pieces.removeFirst(); - List> allArguments = parsingData.arguments.stream() - .flatMap(List::stream) - .toList(); - SkriptCommandExecutor executor = new SkriptCommandExecutor(execute, allArguments); - int argCount = allArguments.size(); - final int finalBaseArgCount = argCount; - base.executes(context -> executor.execute(context, finalBaseArgCount)); + SkriptCommandExecutor executor; + int argCount = 0; + if (hasExecute) { + List> allArguments = parsingData.arguments.stream() + .flatMap(List::stream) + .toList(); + argCount = allArguments.size(); + final int finalBaseArgCount = argCount; + executor = new SkriptCommandExecutor(execute, allArguments); + base.executes(context -> executor.execute(context, finalBaseArgCount)); + } else { + executor = null; + } // attach subcommand pieces // TODO verify error behavior... List> subcommands = entryContainer.getAll("subcommand", ArgumentBuilder.class, false); + if (subcommands.isEmpty() && !hasExecute) { + Skript.error("You must have a 'trigger' entry if there are no subcommands!"); + return null; + } for (var subcommand : subcommands) { base.then(subcommand); } @@ -141,18 +152,18 @@ public SubCommandEntryData(String key, boolean optional, boolean multiple) { // attach rest of argument pieces var iterator = pieces.iterator(); // wasOptional is so that we place an executes on the last, non-optional argument - boolean wasOptional = !arguments.isEmpty() && arguments.getLast().optional(); + boolean wasOptional = hasExecute && !arguments.isEmpty() && arguments.getLast().optional(); while (iterator.hasNext()) { argCount--; - final int finalArgCount = argCount; base = iterator.next().then(base); - if (base instanceof RequiredArgumentBuilder requiredArgument && - ((SkriptBrigadierArgument) requiredArgument.getType()).getArgument().optional()) { - base.executes(context -> executor.execute(context, finalArgCount)); - wasOptional = true; - } else if (wasOptional) { - base.executes(context -> executor.execute(context, finalArgCount)); - wasOptional = false; + if (hasExecute) { + boolean isBaseOptional = base instanceof RequiredArgumentBuilder requiredArgument && + ((SkriptBrigadierArgument) requiredArgument.getType()).getArgument().optional(); + final int finalArgCount = argCount; + if (isBaseOptional || wasOptional) { + base.executes(context -> executor.execute(context, finalArgCount)); + wasOptional = isBaseOptional; + } } iterator.remove(); } From 61d9d234ffd012b244377088c7cf0b2b02f3ae4c Mon Sep 17 00:00:00 2001 From: Patrick Miller Date: Tue, 23 Jun 2026 14:46:23 -0400 Subject: [PATCH 06/41] Simplify argument handling --- .../elements/structures/StructCommand.java | 7 ++- .../structures/util/ArgumentData.java | 8 +++- .../util/SkriptCommandExecutor.java | 2 +- .../structures/util/SubCommandEntryData.java | 48 ++++++++++++++----- 4 files changed, 50 insertions(+), 15 deletions(-) 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 index 8c32ccbf0a0..f1bd2448dc6 100644 --- 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 @@ -70,7 +70,12 @@ public boolean load() { if (command == null) { return false; } - this.command = (LiteralCommandNode) command.build(); + if (!(command.build() instanceof LiteralCommandNode commandNode)) { + Skript.error("A command must have a name."); + return false; + } + this.command = commandNode; + // TODO validate whether command already exists COMMANDS.add(this.command); SYNC_COMMANDS.set(true); diff --git a/src/main/java/org/skriptlang/skript/bukkit/command/elements/structures/util/ArgumentData.java b/src/main/java/org/skriptlang/skript/bukkit/command/elements/structures/util/ArgumentData.java index 562b5bb843f..9af6cb282dd 100644 --- a/src/main/java/org/skriptlang/skript/bukkit/command/elements/structures/util/ArgumentData.java +++ b/src/main/java/org/skriptlang/skript/bukkit/command/elements/structures/util/ArgumentData.java @@ -10,4 +10,10 @@ public record ArgumentData( ClassInfo type, @Nullable Expression defaultValue, boolean optional -) { } +) { + + public boolean isLiteral() { + return type == null; + } + +} diff --git a/src/main/java/org/skriptlang/skript/bukkit/command/elements/structures/util/SkriptCommandExecutor.java b/src/main/java/org/skriptlang/skript/bukkit/command/elements/structures/util/SkriptCommandExecutor.java index e39f7f5685b..08a0fdd1bd9 100644 --- a/src/main/java/org/skriptlang/skript/bukkit/command/elements/structures/util/SkriptCommandExecutor.java +++ b/src/main/java/org/skriptlang/skript/bukkit/command/elements/structures/util/SkriptCommandExecutor.java @@ -26,7 +26,7 @@ public int execute(CommandContext context, int argCount) thr int i = 0; for (ArgumentData argument : arguments) { - if (argument.isAutomaticName()) { + if (argument.isLiteral() || argument.isAutomaticName()) { continue; } Object value = null; 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 index 0923c191047..f549a15fa85 100644 --- 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 @@ -44,7 +44,7 @@ public CommandParsingData(ParserInstance parserInstance) { } private static final Pattern COMMAND_PATTERN = - Pattern.compile("(?i)^\\s*/?(\\S+)\\s*(?:\\s+(.+))?$"); + Pattern.compile("(?i)^\\s*/?\\s*(.+)?$"); public static final EntryValidator SUB_COMMAND_VALIDATOR = org.skriptlang.skript.lang.entry.EntryValidator.builder() .addEntryData(new TriggerEntryData("trigger", null, true)) @@ -76,9 +76,6 @@ public SubCommandEntryData(String key, boolean optional, boolean multiple) { return null; } - String name = commandMatcher.group(1).toLowerCase(); - // TODO validate whether command already exists - // validate entries EntryContainer entryContainer = SUB_COMMAND_VALIDATOR.validate((SectionNode) node); if (entryContainer == null) { @@ -86,9 +83,16 @@ public SubCommandEntryData(String key, boolean optional, boolean multiple) { } // parse arguments - List> arguments = parseArguments(commandMatcher.group(2)); + List> arguments = parseArguments(commandMatcher.group(1)); if (arguments == null) { return null; + } else if (arguments.isEmpty()) { + if (ParserInstance.get().getData(CommandParsingData.class).arguments.isEmpty()) { + Skript.error("A command must have a name."); + } else { + Skript.error("A subcommand must have at least one argument, literal or dynamic."); + } + return null; } // parse execution trigger @@ -98,13 +102,15 @@ public SubCommandEntryData(String key, boolean optional, boolean multiple) { boolean hasExecute = execute != null; parser.deleteCurrentEvent(); - // build the brigadier command - var builder = Commands.literal(name); - List> pieces = new LinkedList<>(); - pieces.addFirst(builder); for (int i = 0; i < arguments.size(); i++) { ArgumentData argument = arguments.get(i); + + if (argument.isLiteral()) { + pieces.addFirst(Commands.literal(argument.name())); + continue; + } + ArgumentType nativeType = SkriptBrigadierArgument.ARGUMENT_TYPE_MAPPINGS.get(argument.type().getC()); if (nativeType == null) { if (i == arguments.size() - 1) { @@ -168,7 +174,7 @@ public SubCommandEntryData(String key, boolean optional, boolean multiple) { iterator.remove(); } - return builder; + return base; } @Override @@ -201,11 +207,22 @@ public boolean canCreateWith(Node node) { if (input == null) { return List.of(); } + input = input.trim(); + if (input.isEmpty()) { + return List.of(); + } input = input.toLowerCase(); List> arguments = new ArrayList<>(); Matcher argumentMatcher = ARGUMENT_PATTERN.matcher(input); boolean optional = false; + int lastEnd = 0; while (argumentMatcher.find()) { + String between = input.substring(lastEnd, argumentMatcher.start()).trim(); + if (!between.isEmpty()) { // there is a literal argument here + arguments.add(new ArgumentData<>(between, false, null, null, optional)); + } + lastEnd = argumentMatcher.end(); + // first, parse the type String rawType = argumentMatcher.group(2); var plural = Utils.isPlural(rawType); @@ -225,12 +242,14 @@ public boolean canCreateWith(Node node) { isAutomaticName = true; name = type.getName().getSingular(); String finalName = name; - if (arguments.stream().anyMatch(data -> data.name().equals(finalName))) { // already taken + // TODO this needs to consider preceding arguments from parser data + if (arguments.stream().anyMatch(data -> + data instanceof ArgumentData argumentData && argumentData.name().equals(finalName))) { // already taken // try to generate a name like 'number2', 'number3', etc int i = 2; baseLoop: while (true) { for (var argument : arguments) { - if (argument.name().equals(name + i)) { + if (argument instanceof ArgumentData argumentData && argumentData.name().equals(name + i)) { i++; continue baseLoop; } @@ -268,6 +287,11 @@ public boolean canCreateWith(Node node) { //noinspection unchecked, rawtypes arguments.add(new ArgumentData(name, isAutomaticName, type, defaultValue, optional)); } + + if (arguments.isEmpty()) { // it is just a singular literal argument then + arguments.add(new ArgumentData<>(input, false, null, null, false)); + } + return arguments; } From eed9d0ec9387dffbf5bae101c69565a0d200849a Mon Sep 17 00:00:00 2001 From: Patrick Miller Date: Tue, 23 Jun 2026 22:18:43 -0400 Subject: [PATCH 07/41] Command parser 9000 has arrived --- .../elements/structures/StructCommand.java | 2 +- .../structures/util/ArgumentData.java | 11 +- .../structures/util/CommandCompiler.java | 244 ++++++++++++++++++ .../util/SkriptBrigadierArgument.java | 9 +- .../util/SkriptCommandExecutor.java | 21 +- .../structures/util/SubCommandEntryData.java | 230 +++++------------ 6 files changed, 328 insertions(+), 189 deletions(-) create mode 100644 src/main/java/org/skriptlang/skript/bukkit/command/elements/structures/util/CommandCompiler.java 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 index f1bd2448dc6..f5d2b3699f9 100644 --- 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 @@ -70,7 +70,7 @@ public boolean load() { if (command == null) { return false; } - if (!(command.build() instanceof LiteralCommandNode commandNode)) { + if (command.size() != 1 || !(command.getFirst().build() instanceof LiteralCommandNode commandNode)) { Skript.error("A command must have a name."); return false; } diff --git a/src/main/java/org/skriptlang/skript/bukkit/command/elements/structures/util/ArgumentData.java b/src/main/java/org/skriptlang/skript/bukkit/command/elements/structures/util/ArgumentData.java index 9af6cb282dd..302d9a5d6f6 100644 --- a/src/main/java/org/skriptlang/skript/bukkit/command/elements/structures/util/ArgumentData.java +++ b/src/main/java/org/skriptlang/skript/bukkit/command/elements/structures/util/ArgumentData.java @@ -8,12 +8,5 @@ public record ArgumentData( String name, boolean isAutomaticName, ClassInfo type, - @Nullable Expression defaultValue, - boolean optional -) { - - public boolean isLiteral() { - return type == null; - } - -} + @Nullable Expression defaultValue +) { } 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..cd72982a7dd --- /dev/null +++ b/src/main/java/org/skriptlang/skript/bukkit/command/elements/structures/util/CommandCompiler.java @@ -0,0 +1,244 @@ +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 java.util.ArrayList; +import java.util.Collection; +import java.util.LinkedHashSet; +import java.util.List; +import java.util.regex.Matcher; +import java.util.regex.Pattern; +import java.util.stream.Stream; + +public class CommandCompiler { + + public static class CommandElement { + + public LinkedHashSet children = new LinkedHashSet<>(); + + public void append(Collection elements) { + // Element validation + elements = elements.stream() + .flatMap(element -> { + if (element instanceof ChoiceCommandElement choice) { + return choice.children.stream(); + } + if (element == this) { + return Stream.empty(); + } + return Stream.of(element); + }) + .toList(); + + if (children.isEmpty()) { // we reached the end + children.addAll(elements); + return; + } + + boolean hasNull = false; + for (CommandElement child : children) { + if (child == null) { + hasNull = true; + } else { + child.append(elements); + } + } + if (hasNull) { + children.remove(null); + children.addAll(elements); + } + } + + } + + public static class LiteralCommandElement extends CommandElement { + + private final String string; + + public LiteralCommandElement(String string) { + this.string = string; + } + + public String literal() { + return string; + } + + } + + public static class ArgumentCommandElement extends CommandElement { + + private final ArgumentData argument; + + public ArgumentCommandElement(ArgumentData argument) { + this.argument = argument; + } + + public ArgumentData argument() { + return argument; + } + + } + + public static class ChoiceCommandElement extends CommandElement { + + @Override + public void append(Collection elements) { + CommandElement last = children.getLast(); + if (last == null) { + children.removeLast(); + children.addAll(elements); + } else { + children.getLast().append(elements); + } + } + + } + + private static StringBuilder tryLiteralAppend(CommandElement first, StringBuilder literalBuilder) { + if (literalBuilder.isEmpty()) { + return literalBuilder; + } + String literal = literalBuilder.toString().trim(); + if (!literal.isEmpty()) { + first.append(List.of(new LiteralCommandElement(literal))); + } + return new StringBuilder(); + } + + public static CommandElement compile(String pattern, List> arguments) { + StringBuilder literalBuilder = new StringBuilder(); + CommandElement first = new CommandElement(); + + for (int i = 0; i < pattern.length(); i++) { + char c = pattern.charAt(i); + if (c == '[') { + literalBuilder = tryLiteralAppend(first, literalBuilder); + + int end = SkriptParser.nextBracket(pattern, ']', c, i + 1, true); + CommandElement commandElement = compile(pattern.substring(i + 1, end), arguments); + if (commandElement == null) { + return null; + } + + List toAppend = new ArrayList<>(commandElement.children); + toAppend.add(null); + first.append(toAppend); + + i = end; + } else if (c == '(') { + literalBuilder = tryLiteralAppend(first, literalBuilder); + + int end = SkriptParser.nextBracket(pattern, ')', c, i + 1, true); + CommandElement commandElement = compile(pattern.substring(i + 1, end), arguments); + if (commandElement == null) { + return null; + } + + first.append(commandElement.children); + + i = end; + } else if (c == '|') { + literalBuilder = tryLiteralAppend(first, literalBuilder); + + ChoiceCommandElement choiceElement; + if (first instanceof ChoiceCommandElement choiceCommandElement) { + choiceElement = choiceCommandElement; + } else { + choiceElement = new ChoiceCommandElement(); + choiceElement.children.addAll(first.children); + first = choiceElement; + } + choiceElement.children.add(null); + } else if (c == '<') { + literalBuilder = tryLiteralAppend(first, literalBuilder); + + int end = SkriptParser.nextBracket(pattern, '>', c, i + 1, true); + ArgumentData argument = parseArgument(pattern.substring(i + 1, end)); + if (argument == null) { + return null; + } + + arguments.add(argument); + first.append(List.of(new ArgumentCommandElement(argument))); + + i = end; + } else if (c == '\\') { + i++; + literalBuilder.append(pattern.charAt(i)); + } else { + literalBuilder.append(c); + } + } + + tryLiteralAppend(first, literalBuilder); + + return first; + } + + private static final Pattern ARGUMENT_PATTERN = + Pattern.compile("^\\s*(?:([^>]+?)\\s*:\\s*)?(.+?)\\s*(?:=\\s*(" + SkriptParser.WILDCARD + "))?\\s*$"); + + private static @Nullable ArgumentData parseArgument(String argument) { + Matcher argumentMatcher = ARGUMENT_PATTERN.matcher(argument); + if (!argumentMatcher.find()) { + return null; + } + + // first, parse the type + String rawType = argumentMatcher.group(2); + 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; + } + + // 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; + // TODO verify not duplicate argument name + } + + // 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 { + 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 { + logHandler.printLog(); + } + } + } + + //noinspection unchecked, rawtypes + return new ArgumentData(name, isAutomaticName, type, defaultValue); + } + +} diff --git a/src/main/java/org/skriptlang/skript/bukkit/command/elements/structures/util/SkriptBrigadierArgument.java b/src/main/java/org/skriptlang/skript/bukkit/command/elements/structures/util/SkriptBrigadierArgument.java index a748e7949ea..e6a5aeefd4f 100644 --- a/src/main/java/org/skriptlang/skript/bukkit/command/elements/structures/util/SkriptBrigadierArgument.java +++ b/src/main/java/org/skriptlang/skript/bukkit/command/elements/structures/util/SkriptBrigadierArgument.java @@ -12,7 +12,6 @@ import com.mojang.brigadier.context.CommandContext; import com.mojang.brigadier.exceptions.CommandSyntaxException; import com.mojang.brigadier.exceptions.Dynamic2CommandExceptionType; -import com.mojang.brigadier.exceptions.SimpleCommandExceptionType; import com.mojang.brigadier.suggestion.Suggestions; import com.mojang.brigadier.suggestion.SuggestionsBuilder; import io.papermc.paper.command.brigadier.argument.ArgumentTypes; @@ -35,12 +34,11 @@ public class SkriptBrigadierArgument implements CustomArgumentType.Converted< Map.entry(Player.class, ArgumentTypes.player()) ); - private static final SimpleCommandExceptionType ERROR_BAD_SOURCE = new SimpleCommandExceptionType( - new LiteralMessage("The source needs to be a CommandSourceStack!")); - private static final Dynamic2CommandExceptionType ERROR_INVALID_INPUT = new Dynamic2CommandExceptionType( (input, type) -> new LiteralMessage("'%s' is not a valid %s.".formatted(input, type))); + static final Object DEFAULT_PLACEHOLDER = new Object(); + private final ArgumentData argument; private final StringArgumentType nativeType; @@ -60,7 +58,8 @@ public ArgumentData getArgument() { T result = argument.type().getParser().parse(input, ParseContext.COMMAND); if (result == null) { if (argument.defaultValue() != null) { // attempt default value - result = argument.defaultValue().getSingle(ContextlessEvent.get()); + //noinspection unchecked + result = (T) DEFAULT_PLACEHOLDER; } if (result == null) { throw ERROR_INVALID_INPUT.create(input, argument.type().getName().getSingular()); diff --git a/src/main/java/org/skriptlang/skript/bukkit/command/elements/structures/util/SkriptCommandExecutor.java b/src/main/java/org/skriptlang/skript/bukkit/command/elements/structures/util/SkriptCommandExecutor.java index 08a0fdd1bd9..a14551ef7bc 100644 --- a/src/main/java/org/skriptlang/skript/bukkit/command/elements/structures/util/SkriptCommandExecutor.java +++ b/src/main/java/org/skriptlang/skript/bukkit/command/elements/structures/util/SkriptCommandExecutor.java @@ -5,11 +5,14 @@ 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 java.util.List; import java.util.SequencedCollection; +import java.util.Set; +import java.util.stream.Collectors; public class SkriptCommandExecutor { @@ -21,28 +24,30 @@ public SkriptCommandExecutor(Trigger trigger, List> arguments) { this.arguments = arguments; } - public int execute(CommandContext context, int argCount) throws CommandSyntaxException { + public int execute(CommandContext context) throws CommandSyntaxException { CommandEvent commandEvent = new CommandEvent(context.getSource().getSender()); - int i = 0; + Set providedArgs = context.getNodes().stream() + .filter(node -> node.getNode() instanceof ArgumentCommandNode) + .map(node -> node.getNode().getName()) + .collect(Collectors.toSet()); for (ArgumentData argument : arguments) { - if (argument.isLiteral() || argument.isAutomaticName()) { - continue; - } Object value = null; - if (i < argCount) { + if (providedArgs.contains(argument.name())) { value = context.getArgument(argument.name(), Object.class); - if (value instanceof ArgumentResolver argumentResolver) { + if (value instanceof ArgumentResolver argumentResolver) { // native type needs resolved value = argumentResolver.resolve(context.getSource()); if (value instanceof SequencedCollection collection) { value = collection.getFirst(); } + } else if (value == SkriptBrigadierArgument.DEFAULT_PLACEHOLDER) { + assert argument.defaultValue() != null; + value = argument.defaultValue().getSingle(commandEvent); } } else if (argument.defaultValue() != null) { value = argument.defaultValue().getSingle(commandEvent); } Variables.setVariable(argument.name(), value, commandEvent, true); - i++; } trigger.execute(commandEvent); 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 index f549a15fa85..422b4091eb6 100644 --- 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 @@ -2,38 +2,36 @@ import ch.njol.skript.ScriptLoader; import ch.njol.skript.Skript; -import ch.njol.skript.classes.ClassInfo; import ch.njol.skript.config.Node; import ch.njol.skript.config.SectionNode; -import ch.njol.skript.lang.Expression; -import ch.njol.skript.lang.ParseContext; -import ch.njol.skript.lang.SkriptParser; import ch.njol.skript.lang.Trigger; import ch.njol.skript.lang.parser.ParserInstance; -import ch.njol.skript.log.RetainingLogHandler; -import ch.njol.skript.registrations.Classes; -import ch.njol.skript.util.Utils; +import ch.njol.skript.lang.parser.ParserInstance.Data; 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 org.jetbrains.annotations.Nullable; +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.LiteralCommandElement; 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.util.TriggerEntryData; import java.util.ArrayList; +import java.util.Collection; import java.util.LinkedList; import java.util.List; +import java.util.Objects; import java.util.regex.Matcher; import java.util.regex.Pattern; -public class SubCommandEntryData extends EntryData> { +public class SubCommandEntryData extends EntryData>> { - private static final class CommandParsingData extends ParserInstance.Data { + private static final class CommandParsingData extends Data { public List>> arguments = new LinkedList<>(); @@ -46,7 +44,7 @@ public CommandParsingData(ParserInstance parserInstance) { private static final Pattern COMMAND_PATTERN = Pattern.compile("(?i)^\\s*/?\\s*(.+)?$"); - public static final EntryValidator SUB_COMMAND_VALIDATOR = org.skriptlang.skript.lang.entry.EntryValidator.builder() + public static final EntryValidator SUB_COMMAND_VALIDATOR = EntryValidator.builder() .addEntryData(new TriggerEntryData("trigger", null, true)) .addEntryData(new SubCommandEntryData("subcommand", true, true)) .build(); @@ -60,7 +58,7 @@ public SubCommandEntryData(String key, boolean optional, boolean multiple) { } @Override - public @Nullable ArgumentBuilder getValue(Node node) { + public @Nullable List> getValue(Node node) { assert node instanceof SectionNode; // validate section node structure @@ -82,11 +80,16 @@ public SubCommandEntryData(String key, boolean optional, boolean multiple) { return null; } + // set context for parsing + ParserInstance parser = ParserInstance.get(); + parser.setCurrentEvent("command", CommandEvent.class); + // parse arguments - List> arguments = parseArguments(commandMatcher.group(1)); - if (arguments == null) { + List> arguments = new ArrayList<>(); + CommandElement parsed = CommandCompiler.compile(commandMatcher.group(1), arguments); + if (parsed == null) { return null; - } else if (arguments.isEmpty()) { + } else if (parsed.children.isEmpty()) { if (ParserInstance.get().getData(CommandParsingData.class).arguments.isEmpty()) { Skript.error("A command must have a name."); } else { @@ -96,85 +99,41 @@ public SubCommandEntryData(String key, boolean optional, boolean multiple) { } // parse execution trigger - ParserInstance parser = ParserInstance.get(); - parser.setCurrentEvent("command execution trigger", CommandEvent.class); Trigger execute = entryContainer.getOptional("trigger", Trigger.class, false); boolean hasExecute = execute != null; parser.deleteCurrentEvent(); - List> pieces = new LinkedList<>(); - for (int i = 0; i < arguments.size(); i++) { - ArgumentData argument = arguments.get(i); - - if (argument.isLiteral()) { - pieces.addFirst(Commands.literal(argument.name())); - continue; - } - - ArgumentType nativeType = SkriptBrigadierArgument.ARGUMENT_TYPE_MAPPINGS.get(argument.type().getC()); - if (nativeType == null) { - if (i == arguments.size() - 1) { - nativeType = StringArgumentType.greedyString(); - } else { - nativeType = StringArgumentType.string(); - } - nativeType = new SkriptBrigadierArgument<>(argument, (StringArgumentType) nativeType); - } - pieces.addFirst(Commands.argument(argument.name(), nativeType)); - } - CommandParsingData parsingData = parser.getData(CommandParsingData.class); parsingData.arguments.addLast(arguments); - // setup executor and base - ArgumentBuilder base = pieces.removeFirst(); + // setup executor + List> allArguments = parsingData.arguments.stream() + .flatMap(List::stream) + .toList(); SkriptCommandExecutor executor; - int argCount = 0; if (hasExecute) { - List> allArguments = parsingData.arguments.stream() - .flatMap(List::stream) - .toList(); - argCount = allArguments.size(); - final int finalBaseArgCount = argCount; executor = new SkriptCommandExecutor(execute, allArguments); - base.executes(context -> executor.execute(context, finalBaseArgCount)); } else { executor = null; } // attach subcommand pieces // TODO verify error behavior... + //noinspection unchecked List> subcommands = - entryContainer.getAll("subcommand", ArgumentBuilder.class, false); + entryContainer.getAll("subcommand", List.class, false).stream() + .flatMap(List::stream) + .toList(); if (subcommands.isEmpty() && !hasExecute) { Skript.error("You must have a 'trigger' entry if there are no subcommands!"); return null; } - for (var subcommand : subcommands) { - base.then(subcommand); - } parsingData.arguments.removeLast(); - // attach rest of argument pieces - var iterator = pieces.iterator(); - // wasOptional is so that we place an executes on the last, non-optional argument - boolean wasOptional = hasExecute && !arguments.isEmpty() && arguments.getLast().optional(); - while (iterator.hasNext()) { - argCount--; - base = iterator.next().then(base); - if (hasExecute) { - boolean isBaseOptional = base instanceof RequiredArgumentBuilder requiredArgument && - ((SkriptBrigadierArgument) requiredArgument.getType()).getArgument().optional(); - final int finalArgCount = argCount; - if (isBaseOptional || wasOptional) { - base.executes(context -> executor.execute(context, finalArgCount)); - wasOptional = isBaseOptional; - } - } - iterator.remove(); - } - - return base; + //noinspection rawtypes, unchecked + return (List) parsed.children.stream() + .map(child -> parse(child, executor, subcommands)) + .toList(); } @Override @@ -190,109 +149,48 @@ public boolean canCreateWith(Node node) { return key.regionMatches(true, 0, prefix, 0, prefix.length()); } - /* - * Argument Parsing - */ - - /** - * Pattern for parsing arguments in the form: {@code }. - *
- * Where {@code name}, {@code type}, and {@code defaultValue} are groups {@code 1}, {@code 2}, and {@code 3}, respectively. - */ - // TODO optional brackets - private static final Pattern ARGUMENT_PATTERN = - Pattern.compile("<\\s*(?:([^>]+?)\\s*:\\s*)?(.+?)\\s*(?:=\\s*(" + SkriptParser.WILDCARD + "))?\\s*>"); - - private static @Nullable List> parseArguments(@Nullable String input) { - if (input == null) { - return List.of(); - } - input = input.trim(); - if (input.isEmpty()) { - return List.of(); - } - input = input.toLowerCase(); - List> arguments = new ArrayList<>(); - Matcher argumentMatcher = ARGUMENT_PATTERN.matcher(input); - boolean optional = false; - int lastEnd = 0; - while (argumentMatcher.find()) { - String between = input.substring(lastEnd, argumentMatcher.start()).trim(); - if (!between.isEmpty()) { // there is a literal argument here - arguments.add(new ArgumentData<>(between, false, null, null, optional)); - } - lastEnd = argumentMatcher.end(); - - // first, parse the type - String rawType = argumentMatcher.group(2); - 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; - } + private static ArgumentBuilder parse(CommandElement commandElement, + @Nullable SkriptCommandExecutor executor, Collection> subcommands) { + Collection children = commandElement.children; - // 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; - // TODO this needs to consider preceding arguments from parser data - if (arguments.stream().anyMatch(data -> - data instanceof ArgumentData argumentData && argumentData.name().equals(finalName))) { // already taken - // try to generate a name like 'number2', 'number3', etc - int i = 2; - baseLoop: while (true) { - for (var argument : arguments) { - if (argument instanceof ArgumentData argumentData && argumentData.name().equals(name + i)) { - i++; - continue baseLoop; - } - } - break; - } - name += i; - } - } - - // 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); + ArgumentBuilder argument; + if (commandElement instanceof LiteralCommandElement literalCommandElement) { + argument = Commands.literal(literalCommandElement.literal()); + } else { // ArgumentCommandElement + ArgumentData data = ((ArgumentCommandElement) commandElement).argument(); + ArgumentType nativeType = SkriptBrigadierArgument.ARGUMENT_TYPE_MAPPINGS.get(data.type().getC()); + if (nativeType == null) { + if (children.isEmpty() && subcommands.isEmpty()) { + nativeType = StringArgumentType.greedyString(); } 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 { - logHandler.printLog(); - } + nativeType = StringArgumentType.string(); } + nativeType = new SkriptBrigadierArgument<>(data, (StringArgumentType) nativeType); } - optional |= defaultValue != null; - - //noinspection unchecked, rawtypes - arguments.add(new ArgumentData(name, isAutomaticName, type, defaultValue, optional)); + argument = Commands.argument(data.name(), nativeType); } - if (arguments.isEmpty()) { // it is just a singular literal argument then - arguments.add(new ArgumentData<>(input, false, null, null, false)); + for (CommandElement element : children) { + if (element == null) { + continue; + } + argument.then(parse(element, executor, 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 + // TODO there is actually more complexity here to handle + // 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 (children.isEmpty() || children.stream().anyMatch(Objects::isNull)) { + for (var subcommand : subcommands) { + argument.then(subcommand); + } + if (executor != null) { + argument.executes(executor::execute); + } } - return arguments; + return argument; } } From 86be3f2f87a1492ca1254882aa2cb90521307ee4 Mon Sep 17 00:00:00 2001 From: Patrick Miller Date: Wed, 24 Jun 2026 13:07:41 -0400 Subject: [PATCH 08/41] Organize and cleanup --- .../structures/util/ArgumentData.java | 10 +- .../structures/util/CommandCompiler.java | 224 +++++++++++++----- .../util/SkriptBrigadierArgument.java | 22 +- .../util/SkriptCommandExecutor.java | 7 +- .../structures/util/SubCommandEntryData.java | 9 +- 5 files changed, 206 insertions(+), 66 deletions(-) diff --git a/src/main/java/org/skriptlang/skript/bukkit/command/elements/structures/util/ArgumentData.java b/src/main/java/org/skriptlang/skript/bukkit/command/elements/structures/util/ArgumentData.java index 302d9a5d6f6..8e5eb0e169a 100644 --- a/src/main/java/org/skriptlang/skript/bukkit/command/elements/structures/util/ArgumentData.java +++ b/src/main/java/org/skriptlang/skript/bukkit/command/elements/structures/util/ArgumentData.java @@ -4,7 +4,15 @@ import ch.njol.skript.lang.Expression; import org.jetbrains.annotations.Nullable; -public record ArgumentData( +/** + * 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. + */ +record ArgumentData( String name, boolean isAutomaticName, ClassInfo type, 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 index cd72982a7dd..e3c0ebdda76 100644 --- 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 @@ -12,114 +12,206 @@ 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; -public class CommandCompiler { +/** + * Compiles a string command definition into a tree of literals ({@link LiteralCommandElement}) + * and arguments {@link ArgumentCommandElement}. + * @see #compile(String, List) + */ +final class CommandCompiler { - public static class CommandElement { + /* + * Tree Structures + */ - public LinkedHashSet children = new LinkedHashSet<>(); + /** + * A node within a command tree. + */ + static class CommandElement { - public void append(Collection elements) { - // Element validation - elements = elements.stream() - .flatMap(element -> { - if (element instanceof ChoiceCommandElement choice) { - return choice.children.stream(); - } - if (element == this) { - return Stream.empty(); - } - return Stream.of(element); - }) - .toList(); + protected final Set children; - if (children.isEmpty()) { // we reached the end - children.addAll(elements); - return; - } + 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); + } - boolean hasNull = false; + /** + * 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) { - hasNull = true; + if (child == null) { // null child indicates that this element is a command edge + addToElement = true; } else { child.append(elements); } } - if (hasNull) { + if (addToElement) { children.remove(null); - children.addAll(elements); + 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); + } + } } } } - public static class LiteralCommandElement extends CommandElement { + /** + * Represents a literal (constant) argument of a command. + */ + static class LiteralCommandElement extends CommandElement { private final String string; - public LiteralCommandElement(String string) { + private LiteralCommandElement(String string) { + super(); this.string = string; } + /** + * @return Literal argument this element represents. + */ public String literal() { return string; } } - public static class ArgumentCommandElement extends CommandElement { + /** + * Represents a dynamic argument of a command. + */ + static class ArgumentCommandElement extends CommandElement { private final ArgumentData argument; - public ArgumentCommandElement(ArgumentData argument) { + private ArgumentCommandElement(ArgumentData argument) { + super(); this.argument = argument; } + /** + * @return Data describing the argument this element represents. + */ public ArgumentData argument() { return argument; } } - public static class ChoiceCommandElement extends CommandElement { + /** + * 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 == null) { + if (last == EMPTY) { children.removeLast(); children.addAll(elements); } else { - children.getLast().append(elements); + last.append(elements); } } - } - - private static StringBuilder tryLiteralAppend(CommandElement first, StringBuilder literalBuilder) { - if (literalBuilder.isEmpty()) { - return literalBuilder; + 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); } - String literal = literalBuilder.toString().trim(); - if (!literal.isEmpty()) { - first.append(List.of(new LiteralCommandElement(literal))); + + @Override + public Collection children() { + LinkedHashSet children = (LinkedHashSet) this.children; + if (children.getLast() == EMPTY) { + children.removeLast(); + children.add(null); + } + return super.children(); } - return new StringBuilder(); + } - public static CommandElement compile(String pattern, List> arguments) { + /* + * Compilation + */ + + /** + * Compiles a string command definition into an element tree. + * @param pattern The command definition to compile. + * @param arguments A list to store argument information in. + * @return A plain {@link CommandElement} containing all children. + * For a regular input, such as {@code "heal "}, this element contains a single {@link LiteralCommandElement}. + */ + public static CommandElement compile(final String pattern, List> arguments) { StringBuilder literalBuilder = new StringBuilder(); CommandElement first = new CommandElement(); - for (int i = 0; i < pattern.length(); i++) { + int patternLength = pattern.length(); + for (int i = 0; i < patternLength; i++) { char c = pattern.charAt(i); - if (c == '[') { + if (c == '[') { // indicates an optional element literalBuilder = tryLiteralAppend(first, literalBuilder); int end = SkriptParser.nextBracket(pattern, ']', c, i + 1, true); @@ -128,12 +220,12 @@ public static CommandElement compile(String pattern, List> argum return null; } - List toAppend = new ArrayList<>(commandElement.children); + List toAppend = new ArrayList<>(commandElement.children()); toAppend.add(null); first.append(toAppend); i = end; - } else if (c == '(') { + } else if (c == '(') { // indicates an optional element literalBuilder = tryLiteralAppend(first, literalBuilder); int end = SkriptParser.nextBracket(pattern, ')', c, i + 1, true); @@ -142,22 +234,25 @@ public static CommandElement compile(String pattern, List> argum return null; } - first.append(commandElement.children); + first.append(commandElement.children()); i = end; - } else if (c == '|') { + } else if (c == '|') { // indicates the end of a single choice literalBuilder = tryLiteralAppend(first, literalBuilder); ChoiceCommandElement choiceElement; if (first instanceof ChoiceCommandElement choiceCommandElement) { choiceElement = choiceCommandElement; - } else { + } 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.children.addAll(first.children); + choiceElement.append(first.children()); first = choiceElement; } - choiceElement.children.add(null); - } else if (c == '<') { + // append an empty space for the following content to append to + choiceElement.appendEmpty(); + } else if (c == '<') { // literalBuilder = tryLiteralAppend(first, literalBuilder); int end = SkriptParser.nextBracket(pattern, '>', c, i + 1, true); @@ -170,7 +265,7 @@ public static CommandElement compile(String pattern, List> argum first.append(List.of(new ArgumentCommandElement(argument))); i = end; - } else if (c == '\\') { + } else if (c == '\\' && i + 1 < patternLength) { i++; literalBuilder.append(pattern.charAt(i)); } else { @@ -183,6 +278,27 @@ public static CommandElement compile(String pattern, List> argum return first; } + /** + * Helper for appending literal content to an element during compilation. + * @param first The element to append to. + * @param literalBuilder Builder representing the literal to append. + * @return A new builder if appending was successful, otherwise {@code literalBuilder}. + */ + private static StringBuilder tryLiteralAppend(CommandElement first, StringBuilder literalBuilder) { + if (literalBuilder.isEmpty()) { + return literalBuilder; + } + String literal = literalBuilder.toString().trim(); + if (!literal.isEmpty()) { // blank literals are not meaningful to append + first.append(List.of(new LiteralCommandElement(literal))); + } + return new StringBuilder(); + } + + /* + * Argument Parsing + */ + private static final Pattern ARGUMENT_PATTERN = Pattern.compile("^\\s*(?:([^>]+?)\\s*:\\s*)?(.+?)\\s*(?:=\\s*(" + SkriptParser.WILDCARD + "))?\\s*$"); diff --git a/src/main/java/org/skriptlang/skript/bukkit/command/elements/structures/util/SkriptBrigadierArgument.java b/src/main/java/org/skriptlang/skript/bukkit/command/elements/structures/util/SkriptBrigadierArgument.java index e6a5aeefd4f..3e1286630f4 100644 --- a/src/main/java/org/skriptlang/skript/bukkit/command/elements/structures/util/SkriptBrigadierArgument.java +++ b/src/main/java/org/skriptlang/skript/bukkit/command/elements/structures/util/SkriptBrigadierArgument.java @@ -1,7 +1,6 @@ package org.skriptlang.skript.bukkit.command.elements.structures.util; import ch.njol.skript.lang.ParseContext; -import ch.njol.skript.lang.util.ContextlessEvent; import ch.njol.skript.registrations.Classes; import com.mojang.brigadier.LiteralMessage; import com.mojang.brigadier.arguments.ArgumentType; @@ -25,8 +24,16 @@ import java.util.concurrent.CompletableFuture; import java.util.function.Supplier; -public class SkriptBrigadierArgument implements CustomArgumentType.Converted { +/** + * 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. + */ +class SkriptBrigadierArgument implements CustomArgumentType.Converted { + /** + * Pre-defined mappings of types that are acceptable to map to other native argument types. + */ public static final Map, ArgumentType> ARGUMENT_TYPE_MAPPINGS = Map.ofEntries( Map.entry(Boolean.class, BoolArgumentType.bool()), Map.entry(Long.class, LongArgumentType.longArg()), @@ -37,7 +44,12 @@ public class SkriptBrigadierArgument implements CustomArgumentType.Converted< private static final Dynamic2CommandExceptionType ERROR_INVALID_INPUT = new Dynamic2CommandExceptionType( (input, type) -> new LiteralMessage("'%s' is not a valid %s.".formatted(input, type))); - static final Object DEFAULT_PLACEHOLDER = new Object(); + /** + * Placeholder result to be used for indicating that the default value of an argument should be resolved. + * Resolution of default values is only possible during general execution (after this argument is evaluated), + * as they may depend on the context of the command execution. + */ + static final Object DEFAULT_VALUE_PLACEHOLDER = new Object(); private final ArgumentData argument; private final StringArgumentType nativeType; @@ -59,7 +71,7 @@ public ArgumentData getArgument() { if (result == null) { if (argument.defaultValue() != null) { // attempt default value //noinspection unchecked - result = (T) DEFAULT_PLACEHOLDER; + result = (T) DEFAULT_VALUE_PLACEHOLDER; } if (result == null) { throw ERROR_INVALID_INPUT.create(input, argument.type().getName().getSingular()); @@ -80,6 +92,8 @@ public ArgumentData getArgument() { return Suggestions.empty(); } + // treat as a valid match for + // treat <"foo b> as a valid match for supplier.get().forEachRemaining(value -> { String name = Classes.toString(value).toLowerCase(Locale.ENGLISH) .replace(' ', '_'); diff --git a/src/main/java/org/skriptlang/skript/bukkit/command/elements/structures/util/SkriptCommandExecutor.java b/src/main/java/org/skriptlang/skript/bukkit/command/elements/structures/util/SkriptCommandExecutor.java index a14551ef7bc..eb32e54dac7 100644 --- a/src/main/java/org/skriptlang/skript/bukkit/command/elements/structures/util/SkriptCommandExecutor.java +++ b/src/main/java/org/skriptlang/skript/bukkit/command/elements/structures/util/SkriptCommandExecutor.java @@ -14,7 +14,10 @@ import java.util.Set; import java.util.stream.Collectors; -public class SkriptCommandExecutor { +/** + * An executor for Brigadier commands. + */ +class SkriptCommandExecutor { private final Trigger trigger; private final List> arguments; @@ -40,7 +43,7 @@ public int execute(CommandContext context) throws CommandSyn if (value instanceof SequencedCollection collection) { value = collection.getFirst(); } - } else if (value == SkriptBrigadierArgument.DEFAULT_PLACEHOLDER) { + } else if (value == SkriptBrigadierArgument.DEFAULT_VALUE_PLACEHOLDER) { assert argument.defaultValue() != null; value = argument.defaultValue().getSingle(commandEvent); } 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 index 422b4091eb6..0b42bb4049f 100644 --- 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 @@ -25,7 +25,6 @@ import java.util.Collection; import java.util.LinkedList; import java.util.List; -import java.util.Objects; import java.util.regex.Matcher; import java.util.regex.Pattern; @@ -89,7 +88,7 @@ public SubCommandEntryData(String key, boolean optional, boolean multiple) { CommandElement parsed = CommandCompiler.compile(commandMatcher.group(1), arguments); if (parsed == null) { return null; - } else if (parsed.children.isEmpty()) { + } else if (parsed.isLeaf()) { if (ParserInstance.get().getData(CommandParsingData.class).arguments.isEmpty()) { Skript.error("A command must have a name."); } else { @@ -131,7 +130,7 @@ public SubCommandEntryData(String key, boolean optional, boolean multiple) { parsingData.arguments.removeLast(); //noinspection rawtypes, unchecked - return (List) parsed.children.stream() + return (List) parsed.children().stream() .map(child -> parse(child, executor, subcommands)) .toList(); } @@ -151,7 +150,7 @@ public boolean canCreateWith(Node node) { private static ArgumentBuilder parse(CommandElement commandElement, @Nullable SkriptCommandExecutor executor, Collection> subcommands) { - Collection children = commandElement.children; + Collection children = commandElement.children(); ArgumentBuilder argument; if (commandElement instanceof LiteralCommandElement literalCommandElement) { @@ -181,7 +180,7 @@ public boolean canCreateWith(Node node) { // TODO there is actually more complexity here to handle // 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 (children.isEmpty() || children.stream().anyMatch(Objects::isNull)) { + if (commandElement.isLeaf()) { for (var subcommand : subcommands) { argument.then(subcommand); } From e113c0fb17780d1c8ae29fc736b373759225a309 Mon Sep 17 00:00:00 2001 From: Patrick Miller Date: Wed, 24 Jun 2026 17:44:45 -0400 Subject: [PATCH 09/41] Command parser 9000v2 --- .../structures/util/CommandCompiler.java | 114 ++++++++++++------ 1 file changed, 75 insertions(+), 39 deletions(-) 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 index e3c0ebdda76..95c574b6b03 100644 --- 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 @@ -67,7 +67,7 @@ public boolean isLeaf() { * {@code elements} will also be appended to that element. * @param elements Elements to append. */ - public void append(Collection elements) { + 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 @@ -158,7 +158,7 @@ private ChoiceCommandElement() { } @Override - public void append(Collection elements) { + public void append(Collection elements) { if (elements.isEmpty()) { return; } @@ -205,40 +205,46 @@ public Collection children() { * For a regular input, such as {@code "heal "}, this element contains a single {@link LiteralCommandElement}. */ public static CommandElement compile(final String pattern, List> arguments) { - StringBuilder literalBuilder = new StringBuilder(); + 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 == '[') { // indicates an optional element - literalBuilder = tryLiteralAppend(first, literalBuilder); - - int end = SkriptParser.nextBracket(pattern, ']', c, i + 1, true); + if (c == '[' || c == '(') { + boolean isOptional = c == '['; + int end = SkriptParser.nextBracket(pattern, isOptional ? ']' : ')', c, i + 1, true); CommandElement commandElement = compile(pattern.substring(i + 1, end), arguments); if (commandElement == null) { return null; } - List toAppend = new ArrayList<>(commandElement.children()); - toAppend.add(null); - first.append(toAppend); - - i = end; - } else if (c == '(') { // indicates an optional element - literalBuilder = tryLiteralAppend(first, literalBuilder); - - int end = SkriptParser.nextBracket(pattern, ')', c, i + 1, true); - CommandElement commandElement = compile(pattern.substring(i + 1, end), 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(); } - first.append(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' + return null; + } + } else { + //noinspection rawtypes, unchecked + pendingLiterals = appendToLiterals(pendingLiterals, (Collection) toAppend); + } i = end; } else if (c == '|') { // indicates the end of a single choice - literalBuilder = tryLiteralAppend(first, literalBuilder); + if (!pendingLiterals.isEmpty()) { + first.append(pendingLiterals); + pendingLiterals.clear(); + } ChoiceCommandElement choiceElement; if (first instanceof ChoiceCommandElement choiceCommandElement) { @@ -252,8 +258,10 @@ public static CommandElement compile(final String pattern, List> } // append an empty space for the following content to append to choiceElement.appendEmpty(); - } else if (c == '<') { // - literalBuilder = tryLiteralAppend(first, literalBuilder); + } else if (c == '<') { // indicates an argument + if (!pendingLiterals.isEmpty()) { // an argument cannot be legally placed here (ex. 'lit') + return null; + } int end = SkriptParser.nextBracket(pattern, '>', c, i + 1, true); ArgumentData argument = parseArgument(pattern.substring(i + 1, end)); @@ -265,34 +273,63 @@ public static CommandElement compile(final String pattern, List> first.append(List.of(new ArgumentCommandElement(argument))); i = end; - } else if (c == '\\' && i + 1 < patternLength) { + } else if (c == '\\' && i + 1 < patternLength) { // escaping i++; - literalBuilder.append(pattern.charAt(i)); + appendToLiterals(pendingLiterals, pattern.charAt(i)); + } else if (c == ' ') { // literal terminator + if (!pendingLiterals.isEmpty()) { + first.append(pendingLiterals); + pendingLiterals.clear(); + } } else { - literalBuilder.append(c); + appendToLiterals(pendingLiterals, c); } } - tryLiteralAppend(first, literalBuilder); + if (!pendingLiterals.isEmpty()) { // append any outstanding literal(s) + first.append(pendingLiterals); + } return first; } /** - * Helper for appending literal content to an element during compilation. - * @param first The element to append to. - * @param literalBuilder Builder representing the literal to append. - * @return A new builder if appending was successful, otherwise {@code literalBuilder}. + * Appends {@code character} to all elements of {@code literals}. + * @param literals The literals to append to. + * @param character The character to append. */ - private static StringBuilder tryLiteralAppend(CommandElement first, StringBuilder literalBuilder) { - if (literalBuilder.isEmpty()) { - return literalBuilder; + private static void appendToLiterals(List literals, char character) { + if (literals.isEmpty()) { + literals.add(new LiteralCommandElement(String.valueOf(character))); + return; } - String literal = literalBuilder.toString().trim(); - if (!literal.isEmpty()) { // blank literals are not meaningful to append - first.append(List.of(new LiteralCommandElement(literal))); + 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) { + 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 new StringBuilder(); + return newLiterals; } /* @@ -326,7 +363,6 @@ private static StringBuilder tryLiteralAppend(CommandElement first, StringBuilde if (name == null) { // user did not specify, manually create one isAutomaticName = true; name = type.getName().getSingular(); - String finalName = name; // TODO verify not duplicate argument name } From 44322a0f289a005b4847251d1f6acb6e7cb04c93 Mon Sep 17 00:00:00 2001 From: Patrick Miller Date: Thu, 25 Jun 2026 12:39:52 -0400 Subject: [PATCH 10/41] Simple argument range support --- .../structures/util/ArgumentData.java | 4 ++- .../structures/util/CommandCompiler.java | 26 ++++++++++++++++-- .../util/SkriptBrigadierArgument.java | 27 +++++++++++++++---- .../structures/util/SubCommandEntryData.java | 7 +++-- 4 files changed, 54 insertions(+), 10 deletions(-) diff --git a/src/main/java/org/skriptlang/skript/bukkit/command/elements/structures/util/ArgumentData.java b/src/main/java/org/skriptlang/skript/bukkit/command/elements/structures/util/ArgumentData.java index 8e5eb0e169a..52c54bff42a 100644 --- a/src/main/java/org/skriptlang/skript/bukkit/command/elements/structures/util/ArgumentData.java +++ b/src/main/java/org/skriptlang/skript/bukkit/command/elements/structures/util/ArgumentData.java @@ -16,5 +16,7 @@ record ArgumentData( String name, boolean isAutomaticName, ClassInfo type, - @Nullable Expression defaultValue + @Nullable Expression defaultValue, + @Nullable T min, + @Nullable T max ) { } 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 index 95c574b6b03..d683f2dec86 100644 --- 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 @@ -339,6 +339,9 @@ private static List appendToLiterals(Collection]+?)\\s*:\\s*)?(.+?)\\s*(?:=\\s*(" + SkriptParser.WILDCARD + "))?\\s*$"); + private static final Pattern TYPE_PATTERN = + Pattern.compile("^(.+?)\\s*(?: from (.+?)(?: to (.+?))?)?$"); + private static @Nullable ArgumentData parseArgument(String argument) { Matcher argumentMatcher = ARGUMENT_PATTERN.matcher(argument); if (!argumentMatcher.find()) { @@ -346,7 +349,12 @@ private static List appendToLiterals(Collection type = Classes.getClassInfoFromUserInput(plural.updated()); if (type == null) { @@ -356,6 +364,20 @@ private static List appendToLiterals(Collection appendToLiterals(Collection implements CustomArgumentType.Converted, ArgumentType> ARGUMENT_TYPE_MAPPINGS = Map.ofEntries( - Map.entry(Boolean.class, BoolArgumentType.bool()), - Map.entry(Long.class, LongArgumentType.longArg()), - Map.entry(Number.class, DoubleArgumentType.doubleArg()), - Map.entry(Player.class, ArgumentTypes.player()) + public static final Map, Function, ArgumentType>> ARGUMENT_TYPE_MAPPINGS = Map.of( + Boolean.class, ignored -> BoolArgumentType.bool(), + Long.class, data -> { + if (data.max() == null) { + if (data.min() == null) { + return LongArgumentType.longArg(); + } + return LongArgumentType.longArg((long) data.min()); + } + return LongArgumentType.longArg((long) data.min(), (long) data.max()); + }, + Number.class, data -> { + if (data.max() == null) { + if (data.min() == null) { + return DoubleArgumentType.doubleArg(); + } + return DoubleArgumentType.doubleArg((double) data.min()); + } + return DoubleArgumentType.doubleArg((double) data.min(), (double) data.max()); + }, + Player.class, ignored -> ArgumentTypes.player() ); private static final Dynamic2CommandExceptionType ERROR_INVALID_INPUT = new Dynamic2CommandExceptionType( 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 index 0b42bb4049f..a5e6d124308 100644 --- 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 @@ -157,14 +157,17 @@ public boolean canCreateWith(Node node) { argument = Commands.literal(literalCommandElement.literal()); } else { // ArgumentCommandElement ArgumentData data = ((ArgumentCommandElement) commandElement).argument(); - ArgumentType nativeType = SkriptBrigadierArgument.ARGUMENT_TYPE_MAPPINGS.get(data.type().getC()); - if (nativeType == null) { + ArgumentType nativeType; + var nativeMapping = SkriptBrigadierArgument.ARGUMENT_TYPE_MAPPINGS.get(data.type().getC()); + if (nativeMapping == null) { if (children.isEmpty() && subcommands.isEmpty()) { nativeType = StringArgumentType.greedyString(); } else { nativeType = StringArgumentType.string(); } nativeType = new SkriptBrigadierArgument<>(data, (StringArgumentType) nativeType); + } else { + nativeType = nativeMapping.apply(data); } argument = Commands.argument(data.name(), nativeType); } From 87b3c987e604eba0347c9efb55e473b7476f562c Mon Sep 17 00:00:00 2001 From: Patrick Miller Date: Sun, 28 Jun 2026 23:39:17 -0400 Subject: [PATCH 11/41] Reimplement automatic argument naming --- .../structures/util/CommandCompiler.java | 54 +++++++++++++++---- .../structures/util/SubCommandEntryData.java | 20 ++++--- .../command}/StructCommand.sk | 51 ++++++++++-------- 3 files changed, 87 insertions(+), 38 deletions(-) rename src/test/skript/tests/{syntaxes/structures => bukkit/command}/StructCommand.sk (84%) 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 index d683f2dec86..c22bd5fee42 100644 --- 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 @@ -18,6 +18,7 @@ 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}) @@ -197,24 +198,38 @@ public Collection 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 arguments A list to store argument information in. - * @return A plain {@link CommandElement} containing all children. - * For a regular input, such as {@code "heal "}, this element contains a single {@link LiteralCommandElement}. + * @param existingArguments Existing arguments to consider during compilation. + * @return A result of the compilation, or null if compilation failed. */ - public static CommandElement compile(final String pattern, List> arguments) { + 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 == '(') { + 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), arguments); + CommandElement commandElement = compile(pattern.substring(i + 1, end), existingArguments, arguments); if (commandElement == null) { return null; } @@ -264,7 +279,8 @@ public static CommandElement compile(final String pattern, List> } int end = SkriptParser.nextBracket(pattern, '>', c, i + 1, true); - ArgumentData argument = parseArgument(pattern.substring(i + 1, end)); + ArgumentData argument = parseArgument(pattern.substring(i + 1, end), + Stream.concat(existingArguments.stream(), arguments.stream()).toList()); if (argument == null) { return null; } @@ -342,7 +358,7 @@ private static List appendToLiterals(Collection parseArgument(String argument) { + private static @Nullable ArgumentData parseArgument(String argument, List> arguments) { Matcher argumentMatcher = ARGUMENT_PATTERN.matcher(argument); if (!argumentMatcher.find()) { return null; @@ -385,7 +401,27 @@ private static List appendToLiterals(Collection 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 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 index a5e6d124308..55d2d41d360 100644 --- 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 @@ -15,6 +15,7 @@ import org.jetbrains.annotations.Nullable; 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.lang.entry.EntryContainer; import org.skriptlang.skript.lang.entry.EntryData; @@ -84,12 +85,17 @@ public SubCommandEntryData(String key, boolean optional, boolean multiple) { parser.setCurrentEvent("command", CommandEvent.class); // parse arguments + CommandParsingData parsingData = parser.getData(CommandParsingData.class); + List> arguments = new ArrayList<>(); - CommandElement parsed = CommandCompiler.compile(commandMatcher.group(1), arguments); - if (parsed == null) { + CompilationResult compilationResult = CommandCompiler.compile(commandMatcher.group(1), + parsingData.arguments.stream() + .flatMap(List::stream) + .toList()); + if (compilationResult == null) { // failed for a reason reported by the compiler return null; - } else if (parsed.isLeaf()) { - if (ParserInstance.get().getData(CommandParsingData.class).arguments.isEmpty()) { + } else if (compilationResult.root().isLeaf()) { + if (parsingData.arguments.isEmpty()) { Skript.error("A command must have a name."); } else { Skript.error("A subcommand must have at least one argument, literal or dynamic."); @@ -102,10 +108,8 @@ public SubCommandEntryData(String key, boolean optional, boolean multiple) { boolean hasExecute = execute != null; parser.deleteCurrentEvent(); - CommandParsingData parsingData = parser.getData(CommandParsingData.class); - parsingData.arguments.addLast(arguments); - // setup executor + parsingData.arguments.addLast(arguments); List> allArguments = parsingData.arguments.stream() .flatMap(List::stream) .toList(); @@ -130,7 +134,7 @@ public SubCommandEntryData(String key, boolean optional, boolean multiple) { parsingData.arguments.removeLast(); //noinspection rawtypes, unchecked - return (List) parsed.children().stream() + return (List) compilationResult.root().children().stream() .map(child -> parse(child, executor, subcommands)) .toList(); } diff --git a/src/test/skript/tests/syntaxes/structures/StructCommand.sk b/src/test/skript/tests/bukkit/command/StructCommand.sk similarity index 84% rename from src/test/skript/tests/syntaxes/structures/StructCommand.sk rename to src/test/skript/tests/bukkit/command/StructCommand.sk index b53600dd4f5..4e5be589040 100644 --- a/src/test/skript/tests/syntaxes/structures/StructCommand.sk +++ b/src/test/skript/tests/bukkit/command/StructCommand.sk @@ -1,6 +1,29 @@ +using local variable type hints + options: command: skriptcommand +parse: + results: {StructCommand::args::1::*} + code: + command /args_test_1 : + trigger: + stop + +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 + command {@command} [] []: trigger: set {_arg1} to arg-1 @@ -21,27 +44,13 @@ 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": +test "StructCommand": + # parsing + assert {StructCommand::args::1::*} is "The argument name 'x' was already used." + # 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 "skriptcommand taco" + execute command "//somecommand burrito is tasty" From a5ec99a63a8dff203aa41724f7cd44126beff483 Mon Sep 17 00:00:00 2001 From: Patrick Miller Date: Mon, 29 Jun 2026 00:07:52 -0400 Subject: [PATCH 12/41] Restore command type hints --- .../structures/util/ArgumentData.java | 1 + .../structures/util/CommandCompiler.java | 2 +- .../structures/util/SubCommandEntryData.java | 38 +++++++++++++++---- .../tests/bukkit/command/StructCommand.sk | 12 +++--- 4 files changed, 38 insertions(+), 15 deletions(-) diff --git a/src/main/java/org/skriptlang/skript/bukkit/command/elements/structures/util/ArgumentData.java b/src/main/java/org/skriptlang/skript/bukkit/command/elements/structures/util/ArgumentData.java index 52c54bff42a..bcfa229a0b4 100644 --- a/src/main/java/org/skriptlang/skript/bukkit/command/elements/structures/util/ArgumentData.java +++ b/src/main/java/org/skriptlang/skript/bukkit/command/elements/structures/util/ArgumentData.java @@ -16,6 +16,7 @@ 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/elements/structures/util/CommandCompiler.java b/src/main/java/org/skriptlang/skript/bukkit/command/elements/structures/util/CommandCompiler.java index c22bd5fee42..a99269b9306 100644 --- 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 @@ -448,7 +448,7 @@ private static List appendToLiterals(Collection> arguments = new ArrayList<>(); CompilationResult compilationResult = CommandCompiler.compile(commandMatcher.group(1), parsingData.arguments.stream() .flatMap(List::stream) @@ -103,16 +103,38 @@ public SubCommandEntryData(String key, boolean optional, boolean multiple) { return null; } + // prepare arguments + parsingData.arguments.addLast(compilationResult.arguments()); + List> allArguments = parsingData.arguments.stream() + .flatMap(List::stream) + .toList(); + // parse execution trigger - Trigger execute = entryContainer.getOptional("trigger", Trigger.class, false); + 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; - parser.deleteCurrentEvent(); // setup executor - parsingData.arguments.addLast(arguments); - List> allArguments = parsingData.arguments.stream() - .flatMap(List::stream) - .toList(); SkriptCommandExecutor executor; if (hasExecute) { executor = new SkriptCommandExecutor(execute, allArguments); diff --git a/src/test/skript/tests/bukkit/command/StructCommand.sk b/src/test/skript/tests/bukkit/command/StructCommand.sk index 4e5be589040..edade6036ea 100644 --- a/src/test/skript/tests/bukkit/command/StructCommand.sk +++ b/src/test/skript/tests/bukkit/command/StructCommand.sk @@ -4,21 +4,21 @@ options: command: skriptcommand parse: - results: {StructCommand::args::1::*} + results: {-StructCommand::args::1::*} code: command /args_test_1 : trigger: stop parse: - results: {StructCommand::hints::1::*} + results: {-StructCommand::hints::1::*} code: command /hint_test_1 : trigger: set {_a} to {_x} in lowercase parse: - results: {StructCommand::hints::2::*} + results: {-StructCommand::hints::2::*} code: command /hint_test_2 : trigger: @@ -46,10 +46,10 @@ command /commandtest : test "StructCommand": # parsing - assert {StructCommand::args::1::*} is "The argument name 'x' was already used." + assert {-StructCommand::args::1::*} is "The argument name 'x' was already used." # 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::*}%)" + 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 "skriptcommand taco" From 4e23a9dc413b1dc8a6a05b086b891561992f07d7 Mon Sep 17 00:00:00 2001 From: Patrick Miller Date: Mon, 29 Jun 2026 00:52:05 -0400 Subject: [PATCH 13/41] Properly handle plural arguments --- .../elements/structures/StructCommand.java | 3 +- .../structures/util/CommandCompiler.java | 2 + .../util/SkriptBrigadierArgument.java | 41 +++++++++++-------- .../util/SkriptCommandExecutor.java | 32 +++++++++++++-- .../structures/util/SubCommandEntryData.java | 13 +++--- .../tests/bukkit/command/StructCommand.sk | 11 ++++- 6 files changed, 75 insertions(+), 27 deletions(-) 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 index f5d2b3699f9..e79bb5b1207 100644 --- 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 @@ -49,8 +49,7 @@ public static void register(SkriptAddon addon, SyntaxRegistry syntaxRegistry) { private static final AtomicBoolean SYNC_COMMANDS = new AtomicBoolean(); private static void performSync() { - if (SYNC_COMMANDS.get()) { - SYNC_COMMANDS.set(false); + if (SYNC_COMMANDS.getAndSet(false)) { Bukkit.reloadData(); } } 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 index a99269b9306..daecfbd13f0 100644 --- 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 @@ -441,6 +441,8 @@ private static List appendToLiterals(Collection The real type of the argument. */ -class SkriptBrigadierArgument implements CustomArgumentType.Converted { +class SkriptBrigadierArgument implements CustomArgumentType.Converted { /** * Pre-defined mappings of types that are acceptable to map to other native argument types. */ public static final Map, Function, ArgumentType>> ARGUMENT_TYPE_MAPPINGS = Map.of( - Boolean.class, ignored -> BoolArgumentType.bool(), + Boolean.class, data -> data.isSingle() ? BoolArgumentType.bool() : null, Long.class, data -> { + if (!data.isSingle()) { + // TODO should error here if min/max is used + return null; + } if (data.max() == null) { if (data.min() == null) { return LongArgumentType.longArg(); @@ -47,6 +53,9 @@ class SkriptBrigadierArgument implements CustomArgumentType.Converted { + if (!data.isSingle()) { + return null; + } if (data.max() == null) { if (data.min() == null) { return DoubleArgumentType.doubleArg(); @@ -55,7 +64,7 @@ class SkriptBrigadierArgument implements CustomArgumentType.Converted ArgumentTypes.player() + Player.class, data -> data.isSingle() ? ArgumentTypes.player() : ArgumentTypes.players() ); private static final Dynamic2CommandExceptionType ERROR_INVALID_INPUT = new Dynamic2CommandExceptionType( @@ -76,25 +85,25 @@ public SkriptBrigadierArgument(@NotNull ArgumentData argument, @NotNull Strin this.nativeType = nativeType; } - public ArgumentData getArgument() { - return argument; - } - @Override - public @NotNull T convert(@NotNull String input) throws CommandSyntaxException { + public @NotNull Object convert(@NotNull String input) throws CommandSyntaxException { assert argument.type().getParser() != null; input = input.replace('_', ' '); - T result = argument.type().getParser().parse(input, ParseContext.COMMAND); + + //noinspection unchecked + Literal 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; + } + if (result == null) { - if (argument.defaultValue() != null) { // attempt default value - //noinspection unchecked - result = (T) DEFAULT_VALUE_PLACEHOLDER; - } - if (result == null) { - throw ERROR_INVALID_INPUT.create(input, argument.type().getName().getSingular()); + if (argument.defaultValue() != null) { // use default value + return DEFAULT_VALUE_PLACEHOLDER; } + throw ERROR_INVALID_INPUT.create(input, argument.type().getName().getSingular()); } - return result; + return argument.isSingle() ? result.getSingle() : result.getArray(); } @Override diff --git a/src/main/java/org/skriptlang/skript/bukkit/command/elements/structures/util/SkriptCommandExecutor.java b/src/main/java/org/skriptlang/skript/bukkit/command/elements/structures/util/SkriptCommandExecutor.java index eb32e54dac7..91c7cb5ef72 100644 --- a/src/main/java/org/skriptlang/skript/bukkit/command/elements/structures/util/SkriptCommandExecutor.java +++ b/src/main/java/org/skriptlang/skript/bukkit/command/elements/structures/util/SkriptCommandExecutor.java @@ -8,6 +8,7 @@ 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.event.Event; import java.util.List; import java.util.SequencedCollection; @@ -19,6 +20,18 @@ */ class SkriptCommandExecutor { + 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; @@ -45,12 +58,25 @@ public int execute(CommandContext context) throws CommandSyn } } else if (value == SkriptBrigadierArgument.DEFAULT_VALUE_PLACEHOLDER) { assert argument.defaultValue() != null; - value = argument.defaultValue().getSingle(commandEvent); + if (argument.isSingle()) { + value = argument.defaultValue().getSingle(commandEvent); + } else { + value = argument.defaultValue().getArray(commandEvent); + } } } else if (argument.defaultValue() != null) { - value = argument.defaultValue().getSingle(commandEvent); + if (argument.isSingle()) { + value = argument.defaultValue().getSingle(commandEvent); + } else { + value = argument.defaultValue().getArray(commandEvent); + } + } + + if (value != null) { + if (!argument.isAutomaticName()) { // store explicitly named arguments as variables + setVariable(argument.name(), value, argument.isSingle(), commandEvent); + } } - Variables.setVariable(argument.name(), value, commandEvent, true); } trigger.execute(commandEvent); 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 index 2ff5b4fadd0..45f4036d8e0 100644 --- 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 @@ -183,18 +183,21 @@ public boolean canCreateWith(Node node) { argument = Commands.literal(literalCommandElement.literal()); } else { // ArgumentCommandElement ArgumentData data = ((ArgumentCommandElement) commandElement).argument(); - ArgumentType nativeType; + + ArgumentType nativeType = null; var nativeMapping = SkriptBrigadierArgument.ARGUMENT_TYPE_MAPPINGS.get(data.type().getC()); - if (nativeMapping == null) { - if (children.isEmpty() && subcommands.isEmpty()) { + if (nativeMapping != null) { // native argument type may be available + nativeType = nativeMapping.apply(data); + } + if (nativeType == null) { + if (children.isEmpty() && subcommands.isEmpty()) { // last argument can be greedy nativeType = StringArgumentType.greedyString(); } else { nativeType = StringArgumentType.string(); } nativeType = new SkriptBrigadierArgument<>(data, (StringArgumentType) nativeType); - } else { - nativeType = nativeMapping.apply(data); } + argument = Commands.argument(data.name(), nativeType); } diff --git a/src/test/skript/tests/bukkit/command/StructCommand.sk b/src/test/skript/tests/bukkit/command/StructCommand.sk index edade6036ea..75a0c4df49f 100644 --- a/src/test/skript/tests/bukkit/command/StructCommand.sk +++ b/src/test/skript/tests/bukkit/command/StructCommand.sk @@ -24,6 +24,10 @@ parse: trigger: set {_a::*} to {_x::*} in lowercase +command /test-args-plural : + trigger: + set {-test-args-plural::*} to {_x::*} + command {@command} [] []: trigger: set {_arg1} to arg-1 @@ -47,10 +51,15 @@ command /commandtest : test "StructCommand": # parsing assert {-StructCommand::args::1::*} is "The argument name 'x' was already used." - # type hints + # 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-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 "skriptcommand taco" execute command "//somecommand burrito is tasty" From 42be4e167ebc8fbbc2746cd281e8eda37d2ddef5 Mon Sep 17 00:00:00 2001 From: Patrick Miller Date: Mon, 29 Jun 2026 10:47:36 -0400 Subject: [PATCH 14/41] Integrate ExprArgument --- .../njol/skript/expressions/ExprArgument.java | 289 ------------------ .../arithmetic/ExprArithmetic.java | 2 +- .../skript/bukkit/command/CommandModule.java | 2 + .../elements/expressions/ExprArgument.java | 289 ++++++++++++++++++ .../structures/util/ArgumentData.java | 2 +- .../structures/util/CommandEvent.java | 4 + .../util/SkriptBrigadierArgument.java | 4 +- .../util/SkriptCommandExecutor.java | 8 +- .../structures/util/SubCommandEntryData.java | 2 +- .../registration/DefaultSyntaxInfos.java | 2 +- 10 files changed, 309 insertions(+), 295 deletions(-) delete mode 100644 src/main/java/ch/njol/skript/expressions/ExprArgument.java create mode 100644 src/main/java/org/skriptlang/skript/bukkit/command/elements/expressions/ExprArgument.java 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/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/org/skriptlang/skript/bukkit/command/CommandModule.java b/src/main/java/org/skriptlang/skript/bukkit/command/CommandModule.java index 12fb68786b9..bf439a91f1c 100644 --- a/src/main/java/org/skriptlang/skript/bukkit/command/CommandModule.java +++ b/src/main/java/org/skriptlang/skript/bukkit/command/CommandModule.java @@ -3,6 +3,7 @@ import org.skriptlang.skript.addon.AddonModule; import org.skriptlang.skript.addon.HierarchicalAddonModule; import org.skriptlang.skript.addon.SkriptAddon; +import org.skriptlang.skript.bukkit.command.elements.expressions.ExprArgument; import org.skriptlang.skript.bukkit.command.elements.structures.StructCommand; public class CommandModule extends HierarchicalAddonModule { @@ -14,6 +15,7 @@ public CommandModule(AddonModule parentModule) { @Override protected void loadSelf(SkriptAddon addon) { register(addon, + ExprArgument::register, syntaxRegistry -> StructCommand.register(addon, syntaxRegistry) ); } 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..c4464ad0d0c --- /dev/null +++ b/src/main/java/org/skriptlang/skript/bukkit/command/elements/expressions/ExprArgument.java @@ -0,0 +1,289 @@ +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.elements.structures.util.ArgumentData; +import org.skriptlang.skript.bukkit.command.elements.structures.util.CommandEvent; +import org.skriptlang.skript.bukkit.command.elements.structures.util.SubCommandEntryData.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 + + private @Nullable ArgumentData argument; + + private boolean couldCauseArithmeticConfusion = false; + + @Override + public boolean init(Expression[] exprs, int matchedPattern, Kleenean isDelayed, ParseResult parseResult) { + boolean scriptCommand = getParser().isCurrentEvent(CommandEvent.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 = ParserInstance.get().getData(CommandParsingData.class).arguments.stream() + .flatMap(List::stream) + .toList(); + 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() { + return CollectionUtils.array(CommandEvent.class, PlayerCommandPreprocessEvent.class, ServerCommandEvent.class); + } + + @Override + protected Object @Nullable [] get(Event event) { + if (argument != null) { + if (argument.isSingle()) { + Object[] result = (Object[]) Array.newInstance(argument.type().getC(), 1); + result[0] = ((CommandEvent) event).arguments.get(argument); + return result; + } else { + return (Object[]) ((CommandEvent) event).arguments.get(argument); + } + } + + 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/structures/util/ArgumentData.java b/src/main/java/org/skriptlang/skript/bukkit/command/elements/structures/util/ArgumentData.java index bcfa229a0b4..12db8e78d30 100644 --- a/src/main/java/org/skriptlang/skript/bukkit/command/elements/structures/util/ArgumentData.java +++ b/src/main/java/org/skriptlang/skript/bukkit/command/elements/structures/util/ArgumentData.java @@ -12,7 +12,7 @@ * @param defaultValue The default value of the argument, if specified. * @param The type of the argument. */ -record ArgumentData( +public record ArgumentData( String name, boolean isAutomaticName, ClassInfo type, diff --git a/src/main/java/org/skriptlang/skript/bukkit/command/elements/structures/util/CommandEvent.java b/src/main/java/org/skriptlang/skript/bukkit/command/elements/structures/util/CommandEvent.java index fcf6db8194d..b3a836681ce 100644 --- a/src/main/java/org/skriptlang/skript/bukkit/command/elements/structures/util/CommandEvent.java +++ b/src/main/java/org/skriptlang/skript/bukkit/command/elements/structures/util/CommandEvent.java @@ -5,9 +5,13 @@ import org.bukkit.event.HandlerList; import org.jetbrains.annotations.NotNull; +import java.util.HashMap; +import java.util.Map; + public class CommandEvent extends Event { public final CommandSender sender; + public final Map, Object> arguments = new HashMap<>(); public CommandEvent(CommandSender sender) { this.sender = sender; diff --git a/src/main/java/org/skriptlang/skript/bukkit/command/elements/structures/util/SkriptBrigadierArgument.java b/src/main/java/org/skriptlang/skript/bukkit/command/elements/structures/util/SkriptBrigadierArgument.java index 2aa7dc91a14..c699f5d1dd7 100644 --- a/src/main/java/org/skriptlang/skript/bukkit/command/elements/structures/util/SkriptBrigadierArgument.java +++ b/src/main/java/org/skriptlang/skript/bukkit/command/elements/structures/util/SkriptBrigadierArgument.java @@ -17,6 +17,7 @@ import com.mojang.brigadier.suggestion.SuggestionsBuilder; import io.papermc.paper.command.brigadier.argument.ArgumentTypes; import io.papermc.paper.command.brigadier.argument.CustomArgumentType; +import org.bukkit.entity.Entity; import org.bukkit.entity.Player; import org.jetbrains.annotations.NotNull; @@ -64,7 +65,8 @@ class SkriptBrigadierArgument implements CustomArgumentType.Converted data.isSingle() ? ArgumentTypes.player() : ArgumentTypes.players() + Player.class, data -> data.isSingle() ? ArgumentTypes.player() : ArgumentTypes.players(), + Entity.class, data -> data.isSingle() ? ArgumentTypes.entity() : ArgumentTypes.entities() ); private static final Dynamic2CommandExceptionType ERROR_INVALID_INPUT = new Dynamic2CommandExceptionType( diff --git a/src/main/java/org/skriptlang/skript/bukkit/command/elements/structures/util/SkriptCommandExecutor.java b/src/main/java/org/skriptlang/skript/bukkit/command/elements/structures/util/SkriptCommandExecutor.java index 91c7cb5ef72..bb0f2125275 100644 --- a/src/main/java/org/skriptlang/skript/bukkit/command/elements/structures/util/SkriptCommandExecutor.java +++ b/src/main/java/org/skriptlang/skript/bukkit/command/elements/structures/util/SkriptCommandExecutor.java @@ -10,6 +10,7 @@ import io.papermc.paper.command.brigadier.argument.resolvers.ArgumentResolver; import org.bukkit.event.Event; +import java.lang.reflect.Array; import java.util.List; import java.util.SequencedCollection; import java.util.Set; @@ -54,7 +55,11 @@ public int execute(CommandContext context) throws CommandSyn if (value instanceof ArgumentResolver argumentResolver) { // native type needs resolved value = argumentResolver.resolve(context.getSource()); if (value instanceof SequencedCollection collection) { - value = collection.getFirst(); + if (argument.isSingle()) { + value = collection.getFirst(); + } else { + value = collection.toArray((Object[]) Array.newInstance(argument.type().getC(), collection.size())); + } } } else if (value == SkriptBrigadierArgument.DEFAULT_VALUE_PLACEHOLDER) { assert argument.defaultValue() != null; @@ -73,6 +78,7 @@ public int execute(CommandContext context) throws CommandSyn } if (value != null) { + commandEvent.arguments.put(argument, value); if (!argument.isAutomaticName()) { // store explicitly named arguments as variables setVariable(argument.name(), value, argument.isSingle(), commandEvent); } 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 index 45f4036d8e0..762ee686d66 100644 --- 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 @@ -32,7 +32,7 @@ public class SubCommandEntryData extends EntryData>> { - private static final class CommandParsingData extends Data { + public static final class CommandParsingData extends Data { public List>> arguments = new LinkedList<>(); diff --git a/src/main/java/org/skriptlang/skript/registration/DefaultSyntaxInfos.java b/src/main/java/org/skriptlang/skript/registration/DefaultSyntaxInfos.java index 0c09886c241..bc8491e06a9 100644 --- a/src/main/java/org/skriptlang/skript/registration/DefaultSyntaxInfos.java +++ b/src/main/java/org/skriptlang/skript/registration/DefaultSyntaxInfos.java @@ -31,7 +31,7 @@ interface Expression, R> extends Syn * @return A syntax info representing {@code type}. */ @Contract("_, _, _ , _-> new") - static , R> SyntaxInfo simple(Class expressionClass, + static , R> Expression simple(Class expressionClass, Supplier instanceSupplier, Class returnType, String... patterns) { return builder(expressionClass, returnType) .supplier(instanceSupplier) From 98180efbcdeeca2d9b3f4bbb9513394507a361dd Mon Sep 17 00:00:00 2001 From: Patrick Miller Date: Mon, 29 Jun 2026 11:17:28 -0400 Subject: [PATCH 15/41] Utility cleanup --- .../util => brigadier}/ArgumentData.java | 2 +- .../util => brigadier}/CommandCompiler.java | 12 +++-- .../command/brigadier/CommandParsingData.java | 48 +++++++++++++++++++ .../ScriptCommandEvent.java} | 6 +-- .../SkriptBrigadierArgument.java | 8 ++-- .../SkriptCommandExecutor.java | 8 ++-- .../elements/expressions/ExprArgument.java | 19 ++++---- .../elements/structures/StructCommand.java | 4 +- .../structures/util/SubCommandEntryData.java | 43 +++++++---------- .../tests/bukkit/command/StructCommand.sk | 12 +++++ 10 files changed, 108 insertions(+), 54 deletions(-) rename src/main/java/org/skriptlang/skript/bukkit/command/{elements/structures/util => brigadier}/ArgumentData.java (89%) rename src/main/java/org/skriptlang/skript/bukkit/command/{elements/structures/util => brigadier}/CommandCompiler.java (97%) create mode 100644 src/main/java/org/skriptlang/skript/bukkit/command/brigadier/CommandParsingData.java rename src/main/java/org/skriptlang/skript/bukkit/command/{elements/structures/util/CommandEvent.java => brigadier/ScriptCommandEvent.java} (76%) rename src/main/java/org/skriptlang/skript/bukkit/command/{elements/structures/util => brigadier}/SkriptBrigadierArgument.java (94%) rename src/main/java/org/skriptlang/skript/bukkit/command/{elements/structures/util => brigadier}/SkriptCommandExecutor.java (92%) diff --git a/src/main/java/org/skriptlang/skript/bukkit/command/elements/structures/util/ArgumentData.java b/src/main/java/org/skriptlang/skript/bukkit/command/brigadier/ArgumentData.java similarity index 89% rename from src/main/java/org/skriptlang/skript/bukkit/command/elements/structures/util/ArgumentData.java rename to src/main/java/org/skriptlang/skript/bukkit/command/brigadier/ArgumentData.java index 12db8e78d30..3f266991862 100644 --- a/src/main/java/org/skriptlang/skript/bukkit/command/elements/structures/util/ArgumentData.java +++ b/src/main/java/org/skriptlang/skript/bukkit/command/brigadier/ArgumentData.java @@ -1,4 +1,4 @@ -package org.skriptlang.skript.bukkit.command.elements.structures.util; +package org.skriptlang.skript.bukkit.command.brigadier; import ch.njol.skript.classes.ClassInfo; import ch.njol.skript.lang.Expression; 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/brigadier/CommandCompiler.java similarity index 97% rename from src/main/java/org/skriptlang/skript/bukkit/command/elements/structures/util/CommandCompiler.java rename to src/main/java/org/skriptlang/skript/bukkit/command/brigadier/CommandCompiler.java index daecfbd13f0..c7e4e4effb6 100644 --- a/src/main/java/org/skriptlang/skript/bukkit/command/elements/structures/util/CommandCompiler.java +++ b/src/main/java/org/skriptlang/skript/bukkit/command/brigadier/CommandCompiler.java @@ -1,4 +1,4 @@ -package org.skriptlang.skript.bukkit.command.elements.structures.util; +package org.skriptlang.skript.bukkit.command.brigadier; import ch.njol.skript.Skript; import ch.njol.skript.classes.ClassInfo; @@ -8,6 +8,7 @@ import ch.njol.skript.log.RetainingLogHandler; import ch.njol.skript.registrations.Classes; import ch.njol.skript.util.Utils; +import org.jetbrains.annotations.ApiStatus; import org.jetbrains.annotations.Nullable; import java.util.ArrayList; @@ -25,7 +26,8 @@ * and arguments {@link ArgumentCommandElement}. * @see #compile(String, List) */ -final class CommandCompiler { +@ApiStatus.Internal +public final class CommandCompiler { /* * Tree Structures @@ -34,7 +36,7 @@ final class CommandCompiler { /** * A node within a command tree. */ - static class CommandElement { + public static class CommandElement { protected final Set children; @@ -102,7 +104,7 @@ public void append(Collection elements) { /** * Represents a literal (constant) argument of a command. */ - static class LiteralCommandElement extends CommandElement { + public static class LiteralCommandElement extends CommandElement { private final String string; @@ -123,7 +125,7 @@ public String literal() { /** * Represents a dynamic argument of a command. */ - static class ArgumentCommandElement extends CommandElement { + public static class ArgumentCommandElement extends CommandElement { private final ArgumentData argument; diff --git a/src/main/java/org/skriptlang/skript/bukkit/command/brigadier/CommandParsingData.java b/src/main/java/org/skriptlang/skript/bukkit/command/brigadier/CommandParsingData.java new file mode 100644 index 00000000000..075c3c10bc1 --- /dev/null +++ b/src/main/java/org/skriptlang/skript/bukkit/command/brigadier/CommandParsingData.java @@ -0,0 +1,48 @@ +package org.skriptlang.skript.bukkit.command.brigadier; + +import ch.njol.skript.lang.parser.ParserInstance; +import ch.njol.skript.lang.parser.ParserInstance.Data; + +import java.util.ArrayDeque; +import java.util.Deque; +import java.util.LinkedList; +import java.util.List; + +/** + * Parsing data holding command context. + */ +public final class CommandParsingData extends Data { + + private final LinkedList> arguments = new LinkedList<>(); + private final Deque indices = new ArrayDeque<>(4); + + public CommandParsingData(ParserInstance parserInstance) { + super(parserInstance); + } + + /** + * @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 record. + * @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(); + } + +} diff --git a/src/main/java/org/skriptlang/skript/bukkit/command/elements/structures/util/CommandEvent.java b/src/main/java/org/skriptlang/skript/bukkit/command/brigadier/ScriptCommandEvent.java similarity index 76% rename from src/main/java/org/skriptlang/skript/bukkit/command/elements/structures/util/CommandEvent.java rename to src/main/java/org/skriptlang/skript/bukkit/command/brigadier/ScriptCommandEvent.java index b3a836681ce..5f5243e065f 100644 --- a/src/main/java/org/skriptlang/skript/bukkit/command/elements/structures/util/CommandEvent.java +++ b/src/main/java/org/skriptlang/skript/bukkit/command/brigadier/ScriptCommandEvent.java @@ -1,4 +1,4 @@ -package org.skriptlang.skript.bukkit.command.elements.structures.util; +package org.skriptlang.skript.bukkit.command.brigadier; import org.bukkit.command.CommandSender; import org.bukkit.event.Event; @@ -8,12 +8,12 @@ import java.util.HashMap; import java.util.Map; -public class CommandEvent extends Event { +public class ScriptCommandEvent extends Event { public final CommandSender sender; public final Map, Object> arguments = new HashMap<>(); - public CommandEvent(CommandSender sender) { + public ScriptCommandEvent(CommandSender sender) { this.sender = sender; } diff --git a/src/main/java/org/skriptlang/skript/bukkit/command/elements/structures/util/SkriptBrigadierArgument.java b/src/main/java/org/skriptlang/skript/bukkit/command/brigadier/SkriptBrigadierArgument.java similarity index 94% rename from src/main/java/org/skriptlang/skript/bukkit/command/elements/structures/util/SkriptBrigadierArgument.java rename to src/main/java/org/skriptlang/skript/bukkit/command/brigadier/SkriptBrigadierArgument.java index c699f5d1dd7..b40cf745019 100644 --- a/src/main/java/org/skriptlang/skript/bukkit/command/elements/structures/util/SkriptBrigadierArgument.java +++ b/src/main/java/org/skriptlang/skript/bukkit/command/brigadier/SkriptBrigadierArgument.java @@ -1,4 +1,4 @@ -package org.skriptlang.skript.bukkit.command.elements.structures.util; +package org.skriptlang.skript.bukkit.command.brigadier; import ch.njol.skript.lang.Literal; import ch.njol.skript.lang.ParseContext; @@ -19,6 +19,7 @@ import io.papermc.paper.command.brigadier.argument.CustomArgumentType; import org.bukkit.entity.Entity; import org.bukkit.entity.Player; +import org.jetbrains.annotations.ApiStatus; import org.jetbrains.annotations.NotNull; import java.util.Iterator; @@ -33,7 +34,8 @@ * This is natively a {@link StringArgumentType} with conversion occurring during execution. * @param The real type of the argument. */ -class SkriptBrigadierArgument implements CustomArgumentType.Converted { +@ApiStatus.Internal +public class SkriptBrigadierArgument implements CustomArgumentType.Converted { /** * Pre-defined mappings of types that are acceptable to map to other native argument types. @@ -77,7 +79,7 @@ class SkriptBrigadierArgument implements CustomArgumentType.Converted argument; private final StringArgumentType nativeType; diff --git a/src/main/java/org/skriptlang/skript/bukkit/command/elements/structures/util/SkriptCommandExecutor.java b/src/main/java/org/skriptlang/skript/bukkit/command/brigadier/SkriptCommandExecutor.java similarity index 92% rename from src/main/java/org/skriptlang/skript/bukkit/command/elements/structures/util/SkriptCommandExecutor.java rename to src/main/java/org/skriptlang/skript/bukkit/command/brigadier/SkriptCommandExecutor.java index bb0f2125275..b990d17dd0b 100644 --- a/src/main/java/org/skriptlang/skript/bukkit/command/elements/structures/util/SkriptCommandExecutor.java +++ b/src/main/java/org/skriptlang/skript/bukkit/command/brigadier/SkriptCommandExecutor.java @@ -1,4 +1,4 @@ -package org.skriptlang.skript.bukkit.command.elements.structures.util; +package org.skriptlang.skript.bukkit.command.brigadier; import ch.njol.skript.lang.Trigger; import ch.njol.skript.variables.Variables; @@ -9,6 +9,7 @@ import io.papermc.paper.command.brigadier.CommandSourceStack; import io.papermc.paper.command.brigadier.argument.resolvers.ArgumentResolver; import org.bukkit.event.Event; +import org.jetbrains.annotations.ApiStatus; import java.lang.reflect.Array; import java.util.List; @@ -19,7 +20,8 @@ /** * An executor for Brigadier commands. */ -class SkriptCommandExecutor { +@ApiStatus.Internal +public class SkriptCommandExecutor { private static void setVariable(String name, Object value, boolean isSingle, Event context) { if (isSingle) { @@ -42,7 +44,7 @@ public SkriptCommandExecutor(Trigger trigger, List> arguments) { } public int execute(CommandContext context) throws CommandSyntaxException { - CommandEvent commandEvent = new CommandEvent(context.getSource().getSender()); + ScriptCommandEvent commandEvent = new ScriptCommandEvent(context.getSource().getSender()); Set providedArgs = context.getNodes().stream() .filter(node -> node.getNode() instanceof ArgumentCommandNode) 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 index c4464ad0d0c..87c71105b0b 100644 --- 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 @@ -21,9 +21,9 @@ import org.bukkit.event.player.PlayerCommandPreprocessEvent; import org.bukkit.event.server.ServerCommandEvent; import org.jetbrains.annotations.Nullable; -import org.skriptlang.skript.bukkit.command.elements.structures.util.ArgumentData; -import org.skriptlang.skript.bukkit.command.elements.structures.util.CommandEvent; -import org.skriptlang.skript.bukkit.command.elements.structures.util.SubCommandEntryData.CommandParsingData; +import org.skriptlang.skript.bukkit.command.brigadier.ArgumentData; +import org.skriptlang.skript.bukkit.command.brigadier.ScriptCommandEvent; +import org.skriptlang.skript.bukkit.command.brigadier.CommandParsingData; import org.skriptlang.skript.registration.SyntaxInfo; import org.skriptlang.skript.registration.SyntaxRegistry; @@ -75,7 +75,8 @@ private enum ArgumentType { @Override public boolean init(Expression[] exprs, int matchedPattern, Kleenean isDelayed, ParseResult parseResult) { - boolean scriptCommand = getParser().isCurrentEvent(CommandEvent.class); + ParserInstance parser = getParser(); + boolean scriptCommand = parser.isCurrentEvent(ScriptCommandEvent.class); type = switch (matchedPattern) { case 0 -> ArgumentType.LAST; @@ -100,9 +101,7 @@ public boolean init(Expression[] exprs, int matchedPattern, Kleenean isDelaye List> currentArguments = null; if (scriptCommand) { - currentArguments = ParserInstance.get().getData(CommandParsingData.class).arguments.stream() - .flatMap(List::stream) - .toList(); + currentArguments = parser.getData(CommandParsingData.class).getArguments(); if (currentArguments.isEmpty()) { Skript.error("This command doesn't have any arguments"); return false; @@ -193,7 +192,7 @@ public boolean init(Expression[] exprs, int matchedPattern, Kleenean isDelaye @Override public Class[] supportedEvents() { - return CollectionUtils.array(CommandEvent.class, PlayerCommandPreprocessEvent.class, ServerCommandEvent.class); + return CollectionUtils.array(ScriptCommandEvent.class, PlayerCommandPreprocessEvent.class, ServerCommandEvent.class); } @Override @@ -201,10 +200,10 @@ public Class[] supportedEvents() { if (argument != null) { if (argument.isSingle()) { Object[] result = (Object[]) Array.newInstance(argument.type().getC(), 1); - result[0] = ((CommandEvent) event).arguments.get(argument); + result[0] = ((ScriptCommandEvent) event).arguments.get(argument); return result; } else { - return (Object[]) ((CommandEvent) event).arguments.get(argument); + return (Object[]) ((ScriptCommandEvent) event).arguments.get(argument); } } 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 index e79bb5b1207..8e42661a3bd 100644 --- 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 @@ -12,7 +12,7 @@ import org.bukkit.event.Event; import org.jetbrains.annotations.Nullable; import org.skriptlang.skript.addon.SkriptAddon; -import org.skriptlang.skript.bukkit.command.elements.structures.util.CommandEvent; +import org.skriptlang.skript.bukkit.command.brigadier.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; @@ -34,7 +34,7 @@ public static void register(SkriptAddon addon, SyntaxRegistry syntaxRegistry) { .build()); EventValueRegistry evRegistry = addon.registry(EventValueRegistry.class); - evRegistry.register(EventValue.simple(CommandEvent.class, CommandSender.class, CommandEvent::getSender)); + evRegistry.register(EventValue.simple(ScriptCommandEvent.class, CommandSender.class, ScriptCommandEvent::getSender)); Skript.getInstance().getLifecycleManager().registerEventHandler(LifecycleEvents.COMMANDS,commands -> { var registrar = commands.registrar(); 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 index 762ee686d66..8a349dd8ae2 100644 --- 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 @@ -7,7 +7,6 @@ import ch.njol.skript.lang.Trigger; import ch.njol.skript.lang.Variable; import ch.njol.skript.lang.parser.ParserInstance; -import ch.njol.skript.lang.parser.ParserInstance.Data; import ch.njol.skript.variables.HintManager; import com.mojang.brigadier.arguments.ArgumentType; import com.mojang.brigadier.arguments.StringArgumentType; @@ -15,33 +14,28 @@ import io.papermc.paper.command.brigadier.CommandSourceStack; import io.papermc.paper.command.brigadier.Commands; import org.jetbrains.annotations.Nullable; -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.brigadier.ArgumentData; +import org.skriptlang.skript.bukkit.command.brigadier.CommandCompiler; +import org.skriptlang.skript.bukkit.command.brigadier.CommandParsingData; +import org.skriptlang.skript.bukkit.command.brigadier.ScriptCommandEvent; +import org.skriptlang.skript.bukkit.command.brigadier.CommandCompiler.ArgumentCommandElement; +import org.skriptlang.skript.bukkit.command.brigadier.CommandCompiler.CommandElement; +import org.skriptlang.skript.bukkit.command.brigadier.CommandCompiler.CompilationResult; +import org.skriptlang.skript.bukkit.command.brigadier.CommandCompiler.LiteralCommandElement; +import org.skriptlang.skript.bukkit.command.brigadier.SkriptBrigadierArgument; +import org.skriptlang.skript.bukkit.command.brigadier.SkriptCommandExecutor; 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.util.TriggerEntryData; import java.util.Collection; -import java.util.LinkedList; import java.util.List; import java.util.regex.Matcher; import java.util.regex.Pattern; public class SubCommandEntryData extends EntryData>> { - public static final class CommandParsingData extends Data { - - public List>> arguments = new LinkedList<>(); - - public CommandParsingData(ParserInstance parserInstance) { - super(parserInstance); - } - - } - private static final Pattern COMMAND_PATTERN = Pattern.compile("(?i)^\\s*/?\\s*(.+)?$"); @@ -83,19 +77,16 @@ public SubCommandEntryData(String key, boolean optional, boolean multiple) { // set context for parsing ParserInstance parser = ParserInstance.get(); - parser.setCurrentEvent("command", CommandEvent.class); + parser.setCurrentEvent("command", ScriptCommandEvent.class); // parse arguments CommandParsingData parsingData = parser.getData(CommandParsingData.class); - CompilationResult compilationResult = CommandCompiler.compile(commandMatcher.group(1), - parsingData.arguments.stream() - .flatMap(List::stream) - .toList()); + 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 (parsingData.arguments.isEmpty()) { + if (parsingData.getArguments().isEmpty()) { Skript.error("A command must have a name."); } else { Skript.error("A subcommand must have at least one argument, literal or dynamic."); @@ -104,10 +95,8 @@ public SubCommandEntryData(String key, boolean optional, boolean multiple) { } // prepare arguments - parsingData.arguments.addLast(compilationResult.arguments()); - List> allArguments = parsingData.arguments.stream() - .flatMap(List::stream) - .toList(); + parsingData.pushArguments(compilationResult.arguments()); + List> allArguments = parsingData.getArguments(); // parse execution trigger HintManager hintManager = parser.getHintManager(); @@ -153,7 +142,7 @@ public SubCommandEntryData(String key, boolean optional, boolean multiple) { Skript.error("You must have a 'trigger' entry if there are no subcommands!"); return null; } - parsingData.arguments.removeLast(); + parsingData.popArguments(); //noinspection rawtypes, unchecked return (List) compilationResult.root().children().stream() diff --git a/src/test/skript/tests/bukkit/command/StructCommand.sk b/src/test/skript/tests/bukkit/command/StructCommand.sk index 75a0c4df49f..d0242be1398 100644 --- a/src/test/skript/tests/bukkit/command/StructCommand.sk +++ b/src/test/skript/tests/bukkit/command/StructCommand.sk @@ -24,6 +24,13 @@ parse: 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::*} @@ -56,6 +63,11 @@ test "StructCommand": 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" From 70bac5d7ff585773489ce07e40dade7297a5a417 Mon Sep 17 00:00:00 2001 From: Patrick Miller Date: Mon, 29 Jun 2026 12:12:20 -0400 Subject: [PATCH 16/41] Improve default string argument behavior --- .../bukkit/command/brigadier/CommandCompiler.java | 10 ++++++++++ src/test/skript/tests/misc/timespans.sk | 3 +-- .../regressions/6936-single quotes in commands.sk | 4 ++-- 3 files changed, 13 insertions(+), 4 deletions(-) diff --git a/src/main/java/org/skriptlang/skript/bukkit/command/brigadier/CommandCompiler.java b/src/main/java/org/skriptlang/skript/bukkit/command/brigadier/CommandCompiler.java index c7e4e4effb6..d9289bee504 100644 --- a/src/main/java/org/skriptlang/skript/bukkit/command/brigadier/CommandCompiler.java +++ b/src/main/java/org/skriptlang/skript/bukkit/command/brigadier/CommandCompiler.java @@ -249,6 +249,7 @@ public record CompilationResult(CommandElement root, List> argum 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 { @@ -277,6 +278,7 @@ public record CompilationResult(CommandElement root, List> argum 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; } @@ -333,6 +335,9 @@ private static void appendToLiterals(List literals, char */ 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) @@ -363,6 +368,7 @@ private static List appendToLiterals(Collection 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; } @@ -387,12 +393,14 @@ private static List appendToLiterals(Collection appendToLiterals(Collection"]: +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" From 544dcc1ca4ec7f171dcf3113dbf263ab727bbc9a Mon Sep 17 00:00:00 2001 From: Patrick Miller Date: Mon, 29 Jun 2026 16:49:48 -0400 Subject: [PATCH 17/41] Add back several command entries --- .../command/brigadier/CommandParsingData.java | 45 ++++- .../command/brigadier/ExecutorData.java | 90 ++++++++++ .../elements/structures/StructCommand.java | 23 ++- .../structures/util/SubCommandEntryData.java | 161 ++++++++++++++++-- .../tests/bukkit/command/StructCommand.sk | 13 ++ 5 files changed, 311 insertions(+), 21 deletions(-) create mode 100644 src/main/java/org/skriptlang/skript/bukkit/command/brigadier/ExecutorData.java diff --git a/src/main/java/org/skriptlang/skript/bukkit/command/brigadier/CommandParsingData.java b/src/main/java/org/skriptlang/skript/bukkit/command/brigadier/CommandParsingData.java index 075c3c10bc1..5927b8675f1 100644 --- a/src/main/java/org/skriptlang/skript/bukkit/command/brigadier/CommandParsingData.java +++ b/src/main/java/org/skriptlang/skript/bukkit/command/brigadier/CommandParsingData.java @@ -2,11 +2,13 @@ import ch.njol.skript.lang.parser.ParserInstance; import ch.njol.skript.lang.parser.ParserInstance.Data; +import org.jetbrains.annotations.Nullable; import java.util.ArrayDeque; import java.util.Deque; import java.util.LinkedList; import java.util.List; +import java.util.function.Function; /** * Parsing data holding command context. @@ -16,10 +18,19 @@ public final class CommandParsingData extends Data { private final LinkedList> arguments = new LinkedList<>(); private final Deque indices = new ArrayDeque<>(4); + private final Deque executorDatas = new ArrayDeque<>(4); + 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. */ @@ -29,7 +40,7 @@ public List> getArguments() { /** * Pushes arguments to this data. - * @param arguments The arguments to record. + * @param arguments The arguments to push. * @see #popArguments() */ public void pushArguments(List> arguments) { @@ -45,4 +56,36 @@ 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/brigadier/ExecutorData.java b/src/main/java/org/skriptlang/skript/bukkit/command/brigadier/ExecutorData.java new file mode 100644 index 00000000000..09a134bf3b6 --- /dev/null +++ b/src/main/java/org/skriptlang/skript/bukkit/command/brigadier/ExecutorData.java @@ -0,0 +1,90 @@ +package org.skriptlang.skript.bukkit.command.brigadier; + +import org.bukkit.command.CommandSender; +import org.bukkit.command.ConsoleCommandSender; +import org.bukkit.command.RemoteConsoleCommandSender; +import org.bukkit.entity.Player; +import org.jetbrains.annotations.Nullable; + +import java.util.function.Predicate; + +public record ExecutorData( + @Nullable SkriptCommandExecutor executor, + @Nullable ExecutableBy executableBy, + @Nullable String permission +) { + + /** + * Enum describing the types of {@link org.bukkit.command.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 console. + */ + CONSOLE(sender -> sender instanceof ConsoleCommandSender || sender instanceof RemoteConsoleCommandSender), + + /** + * This command is executable by any {@link org.bukkit.command.CommandSender}. + */ + ALL(ignored -> true), + + /** + * This command is not executable. + */ + NONE(ignored -> false); + + 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; + } + + /** + * Combines (ORs) this ExecutableBy with another. + * For example, {@link #PLAYERS}.with({@link #CONSOLE}) is {@link #ALL}. + * @param other Other to OR with. + * @return The resulting ExecutableBy. + */ + public ExecutableBy with(ExecutableBy other) { + if (this == NONE) { + return other; + } else if (other == NONE) { + return this; + } + return this == other ? this : ALL; + } + + /** + * @param other Subset ExecutableBy to consider. + * @return Whether this ExecutableBy is the same as or covers a superset of {@code other}. + */ + public boolean includes(ExecutableBy other) { + return this == ALL || this == other; + } + + @Override + public String toString() { + return switch (this) { + case PLAYERS -> "players"; + case CONSOLE -> "the console"; + case ALL -> "the console and players"; + case NONE -> "none"; + }; + } + + } + +} 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 index 8e42661a3bd..d2f4a12b792 100644 --- 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 @@ -21,6 +21,7 @@ import org.skriptlang.skript.registration.SyntaxInfo; import org.skriptlang.skript.registration.SyntaxRegistry; +import java.util.List; import java.util.Set; import java.util.concurrent.ConcurrentHashMap; import java.util.concurrent.atomic.AtomicBoolean; @@ -38,14 +39,22 @@ public static void register(SkriptAddon addon, SyntaxRegistry syntaxRegistry) { Skript.getInstance().getLifecycleManager().registerEventHandler(LifecycleEvents.COMMANDS,commands -> { var registrar = commands.registrar(); - COMMANDS.forEach(registrar::register); + for (var registration : COMMANDS) { + registrar.register(registration.node, registration.description, registration.aliases); + } }); } private static final SubCommandEntryData ROOT_ENTRY_DATA = new SubCommandEntryData("command", false, false); - private static final Set> COMMANDS = ConcurrentHashMap.newKeySet(); + private record CommandRegistration( + LiteralCommandNode node, + List aliases, + String description + ) { } + + private static final Set COMMANDS = ConcurrentHashMap.newKeySet(); private static final AtomicBoolean SYNC_COMMANDS = new AtomicBoolean(); private static void performSync() { @@ -55,7 +64,7 @@ private static void performSync() { } private SectionNode rootNode; - private LiteralCommandNode command; + private CommandRegistration command; @Override public boolean init(Literal[] args, int matchedPattern, ParseResult parseResult, EntryContainer entryContainer) { @@ -65,15 +74,15 @@ public boolean init(Literal[] args, int matchedPattern, ParseResult parseResu @Override public boolean load() { - var command = ROOT_ENTRY_DATA.getValue(rootNode); - if (command == null) { + var result = ROOT_ENTRY_DATA.getValue(rootNode); + if (result == null) { // parsing failed, entry will have emitted a specific error message return false; } - if (command.size() != 1 || !(command.getFirst().build() instanceof LiteralCommandNode commandNode)) { + if (result.arguments().size() != 1 || !(result.arguments().getFirst().build() instanceof LiteralCommandNode node)) { Skript.error("A command must have a name."); return false; } - this.command = commandNode; + this.command = new CommandRegistration(node, result.aliases(), result.description()); // TODO validate whether command already exists COMMANDS.add(this.command); 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 index 8a349dd8ae2..1a056b3fe3b 100644 --- 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 @@ -13,10 +13,12 @@ import com.mojang.brigadier.builder.ArgumentBuilder; import io.papermc.paper.command.brigadier.CommandSourceStack; import io.papermc.paper.command.brigadier.Commands; +import org.bukkit.command.CommandSender; import org.jetbrains.annotations.Nullable; import org.skriptlang.skript.bukkit.command.brigadier.ArgumentData; import org.skriptlang.skript.bukkit.command.brigadier.CommandCompiler; import org.skriptlang.skript.bukkit.command.brigadier.CommandParsingData; +import org.skriptlang.skript.bukkit.command.brigadier.ExecutorData; import org.skriptlang.skript.bukkit.command.brigadier.ScriptCommandEvent; import org.skriptlang.skript.bukkit.command.brigadier.CommandCompiler.ArgumentCommandElement; import org.skriptlang.skript.bukkit.command.brigadier.CommandCompiler.CommandElement; @@ -24,24 +26,79 @@ import org.skriptlang.skript.bukkit.command.brigadier.CommandCompiler.LiteralCommandElement; import org.skriptlang.skript.bukkit.command.brigadier.SkriptBrigadierArgument; import org.skriptlang.skript.bukkit.command.brigadier.SkriptCommandExecutor; +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.TriggerEntryData; +import org.skriptlang.skript.lang.script.ScriptWarning; +import java.util.ArrayList; +import java.util.Arrays; import java.util.Collection; import java.util.List; +import java.util.function.Predicate; import java.util.regex.Matcher; import java.util.regex.Pattern; -public class SubCommandEntryData extends EntryData>> { +public class SubCommandEntryData extends EntryData { + + public record Result( + List> arguments, + List aliases, + String description + ) { } + + 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", "", true) + .addEntry("permission", null, true) + .addEntryData(new KeyValueEntryData("executable by", null, true) { + private final Pattern pattern = Pattern.compile("\\s*,\\s*|\\s+(and|or)\\s+"); + + @Override + protected ExecutorData.ExecutableBy getValue(String value) { + ExecutorData.ExecutableBy executableBy = ExecutorData.ExecutableBy.NONE; + for (String type : pattern.split(value)) { + if (type.equalsIgnoreCase("console") || type.equalsIgnoreCase("the console")) { + executableBy = executableBy.with(ExecutorData.ExecutableBy.CONSOLE); + } else if (type.equalsIgnoreCase("players") || type.equalsIgnoreCase("player")) { + executableBy = executableBy.with(ExecutorData.ExecutableBy.PLAYERS); + } else { + Skript.error("Invalid command sender type: " + type); + return ExecutorData.ExecutableBy.NONE; + } + } + return executableBy; + } + }) .addEntryData(new TriggerEntryData("trigger", null, true)) .addEntryData(new SubCommandEntryData("subcommand", true, true)) + // deprecated entries + .addEntry("usage", null, true) + .addEntry("prefix", null, true) + .addEntry("permission message", null, true) .build(); static { @@ -53,7 +110,7 @@ public SubCommandEntryData(String key, boolean optional, boolean multiple) { } @Override - public @Nullable List> getValue(Node node) { + public @Nullable Result getValue(Node node) { assert node instanceof SectionNode; // validate section node structure @@ -81,12 +138,13 @@ public SubCommandEntryData(String key, boolean optional, boolean multiple) { // 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 (parsingData.getArguments().isEmpty()) { + if (isRoot) { Skript.error("A command must have a name."); } else { Skript.error("A subcommand must have at least one argument, literal or dynamic."); @@ -98,6 +156,70 @@ public SubCommandEntryData(String key, boolean optional, boolean multiple) { parsingData.pushArguments(compilationResult.arguments()); List> allArguments = parsingData.getArguments(); + // 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.get("description", String.class, true); + if (!isRoot && !description.isEmpty()) { + Skript.error("Only the root of a command may have a description."); + 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 + ExecutorData.ExecutableBy executableBy = entryContainer.getOptional("executable by", ExecutorData.ExecutableBy.class, false); + if (executableBy != null) { + if (executableBy == ExecutorData.ExecutableBy.NONE) { // parsing failed + return null; + } + ExecutorData.ExecutableBy parent = parsingData.getExecutorData(ExecutorData::executableBy); + if (parent != null && !parent.includes(executableBy)) { + Skript.error("It is not possible to restrict execution to " + executableBy + + " as the parent command is only executable by " + parent + "."); + } + } + + // prepare final requirements predicate + Predicate commandRequires; + if (requires != TRUE_PREDICATE) { + final Predicate finalRequires = requires; + commandRequires = source -> finalRequires.test(source.getSender()); + } else { + commandRequires = null; + } + + // handle deprecated entries + if (entryContainer.hasEntry("usage")) { + ScriptWarning.printDeprecationWarning("The 'usage' entry has been deprecated for removal in a future release." + + " Incorrect command usage is now handled by the client's command validator."); + } + if (entryContainer.hasEntry("prefix")) { + ScriptWarning.printDeprecationWarning("The 'prefix' entry has been deprecated for removal in a future release." + + " It is no longer possible to modify the prefix (namespace) of a command."); + } + 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."); + } + // parse execution trigger HintManager hintManager = parser.getHintManager(); Trigger execute; @@ -130,24 +252,29 @@ public SubCommandEntryData(String key, boolean optional, boolean multiple) { } else { executor = null; } + parsingData.pushExecutorData(new ExecutorData(executor, executableBy, permission)); // attach subcommand pieces // TODO verify error behavior... - //noinspection unchecked List> subcommands = - entryContainer.getAll("subcommand", List.class, false).stream() - .flatMap(List::stream) + 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; } - parsingData.popArguments(); - //noinspection rawtypes, unchecked - return (List) compilationResult.root().children().stream() - .map(child -> parse(child, executor, subcommands)) + //noinspection rawtypes + var result = (List) compilationResult.root().children().stream() + .map(child -> parse(child, executor, commandRequires, subcommands)) .toList(); + + parsingData.popExecutorData(); + parsingData.popArguments(); + + //noinspection unchecked + return new Result(result, aliases, description); } @Override @@ -163,8 +290,12 @@ public boolean canCreateWith(Node node) { return key.regionMatches(true, 0, prefix, 0, prefix.length()); } - private static ArgumentBuilder parse(CommandElement commandElement, - @Nullable SkriptCommandExecutor executor, Collection> subcommands) { + private static ArgumentBuilder parse( + CommandElement commandElement, + @Nullable SkriptCommandExecutor executor, + @Nullable Predicate requires, + Collection> subcommands + ) { Collection children = commandElement.children(); ArgumentBuilder argument; @@ -194,8 +325,9 @@ public boolean canCreateWith(Node node) { if (element == null) { continue; } - argument.then(parse(element, executor, subcommands)); + argument.then(parse(element, executor, requires, 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 // TODO there is actually more complexity here to handle @@ -208,6 +340,9 @@ public boolean canCreateWith(Node node) { if (executor != null) { argument.executes(executor::execute); } + if (requires != null) { + argument.requires(requires); + } } return argument; diff --git a/src/test/skript/tests/bukkit/command/StructCommand.sk b/src/test/skript/tests/bukkit/command/StructCommand.sk index d0242be1398..6e813050146 100644 --- a/src/test/skript/tests/bukkit/command/StructCommand.sk +++ b/src/test/skript/tests/bukkit/command/StructCommand.sk @@ -3,6 +3,18 @@ using local variable type hints options: command: skriptcommand +parse: + results: {-StructCommand::entries::1::*} + code: + command /entries_test_1: + executable by: players + trigger: + stop + subcommand test: + executable by: console + trigger: + stop + parse: results: {-StructCommand::args::1::*} code: @@ -57,6 +69,7 @@ command /commandtest : test "StructCommand": # parsing + assert {-StructCommand::entries::1::*} is "It is not possible to restrict execution to the console as the parent command is only executable by players." assert {-StructCommand::args::1::*} is "The argument name 'x' was already used." # 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::*}%)" From c9d73b1d1856fb640d3b426e82a5e3ed8533e65c Mon Sep 17 00:00:00 2001 From: Patrick Miller Date: Tue, 30 Jun 2026 16:27:49 -0400 Subject: [PATCH 18/41] Cooldowns and fixes --- .../skript/effects/EffCancelCooldown.java | 69 ------ .../expressions/ExprCmdCooldownInfo.java | 211 ------------------ .../skript/bukkit/command/CommandModule.java | 4 + .../command/brigadier/CommandParsingData.java | 2 + .../command/brigadier/ExecutorData.java | 203 ++++++++++++++++- .../command/brigadier/ScriptCommandEvent.java | 20 +- .../brigadier/SkriptBrigadierArgument.java | 3 + .../brigadier/SkriptCommandExecutor.java | 20 +- .../elements/effects/EffCancelCooldown.java | 78 +++++++ .../elements/expressions/ExprArgument.java | 5 +- .../expressions/ExprCmdCooldownInfo.java | 201 +++++++++++++++++ .../structures/util/SubCommandEntryData.java | 74 ++++-- 12 files changed, 584 insertions(+), 306 deletions(-) delete mode 100644 src/main/java/ch/njol/skript/effects/EffCancelCooldown.java delete mode 100644 src/main/java/ch/njol/skript/expressions/ExprCmdCooldownInfo.java create mode 100644 src/main/java/org/skriptlang/skript/bukkit/command/elements/effects/EffCancelCooldown.java create mode 100644 src/main/java/org/skriptlang/skript/bukkit/command/elements/expressions/ExprCmdCooldownInfo.java 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/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/org/skriptlang/skript/bukkit/command/CommandModule.java b/src/main/java/org/skriptlang/skript/bukkit/command/CommandModule.java index bf439a91f1c..e9735d78a01 100644 --- a/src/main/java/org/skriptlang/skript/bukkit/command/CommandModule.java +++ b/src/main/java/org/skriptlang/skript/bukkit/command/CommandModule.java @@ -3,7 +3,9 @@ import org.skriptlang.skript.addon.AddonModule; import org.skriptlang.skript.addon.HierarchicalAddonModule; import org.skriptlang.skript.addon.SkriptAddon; +import org.skriptlang.skript.bukkit.command.elements.effects.EffCancelCooldown; import org.skriptlang.skript.bukkit.command.elements.expressions.ExprArgument; +import org.skriptlang.skript.bukkit.command.elements.expressions.ExprCmdCooldownInfo; import org.skriptlang.skript.bukkit.command.elements.structures.StructCommand; public class CommandModule extends HierarchicalAddonModule { @@ -15,7 +17,9 @@ public CommandModule(AddonModule parentModule) { @Override protected void loadSelf(SkriptAddon addon) { register(addon, + EffCancelCooldown::register, ExprArgument::register, + ExprCmdCooldownInfo::register, syntaxRegistry -> StructCommand.register(addon, syntaxRegistry) ); } diff --git a/src/main/java/org/skriptlang/skript/bukkit/command/brigadier/CommandParsingData.java b/src/main/java/org/skriptlang/skript/bukkit/command/brigadier/CommandParsingData.java index 5927b8675f1..137acf62ad9 100644 --- a/src/main/java/org/skriptlang/skript/bukkit/command/brigadier/CommandParsingData.java +++ b/src/main/java/org/skriptlang/skript/bukkit/command/brigadier/CommandParsingData.java @@ -20,6 +20,8 @@ public final class CommandParsingData extends Data { private final Deque executorDatas = new ArrayDeque<>(4); + public boolean isParsingCooldownEntry = false; + public CommandParsingData(ParserInstance parserInstance) { super(parserInstance); } diff --git a/src/main/java/org/skriptlang/skript/bukkit/command/brigadier/ExecutorData.java b/src/main/java/org/skriptlang/skript/bukkit/command/brigadier/ExecutorData.java index 09a134bf3b6..5e46b64af7a 100644 --- a/src/main/java/org/skriptlang/skript/bukkit/command/brigadier/ExecutorData.java +++ b/src/main/java/org/skriptlang/skript/bukkit/command/brigadier/ExecutorData.java @@ -1,21 +1,37 @@ package org.skriptlang.skript.bukkit.command.brigadier; +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.command.ConsoleCommandSender; import org.bukkit.command.RemoteConsoleCommandSender; 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; import java.util.function.Predicate; +/** + * @param executableBy Describes what kind of {@link CommandSender} can execute the command. + * @param cooldownManager Handles cooldown management for cooldown command entries. + */ public record ExecutorData( - @Nullable SkriptCommandExecutor executor, @Nullable ExecutableBy executableBy, - @Nullable String permission + @Nullable CooldownManager cooldownManager ) { /** - * Enum describing the types of {@link org.bukkit.command.CommandSender}s that can execute this command. + * Enum describing the types of {@link CommandSender}s that can execute this command. */ public enum ExecutableBy { @@ -30,7 +46,7 @@ public enum ExecutableBy { CONSOLE(sender -> sender instanceof ConsoleCommandSender || sender instanceof RemoteConsoleCommandSender), /** - * This command is executable by any {@link org.bukkit.command.CommandSender}. + * This command is executable by any {@link CommandSender}. */ ALL(ignored -> true), @@ -87,4 +103,183 @@ public String toString() { } + /** + * Used to manage cooldowns for an individual command. + */ + public static 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/brigadier/ScriptCommandEvent.java b/src/main/java/org/skriptlang/skript/bukkit/command/brigadier/ScriptCommandEvent.java index 5f5243e065f..c987b8e7072 100644 --- a/src/main/java/org/skriptlang/skript/bukkit/command/brigadier/ScriptCommandEvent.java +++ b/src/main/java/org/skriptlang/skript/bukkit/command/brigadier/ScriptCommandEvent.java @@ -8,19 +8,33 @@ 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 { - public final CommandSender sender; - public final Map, Object> arguments = new HashMap<>(); + private final CommandSender sender; + private final SkriptCommandExecutor executor; - public ScriptCommandEvent(CommandSender sender) { + final Map arguments = new HashMap<>(); + + public ScriptCommandEvent(CommandSender sender, SkriptCommandExecutor executor) { this.sender = sender; + this.executor = executor; } public CommandSender getSender() { return sender; } + public SkriptCommandExecutor getExecutor() { + return executor; + } + + 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/brigadier/SkriptBrigadierArgument.java b/src/main/java/org/skriptlang/skript/bukkit/command/brigadier/SkriptBrigadierArgument.java index b40cf745019..14bd31934ce 100644 --- a/src/main/java/org/skriptlang/skript/bukkit/command/brigadier/SkriptBrigadierArgument.java +++ b/src/main/java/org/skriptlang/skript/bukkit/command/brigadier/SkriptBrigadierArgument.java @@ -125,6 +125,9 @@ public SkriptBrigadierArgument(@NotNull ArgumentData argument, @NotNull Strin // treat as a valid match for // treat <"foo b> as a valid match for supplier.get().forEachRemaining(value -> { + if (value == null) { + return; + } String name = Classes.toString(value).toLowerCase(Locale.ENGLISH) .replace(' ', '_'); String remaining = builder.getRemainingLowerCase(); diff --git a/src/main/java/org/skriptlang/skript/bukkit/command/brigadier/SkriptCommandExecutor.java b/src/main/java/org/skriptlang/skript/bukkit/command/brigadier/SkriptCommandExecutor.java index b990d17dd0b..1c1ae159e65 100644 --- a/src/main/java/org/skriptlang/skript/bukkit/command/brigadier/SkriptCommandExecutor.java +++ b/src/main/java/org/skriptlang/skript/bukkit/command/brigadier/SkriptCommandExecutor.java @@ -8,8 +8,11 @@ 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 org.skriptlang.skript.bukkit.command.brigadier.ExecutorData.CooldownManager; import java.lang.reflect.Array; import java.util.List; @@ -37,14 +40,25 @@ private static void setVariable(String name, Object value, boolean isSingle, Eve private final Trigger trigger; private final List> arguments; + private final @Nullable CooldownManager cooldownManager; - public SkriptCommandExecutor(Trigger trigger, List> arguments) { + public SkriptCommandExecutor(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 { - ScriptCommandEvent commandEvent = new ScriptCommandEvent(context.getSource().getSender()); + CommandSender sender = context.getSource().getSender(); + ScriptCommandEvent commandEvent = new ScriptCommandEvent(sender, this); + + if (cooldownManager != null && !cooldownManager.checkExecution(commandEvent, sender)) { + return Command.SINGLE_SUCCESS; + } Set providedArgs = context.getNodes().stream() .filter(node -> node.getNode() instanceof ArgumentCommandNode) @@ -80,7 +94,7 @@ public int execute(CommandContext context) throws CommandSyn } if (value != null) { - commandEvent.arguments.put(argument, value); + commandEvent.arguments.put(argument.name(), value); if (!argument.isAutomaticName()) { // store explicitly named arguments as variables setVariable(argument.name(), value, argument.isSingle(), commandEvent); } 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..17512e0a4a9 --- /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.brigadier.CommandParsingData; +import org.skriptlang.skript.bukkit.command.brigadier.ExecutorData; +import org.skriptlang.skript.bukkit.command.brigadier.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.getExecutor().getCooldownManager() != null; + commandEvent.getExecutor().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/expressions/ExprArgument.java b/src/main/java/org/skriptlang/skript/bukkit/command/elements/expressions/ExprArgument.java index 87c71105b0b..bedb6f34a40 100644 --- 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 @@ -198,12 +198,13 @@ public Class[] supportedEvents() { @Override protected Object @Nullable [] get(Event event) { if (argument != null) { + Object value = ((ScriptCommandEvent) event).getArgument(argument.name()); if (argument.isSingle()) { Object[] result = (Object[]) Array.newInstance(argument.type().getC(), 1); - result[0] = ((ScriptCommandEvent) event).arguments.get(argument); + result[0] = value; return result; } else { - return (Object[]) ((ScriptCommandEvent) event).arguments.get(argument); + return (Object[]) value; } } 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..1f9d3ef01b8 --- /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.brigadier.CommandParsingData; +import org.skriptlang.skript.bukkit.command.brigadier.ExecutorData; +import org.skriptlang.skript.bukkit.command.brigadier.ExecutorData.CooldownManager; +import org.skriptlang.skript.bukkit.command.brigadier.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.getExecutor().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.getExecutor().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/org/skriptlang/skript/bukkit/command/elements/structures/util/SubCommandEntryData.java b/src/main/java/org/skriptlang/skript/bukkit/command/elements/structures/util/SubCommandEntryData.java index 1a056b3fe3b..6e83dc65e33 100644 --- 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 @@ -6,19 +6,25 @@ 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 com.mojang.brigadier.arguments.ArgumentType; import com.mojang.brigadier.arguments.StringArgumentType; import com.mojang.brigadier.builder.ArgumentBuilder; 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.brigadier.ArgumentData; import org.skriptlang.skript.bukkit.command.brigadier.CommandCompiler; import org.skriptlang.skript.bukkit.command.brigadier.CommandParsingData; import org.skriptlang.skript.bukkit.command.brigadier.ExecutorData; +import org.skriptlang.skript.bukkit.command.brigadier.ExecutorData.CooldownManager; +import org.skriptlang.skript.bukkit.command.brigadier.ExecutorData.ExecutableBy; import org.skriptlang.skript.bukkit.command.brigadier.ScriptCommandEvent; import org.skriptlang.skript.bukkit.command.brigadier.CommandCompiler.ArgumentCommandElement; import org.skriptlang.skript.bukkit.command.brigadier.CommandCompiler.CommandElement; @@ -31,8 +37,11 @@ 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; @@ -74,25 +83,29 @@ protected List getValue(String value) { }) .addEntry("description", "", true) .addEntry("permission", null, true) - .addEntryData(new KeyValueEntryData("executable by", null, true) { + .addEntryData(new KeyValueEntryData("executable by", null, true) { private final Pattern pattern = Pattern.compile("\\s*,\\s*|\\s+(and|or)\\s+"); @Override - protected ExecutorData.ExecutableBy getValue(String value) { - ExecutorData.ExecutableBy executableBy = ExecutorData.ExecutableBy.NONE; + protected ExecutableBy getValue(String value) { + ExecutableBy executableBy = ExecutableBy.NONE; for (String type : pattern.split(value)) { if (type.equalsIgnoreCase("console") || type.equalsIgnoreCase("the console")) { - executableBy = executableBy.with(ExecutorData.ExecutableBy.CONSOLE); + executableBy = executableBy.with(ExecutableBy.CONSOLE); } else if (type.equalsIgnoreCase("players") || type.equalsIgnoreCase("player")) { - executableBy = executableBy.with(ExecutorData.ExecutableBy.PLAYERS); + executableBy = executableBy.with(ExecutableBy.PLAYERS); } else { Skript.error("Invalid command sender type: " + type); - return ExecutorData.ExecutableBy.NONE; + return ExecutableBy.NONE; } } 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 @@ -185,16 +198,18 @@ public SubCommandEntryData(String key, boolean optional, boolean multiple) { } // executable by requirement - ExecutorData.ExecutableBy executableBy = entryContainer.getOptional("executable by", ExecutorData.ExecutableBy.class, false); + ExecutableBy executableBy = entryContainer.getOptional("executable by", ExecutableBy.class, false); if (executableBy != null) { - if (executableBy == ExecutorData.ExecutableBy.NONE) { // parsing failed + if (executableBy == ExecutableBy.NONE) { // parsing failed return null; } - ExecutorData.ExecutableBy parent = parsingData.getExecutorData(ExecutorData::executableBy); + ExecutableBy parent = parsingData.getExecutorData(ExecutorData::executableBy); if (parent != null && !parent.includes(executableBy)) { Skript.error("It is not possible to restrict execution to " + executableBy + " as the parent command is only executable by " + parent + "."); + return null; } + requires = requires.and(executableBy.predicate()); } // prepare final requirements predicate @@ -206,6 +221,35 @@ public SubCommandEntryData(String key, boolean optional, boolean multiple) { 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("usage")) { ScriptWarning.printDeprecationWarning("The 'usage' entry has been deprecated for removal in a future release." + @@ -220,6 +264,8 @@ public SubCommandEntryData(String key, boolean optional, boolean multiple) { " Commands that a player does not have permission to execute are no longer sent to their client."); } + parsingData.pushExecutorData(new ExecutorData(executableBy, cooldownManager)); + // parse execution trigger HintManager hintManager = parser.getHintManager(); Trigger execute; @@ -248,11 +294,10 @@ public SubCommandEntryData(String key, boolean optional, boolean multiple) { // setup executor SkriptCommandExecutor executor; if (hasExecute) { - executor = new SkriptCommandExecutor(execute, allArguments); + executor = new SkriptCommandExecutor(execute, allArguments, cooldownManager); } else { executor = null; } - parsingData.pushExecutorData(new ExecutorData(executor, executableBy, permission)); // attach subcommand pieces // TODO verify error behavior... @@ -321,6 +366,10 @@ public boolean canCreateWith(Node node) { argument = Commands.argument(data.name(), nativeType); } + if (requires != null) { + argument.requires(requires); + } + for (CommandElement element : children) { if (element == null) { continue; @@ -340,9 +389,6 @@ public boolean canCreateWith(Node node) { if (executor != null) { argument.executes(executor::execute); } - if (requires != null) { - argument.requires(requires); - } } return argument; From 26b14d7f15795f66a1e908206c1c5ca6102e82a1 Mon Sep 17 00:00:00 2001 From: Patrick Miller Date: Thu, 2 Jul 2026 12:41:22 -0400 Subject: [PATCH 19/41] Add alternative reflection-based command registration This is significantly faster for reloads than the "safe" approach (20x in my testing) --- .../skript/bukkit/command/CommandModule.java | 4 + .../brigadier/RuntimeCommandRegistrar.java | 168 ++++++++++++++++++ .../elements/structures/StructCommand.java | 41 ++--- .../structures/util/SubCommandEntryData.java | 10 +- 4 files changed, 188 insertions(+), 35 deletions(-) create mode 100644 src/main/java/org/skriptlang/skript/bukkit/command/brigadier/RuntimeCommandRegistrar.java diff --git a/src/main/java/org/skriptlang/skript/bukkit/command/CommandModule.java b/src/main/java/org/skriptlang/skript/bukkit/command/CommandModule.java index e9735d78a01..2e2f8615141 100644 --- a/src/main/java/org/skriptlang/skript/bukkit/command/CommandModule.java +++ b/src/main/java/org/skriptlang/skript/bukkit/command/CommandModule.java @@ -1,8 +1,10 @@ package org.skriptlang.skript.bukkit.command; +import ch.njol.skript.Skript; import org.skriptlang.skript.addon.AddonModule; import org.skriptlang.skript.addon.HierarchicalAddonModule; import org.skriptlang.skript.addon.SkriptAddon; +import org.skriptlang.skript.bukkit.command.brigadier.RuntimeCommandRegistrar; import org.skriptlang.skript.bukkit.command.elements.effects.EffCancelCooldown; import org.skriptlang.skript.bukkit.command.elements.expressions.ExprArgument; import org.skriptlang.skript.bukkit.command.elements.expressions.ExprCmdCooldownInfo; @@ -16,6 +18,8 @@ public CommandModule(AddonModule parentModule) { @Override protected void loadSelf(SkriptAddon addon) { + RuntimeCommandRegistrar.init(Skript.getInstance()); + register(addon, EffCancelCooldown::register, ExprArgument::register, diff --git a/src/main/java/org/skriptlang/skript/bukkit/command/brigadier/RuntimeCommandRegistrar.java b/src/main/java/org/skriptlang/skript/bukkit/command/brigadier/RuntimeCommandRegistrar.java new file mode 100644 index 00000000000..11fb5fcb32a --- /dev/null +++ b/src/main/java/org/skriptlang/skript/bukkit/command/brigadier/RuntimeCommandRegistrar.java @@ -0,0 +1,168 @@ +package org.skriptlang.skript.bukkit.command.brigadier; + +import ch.njol.skript.Skript; +import com.mojang.brigadier.tree.LiteralCommandNode; +import io.papermc.paper.command.brigadier.CommandSourceStack; +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.plugin.java.JavaPlugin; +import org.jetbrains.annotations.Nullable; + +import java.lang.invoke.MethodHandle; +import java.lang.invoke.MethodHandles; +import java.lang.invoke.MethodType; +import java.util.Collection; +import java.util.Map; +import java.util.Set; +import java.util.concurrent.ConcurrentHashMap; + +/** + * 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 RuntimeCommandRegistrar { + + /** + * + * @param node The command node to register. + * @param aliases Aliases used to reference the command. Can be empty. + * @param description String describing the command. + */ + public record CommandRegistration( + LiteralCommandNode node, + Collection aliases, + @Nullable String description + ) { } + + private static JavaPlugin plugin; + + private static Commands commandRegistrar; + private static final Map> REGISTERED_COMMANDS = new ConcurrentHashMap<>(); + private static final Set PENDING_REGISTRATION = ConcurrentHashMap.newKeySet(); + private static final Set PENDING_UNREGISTRATION = ConcurrentHashMap.newKeySet(); + + 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 boolean useSafeReload; + + public static void init(JavaPlugin plugin) { + RuntimeCommandRegistrar.plugin = plugin; + plugin.getLifecycleManager().registerEventHandler(LifecycleEvents.COMMANDS, commands -> { + commandRegistrar = commands.registrar(); + + if (!REGISTERED_COMMANDS.isEmpty() || !PENDING_REGISTRATION.isEmpty()) { + REGISTERED_COMMANDS.replaceAll((command, ignored) -> + commandRegistrar.register(command.node, command.description, command.aliases)); + for (CommandRegistration command : PENDING_REGISTRATION) { + REGISTERED_COMMANDS.put(command, commandRegistrar.register(command.node, command.description, command.aliases)); + } + PENDING_REGISTRATION.clear(); + return; + } + + 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; + } + }); + } + + /** + * 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(CommandRegistration command) { + PENDING_REGISTRATION.add(command); + } + + /** + * Processes all pending registrations, synchronizing them with the server's command dispatcher. + * @see #register(CommandRegistration) + */ + public static void processRegistrations() { + if (!Bukkit.isPrimaryThread()) { + Bukkit.getScheduler().runTask(plugin, RuntimeCommandRegistrar::processRegistrations); + return; + } + + if (!PENDING_UNREGISTRATION.isEmpty()) { + processUnregistrations(); + } + + if (useSafeReload) { + Bukkit.reloadData(); + return; + } + assert SET_VALID != null && INVALIDATE != null && SYNC_COMMANDS != null; + + try { + SET_VALID.invoke(commandRegistrar); + PluginMeta pluginMeta = plugin.getPluginMeta(); + for (CommandRegistration command : PENDING_REGISTRATION) { + REGISTERED_COMMANDS.put(command, commandRegistrar.register(pluginMeta, command.node, command.description, command.aliases)); + } + INVALIDATE.invoke(commandRegistrar); + SYNC_COMMANDS.invoke(Bukkit.getServer()); + } catch (Throwable e) { + throw Skript.exception(e); + } + } + + /** + * 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(CommandRegistration command) { + PENDING_UNREGISTRATION.addAll(REGISTERED_COMMANDS.remove(command)); + } + + /** + * Processes all pending unregistrations, synchronizing them with the server's command dispatcher. + * @see #unregister(CommandRegistration) + */ + public static void processUnregistrations() { + if (!Bukkit.isPrimaryThread()) { + Bukkit.getScheduler().runTask(plugin, RuntimeCommandRegistrar::processUnregistrations); + return; + } + + if (useSafeReload) { + PENDING_UNREGISTRATION.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_UNREGISTRATION) { + REMOVE_COMMAND.invoke(root, command); + } + PENDING_UNREGISTRATION.clear(); + INVALIDATE.invoke(commandRegistrar); + SYNC_COMMANDS.invoke(Bukkit.getServer()); + } catch (Throwable e) { + throw Skript.exception(e); + } + } + +} 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 index d2f4a12b792..acaffa1ee72 100644 --- 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 @@ -6,12 +6,12 @@ import ch.njol.skript.lang.SkriptParser.ParseResult; import com.mojang.brigadier.tree.LiteralCommandNode; import io.papermc.paper.command.brigadier.CommandSourceStack; -import io.papermc.paper.plugin.lifecycle.event.types.LifecycleEvents; -import org.bukkit.Bukkit; 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.brigadier.RuntimeCommandRegistrar; +import org.skriptlang.skript.bukkit.command.brigadier.RuntimeCommandRegistrar.CommandRegistration; import org.skriptlang.skript.bukkit.command.brigadier.ScriptCommandEvent; import org.skriptlang.skript.bukkit.command.elements.structures.util.SubCommandEntryData; import org.skriptlang.skript.bukkit.lang.eventvalue.EventValue; @@ -21,9 +21,6 @@ import org.skriptlang.skript.registration.SyntaxInfo; import org.skriptlang.skript.registration.SyntaxRegistry; -import java.util.List; -import java.util.Set; -import java.util.concurrent.ConcurrentHashMap; import java.util.concurrent.atomic.AtomicBoolean; public class StructCommand extends Structure { @@ -36,33 +33,13 @@ public static void register(SkriptAddon addon, SyntaxRegistry syntaxRegistry) { EventValueRegistry evRegistry = addon.registry(EventValueRegistry.class); evRegistry.register(EventValue.simple(ScriptCommandEvent.class, CommandSender.class, ScriptCommandEvent::getSender)); - - Skript.getInstance().getLifecycleManager().registerEventHandler(LifecycleEvents.COMMANDS,commands -> { - var registrar = commands.registrar(); - for (var registration : COMMANDS) { - registrar.register(registration.node, registration.description, registration.aliases); - } - }); } private static final SubCommandEntryData ROOT_ENTRY_DATA = new SubCommandEntryData("command", false, false); - private record CommandRegistration( - LiteralCommandNode node, - List aliases, - String description - ) { } - - private static final Set COMMANDS = ConcurrentHashMap.newKeySet(); private static final AtomicBoolean SYNC_COMMANDS = new AtomicBoolean(); - private static void performSync() { - if (SYNC_COMMANDS.getAndSet(false)) { - Bukkit.reloadData(); - } - } - private SectionNode rootNode; private CommandRegistration command; @@ -82,10 +59,10 @@ public boolean load() { Skript.error("A command must have a name."); return false; } - this.command = new CommandRegistration(node, result.aliases(), result.description()); + command = new CommandRegistration(node, result.aliases(), result.description()); // TODO validate whether command already exists - COMMANDS.add(this.command); + RuntimeCommandRegistrar.register(command); SYNC_COMMANDS.set(true); return true; @@ -93,19 +70,23 @@ public boolean load() { @Override public boolean postLoad() { - performSync(); + if (SYNC_COMMANDS.compareAndSet(true, false)) { + RuntimeCommandRegistrar.processRegistrations(); + } return true; } @Override public void unload() { - COMMANDS.remove(command); + RuntimeCommandRegistrar.unregister(command); SYNC_COMMANDS.set(true); } @Override public void postUnload() { - performSync(); + if (SYNC_COMMANDS.compareAndSet(true, false)) { + RuntimeCommandRegistrar.processUnregistrations(); + } } @Override 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 index 6e83dc65e33..e382744e010 100644 --- 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 @@ -55,8 +55,8 @@ public class SubCommandEntryData extends EntryData { public record Result( List> arguments, - List aliases, - String description + Collection aliases, + @Nullable String description ) { } private static final Predicate TRUE_PREDICATE = ignored -> true; @@ -81,7 +81,7 @@ protected List getValue(String value) { return aliases; } }) - .addEntry("description", "", true) + .addEntry("description", null, true) .addEntry("permission", null, true) .addEntryData(new KeyValueEntryData("executable by", null, true) { private final Pattern pattern = Pattern.compile("\\s*,\\s*|\\s+(and|or)\\s+"); @@ -182,8 +182,8 @@ public SubCommandEntryData(String key, boolean optional, boolean multiple) { } // command description - String description = entryContainer.get("description", String.class, true); - if (!isRoot && !description.isEmpty()) { + 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; } From 6e6d4892196537f8eae3b11dfb7f5a011898f862 Mon Sep 17 00:00:00 2001 From: Patrick Miller Date: Thu, 2 Jul 2026 13:37:09 -0400 Subject: [PATCH 20/41] Prevent legacy command registration --- src/main/java/ch/njol/skript/structures/StructCommand.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/main/java/ch/njol/skript/structures/StructCommand.java b/src/main/java/ch/njol/skript/structures/StructCommand.java index 7dde39e5047..e9349b31e32 100644 --- a/src/main/java/ch/njol/skript/structures/StructCommand.java +++ b/src/main/java/ch/njol/skript/structures/StructCommand.java @@ -74,7 +74,7 @@ public class StructCommand extends Structure { private static final AtomicBoolean SYNC_COMMANDS = new AtomicBoolean(); - static { + private static void register() { // don't register for now Skript.registerStructure( StructCommand.class, EntryValidator.builder() From a275c4c28b308f87763faf6aa6ddbc82b41657e0 Mon Sep 17 00:00:00 2001 From: Patrick Miller Date: Fri, 3 Jul 2026 00:27:20 -0400 Subject: [PATCH 21/41] Add questionable "prefix" entry compatibility --- .../brigadier/RuntimeCommandRegistrar.java | 77 ++++++++++++++----- .../elements/structures/StructCommand.java | 2 +- .../structures/util/SubCommandEntryData.java | 17 ++-- 3 files changed, 70 insertions(+), 26 deletions(-) diff --git a/src/main/java/org/skriptlang/skript/bukkit/command/brigadier/RuntimeCommandRegistrar.java b/src/main/java/org/skriptlang/skript/bukkit/command/brigadier/RuntimeCommandRegistrar.java index 11fb5fcb32a..0400ceab79b 100644 --- a/src/main/java/org/skriptlang/skript/bukkit/command/brigadier/RuntimeCommandRegistrar.java +++ b/src/main/java/org/skriptlang/skript/bukkit/command/brigadier/RuntimeCommandRegistrar.java @@ -13,7 +13,11 @@ 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.Collection; +import java.util.Locale; import java.util.Map; import java.util.Set; import java.util.concurrent.ConcurrentHashMap; @@ -34,15 +38,16 @@ public final class RuntimeCommandRegistrar { public record CommandRegistration( LiteralCommandNode node, Collection aliases, - @Nullable String description + @Nullable String description, + @Nullable String namespace ) { } private static JavaPlugin plugin; private static Commands commandRegistrar; private static final Map> REGISTERED_COMMANDS = new ConcurrentHashMap<>(); - private static final Set PENDING_REGISTRATION = ConcurrentHashMap.newKeySet(); - private static final Set PENDING_UNREGISTRATION = ConcurrentHashMap.newKeySet(); + private static final Set PENDING_REGISTRATIONS = ConcurrentHashMap.newKeySet(); + private static final Set PENDING_UNREGISTRATIONS = ConcurrentHashMap.newKeySet(); private static @Nullable MethodHandle SET_VALID; private static @Nullable MethodHandle INVALIDATE; @@ -56,13 +61,10 @@ public static void init(JavaPlugin plugin) { plugin.getLifecycleManager().registerEventHandler(LifecycleEvents.COMMANDS, commands -> { commandRegistrar = commands.registrar(); - if (!REGISTERED_COMMANDS.isEmpty() || !PENDING_REGISTRATION.isEmpty()) { + if (!REGISTERED_COMMANDS.isEmpty() || !PENDING_REGISTRATIONS.isEmpty()) { REGISTERED_COMMANDS.replaceAll((command, ignored) -> commandRegistrar.register(command.node, command.description, command.aliases)); - for (CommandRegistration command : PENDING_REGISTRATION) { - REGISTERED_COMMANDS.put(command, commandRegistrar.register(command.node, command.description, command.aliases)); - } - PENDING_REGISTRATION.clear(); + processRegistrationSet(); return; } @@ -88,7 +90,7 @@ public static void init(JavaPlugin plugin) { * @see #processRegistrations() */ public static void register(CommandRegistration command) { - PENDING_REGISTRATION.add(command); + PENDING_REGISTRATIONS.add(command); } /** @@ -101,7 +103,7 @@ public static void processRegistrations() { return; } - if (!PENDING_UNREGISTRATION.isEmpty()) { + if (!PENDING_UNREGISTRATIONS.isEmpty()) { processUnregistrations(); } @@ -113,10 +115,7 @@ public static void processRegistrations() { try { SET_VALID.invoke(commandRegistrar); - PluginMeta pluginMeta = plugin.getPluginMeta(); - for (CommandRegistration command : PENDING_REGISTRATION) { - REGISTERED_COMMANDS.put(command, commandRegistrar.register(pluginMeta, command.node, command.description, command.aliases)); - } + processRegistrationSet(); INVALIDATE.invoke(commandRegistrar); SYNC_COMMANDS.invoke(Bukkit.getServer()); } catch (Throwable e) { @@ -124,6 +123,48 @@ public static void processRegistrations() { } } + private static void processRegistrationSet() { + PluginMeta pluginMeta = plugin.getPluginMeta(); + for (CommandRegistration 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; + } + } + 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")) { + return name; + } else if (methodName.equals("namespace")) { + return name.toLowerCase(Locale.ROOT); + } + } + return method.invoke(source, args); + } + + } + /** * Adds the {@code command} to the unregistration queue. * To finalize unregistration, the queue must be processed using {@link #processUnregistrations()}. @@ -131,7 +172,7 @@ public static void processRegistrations() { * @see #processUnregistrations() */ public static void unregister(CommandRegistration command) { - PENDING_UNREGISTRATION.addAll(REGISTERED_COMMANDS.remove(command)); + PENDING_UNREGISTRATIONS.addAll(REGISTERED_COMMANDS.remove(command)); } /** @@ -145,7 +186,7 @@ public static void processUnregistrations() { } if (useSafeReload) { - PENDING_UNREGISTRATION.clear(); + PENDING_UNREGISTRATIONS.clear(); Bukkit.reloadData(); return; } @@ -154,10 +195,10 @@ public static void processUnregistrations() { try { SET_VALID.invoke(commandRegistrar); var root = commandRegistrar.getDispatcher().getRoot(); - for (String command : PENDING_UNREGISTRATION) { + for (String command : PENDING_UNREGISTRATIONS) { REMOVE_COMMAND.invoke(root, command); } - PENDING_UNREGISTRATION.clear(); + PENDING_UNREGISTRATIONS.clear(); INVALIDATE.invoke(commandRegistrar); SYNC_COMMANDS.invoke(Bukkit.getServer()); } catch (Throwable e) { 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 index acaffa1ee72..37bd9dc6923 100644 --- 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 @@ -59,7 +59,7 @@ public boolean load() { Skript.error("A command must have a name."); return false; } - command = new CommandRegistration(node, result.aliases(), result.description()); + command = new CommandRegistration(node, result.aliases(), result.description(), result.prefix()); // TODO validate whether command already exists RuntimeCommandRegistrar.register(command); 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 index e382744e010..51a056ee6bf 100644 --- 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 @@ -56,7 +56,8 @@ public class SubCommandEntryData extends EntryData { public record Result( List> arguments, Collection aliases, - @Nullable String description + @Nullable String description, + @Nullable String prefix ) { } private static final Predicate TRUE_PREDICATE = ignored -> true; @@ -82,6 +83,7 @@ protected List getValue(String value) { } }) .addEntry("description", null, true) + .addEntry("prefix", null, true) .addEntry("permission", null, true) .addEntryData(new KeyValueEntryData("executable by", null, true) { private final Pattern pattern = Pattern.compile("\\s*,\\s*|\\s+(and|or)\\s+"); @@ -110,7 +112,6 @@ protected ExecutableBy getValue(String value) { .addEntryData(new SubCommandEntryData("subcommand", true, true)) // deprecated entries .addEntry("usage", null, true) - .addEntry("prefix", null, true) .addEntry("permission message", null, true) .build(); @@ -188,6 +189,12 @@ public SubCommandEntryData(String key, boolean optional, boolean multiple) { return null; } + String prefix = entryContainer.getOptional("prefix", String.class, false); + if (!isRoot && prefix != null) { + Skript.error("Only the root of a command may have a prefix."); + return null; + } + // command requirements Predicate requires = TRUE_PREDICATE; @@ -255,10 +262,6 @@ public SubCommandEntryData(String key, boolean optional, boolean multiple) { ScriptWarning.printDeprecationWarning("The 'usage' entry has been deprecated for removal in a future release." + " Incorrect command usage is now handled by the client's command validator."); } - if (entryContainer.hasEntry("prefix")) { - ScriptWarning.printDeprecationWarning("The 'prefix' entry has been deprecated for removal in a future release." + - " It is no longer possible to modify the prefix (namespace) of a command."); - } 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."); @@ -319,7 +322,7 @@ public SubCommandEntryData(String key, boolean optional, boolean multiple) { parsingData.popArguments(); //noinspection unchecked - return new Result(result, aliases, description); + return new Result(result, aliases, description, prefix); } @Override From 3e49a44d55a392841752fbda07688fee4eb5c0a1 Mon Sep 17 00:00:00 2001 From: Patrick Miller Date: Fri, 3 Jul 2026 00:37:31 -0400 Subject: [PATCH 22/41] Ensure other test commands run --- .../tests/bukkit/command/StructCommand.sk | 20 +++++++++---------- 1 file changed, 10 insertions(+), 10 deletions(-) diff --git a/src/test/skript/tests/bukkit/command/StructCommand.sk b/src/test/skript/tests/bukkit/command/StructCommand.sk index 6e813050146..1d98437238f 100644 --- a/src/test/skript/tests/bukkit/command/StructCommand.sk +++ b/src/test/skript/tests/bukkit/command/StructCommand.sk @@ -49,18 +49,13 @@ command /test-args-plural : 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}%')" + 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 {_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}%')" + set {-test-somecommand::arg1} to arg-1 # see https://github.com/SkriptLang/Skript/pull/6286 command /commandtest : @@ -86,5 +81,10 @@ test "StructCommand": 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 "skriptcommand taco" + 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" + execute command "//somecommand burrito is tasty" + assert {-test-somecommand::arg1} is "burrito is tasty" with "arg-1 is 'burrito is tasty' test failed" From f889689769e411d5e71bba7aa08740ac68606200 Mon Sep 17 00:00:00 2001 From: Patrick Miller Date: Fri, 3 Jul 2026 00:59:37 -0400 Subject: [PATCH 23/41] Fix double slash execution on older versions --- src/test/skript/tests/bukkit/command/StructCommand.sk | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/src/test/skript/tests/bukkit/command/StructCommand.sk b/src/test/skript/tests/bukkit/command/StructCommand.sk index 1d98437238f..b2433de8c8b 100644 --- a/src/test/skript/tests/bukkit/command/StructCommand.sk +++ b/src/test/skript/tests/bukkit/command/StructCommand.sk @@ -86,5 +86,7 @@ test "StructCommand": 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" - execute command "//somecommand burrito is tasty" + # 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" From 014cb219925f60fb0e32af2b3b0af29a1f0dc1a5 Mon Sep 17 00:00:00 2001 From: Patrick Miller Date: Fri, 3 Jul 2026 01:09:42 -0400 Subject: [PATCH 24/41] Improve prefix validation --- .../brigadier/RuntimeCommandRegistrar.java | 5 +---- .../structures/util/SubCommandEntryData.java | 17 ++++++++++++++--- 2 files changed, 15 insertions(+), 7 deletions(-) diff --git a/src/main/java/org/skriptlang/skript/bukkit/command/brigadier/RuntimeCommandRegistrar.java b/src/main/java/org/skriptlang/skript/bukkit/command/brigadier/RuntimeCommandRegistrar.java index 0400ceab79b..8d37e333f10 100644 --- a/src/main/java/org/skriptlang/skript/bukkit/command/brigadier/RuntimeCommandRegistrar.java +++ b/src/main/java/org/skriptlang/skript/bukkit/command/brigadier/RuntimeCommandRegistrar.java @@ -17,7 +17,6 @@ import java.lang.reflect.Method; import java.lang.reflect.Proxy; import java.util.Collection; -import java.util.Locale; import java.util.Map; import java.util.Set; import java.util.concurrent.ConcurrentHashMap; @@ -154,10 +153,8 @@ public TemporaryNamePluginMeta(PluginMeta source, String name) { public Object invoke(Object proxy, Method method, Object[] args) throws Throwable { if (useAlternativeName) { String methodName = method.getName(); - if (methodName.equals("getName")) { + if (methodName.equals("getName") || methodName.equals("namespace")) { return name; - } else if (methodName.equals("namespace")) { - return name.toLowerCase(Locale.ROOT); } } return method.invoke(source, args); 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 index 51a056ee6bf..25341ea1388 100644 --- 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 @@ -190,9 +190,20 @@ public SubCommandEntryData(String key, boolean optional, boolean multiple) { } String prefix = entryContainer.getOptional("prefix", String.class, false); - if (!isRoot && prefix != null) { - Skript.error("Only the root of a command may have a prefix."); - return null; + 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 From 868875b4239a5da6b77fa03422ace99e5a25b800 Mon Sep 17 00:00:00 2001 From: Patrick Miller Date: Fri, 3 Jul 2026 02:10:52 -0400 Subject: [PATCH 25/41] Improve registered command organization --- .../skript/bukkit/command/CommandModule.java | 4 +- .../brigadier/SkriptBrigadierCommand.java | 24 ++++++++ ...strar.java => SkriptCommandRegistrar.java} | 61 +++++++++---------- .../elements/structures/StructCommand.java | 27 +++++--- 4 files changed, 73 insertions(+), 43 deletions(-) create mode 100644 src/main/java/org/skriptlang/skript/bukkit/command/brigadier/SkriptBrigadierCommand.java rename src/main/java/org/skriptlang/skript/bukkit/command/brigadier/{RuntimeCommandRegistrar.java => SkriptCommandRegistrar.java} (78%) diff --git a/src/main/java/org/skriptlang/skript/bukkit/command/CommandModule.java b/src/main/java/org/skriptlang/skript/bukkit/command/CommandModule.java index 2e2f8615141..5b9867dce7c 100644 --- a/src/main/java/org/skriptlang/skript/bukkit/command/CommandModule.java +++ b/src/main/java/org/skriptlang/skript/bukkit/command/CommandModule.java @@ -4,7 +4,7 @@ import org.skriptlang.skript.addon.AddonModule; import org.skriptlang.skript.addon.HierarchicalAddonModule; import org.skriptlang.skript.addon.SkriptAddon; -import org.skriptlang.skript.bukkit.command.brigadier.RuntimeCommandRegistrar; +import org.skriptlang.skript.bukkit.command.brigadier.SkriptCommandRegistrar; import org.skriptlang.skript.bukkit.command.elements.effects.EffCancelCooldown; import org.skriptlang.skript.bukkit.command.elements.expressions.ExprArgument; import org.skriptlang.skript.bukkit.command.elements.expressions.ExprCmdCooldownInfo; @@ -18,7 +18,7 @@ public CommandModule(AddonModule parentModule) { @Override protected void loadSelf(SkriptAddon addon) { - RuntimeCommandRegistrar.init(Skript.getInstance()); + SkriptCommandRegistrar.init(Skript.getInstance()); register(addon, EffCancelCooldown::register, diff --git a/src/main/java/org/skriptlang/skript/bukkit/command/brigadier/SkriptBrigadierCommand.java b/src/main/java/org/skriptlang/skript/bukkit/command/brigadier/SkriptBrigadierCommand.java new file mode 100644 index 00000000000..3023c1162c4 --- /dev/null +++ b/src/main/java/org/skriptlang/skript/bukkit/command/brigadier/SkriptBrigadierCommand.java @@ -0,0 +1,24 @@ +package org.skriptlang.skript.bukkit.command.brigadier; + +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 SkriptBrigadierCommand( + Script script, + LiteralCommandNode node, + Collection aliases, + @Nullable String description, + @Nullable String namespace +) { } diff --git a/src/main/java/org/skriptlang/skript/bukkit/command/brigadier/RuntimeCommandRegistrar.java b/src/main/java/org/skriptlang/skript/bukkit/command/brigadier/SkriptCommandRegistrar.java similarity index 78% rename from src/main/java/org/skriptlang/skript/bukkit/command/brigadier/RuntimeCommandRegistrar.java rename to src/main/java/org/skriptlang/skript/bukkit/command/brigadier/SkriptCommandRegistrar.java index 8d37e333f10..dc5d5b406c2 100644 --- a/src/main/java/org/skriptlang/skript/bukkit/command/brigadier/RuntimeCommandRegistrar.java +++ b/src/main/java/org/skriptlang/skript/bukkit/command/brigadier/SkriptCommandRegistrar.java @@ -1,8 +1,6 @@ package org.skriptlang.skript.bukkit.command.brigadier; import ch.njol.skript.Skript; -import com.mojang.brigadier.tree.LiteralCommandNode; -import io.papermc.paper.command.brigadier.CommandSourceStack; import io.papermc.paper.command.brigadier.Commands; import io.papermc.paper.plugin.configuration.PluginMeta; import io.papermc.paper.plugin.lifecycle.event.types.LifecycleEvents; @@ -16,36 +14,23 @@ import java.lang.reflect.InvocationHandler; import java.lang.reflect.Method; import java.lang.reflect.Proxy; -import java.util.Collection; import java.util.Map; import java.util.Set; 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 RuntimeCommandRegistrar { - - /** - * - * @param node The command node to register. - * @param aliases Aliases used to reference the command. Can be empty. - * @param description String describing the command. - */ - public record CommandRegistration( - LiteralCommandNode node, - Collection aliases, - @Nullable String description, - @Nullable String namespace - ) { } +public final class SkriptCommandRegistrar { 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 Map> REGISTERED_COMMANDS = new ConcurrentHashMap<>(); + private static final Set PENDING_REGISTRATIONS = ConcurrentHashMap.newKeySet(); private static final Set PENDING_UNREGISTRATIONS = ConcurrentHashMap.newKeySet(); private static @Nullable MethodHandle SET_VALID; @@ -56,13 +41,13 @@ public record CommandRegistration( private static boolean useSafeReload; public static void init(JavaPlugin plugin) { - RuntimeCommandRegistrar.plugin = plugin; + SkriptCommandRegistrar.plugin = 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)); + commandRegistrar.register(command.node(), command.description(), command.aliases())); processRegistrationSet(); return; } @@ -88,17 +73,17 @@ public static void init(JavaPlugin plugin) { * @param command The command to register. * @see #processRegistrations() */ - public static void register(CommandRegistration command) { + public static void register(SkriptBrigadierCommand command) { PENDING_REGISTRATIONS.add(command); } /** * Processes all pending registrations, synchronizing them with the server's command dispatcher. - * @see #register(CommandRegistration) + * @see #register(SkriptBrigadierCommand) */ public static void processRegistrations() { if (!Bukkit.isPrimaryThread()) { - Bukkit.getScheduler().runTask(plugin, RuntimeCommandRegistrar::processRegistrations); + Bukkit.getScheduler().runTask(plugin, SkriptCommandRegistrar::processRegistrations); return; } @@ -124,14 +109,14 @@ public static void processRegistrations() { private static void processRegistrationSet() { PluginMeta pluginMeta = plugin.getPluginMeta(); - for (CommandRegistration command : PENDING_REGISTRATIONS) { - if (command.namespace == null) { - REGISTERED_COMMANDS.put(command, commandRegistrar.register(pluginMeta, command.node, command.description, command.aliases)); + for (SkriptBrigadierCommand 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); + 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)); + REGISTERED_COMMANDS.put(command, commandRegistrar.register(meta, command.node(), command.description(), command.aliases())); handler.useAlternativeName = false; } } @@ -168,17 +153,17 @@ public Object invoke(Object proxy, Method method, Object[] args) throws Throwabl * @param command The command to unregister. * @see #processUnregistrations() */ - public static void unregister(CommandRegistration command) { + public static void unregister(SkriptBrigadierCommand command) { PENDING_UNREGISTRATIONS.addAll(REGISTERED_COMMANDS.remove(command)); } /** * Processes all pending unregistrations, synchronizing them with the server's command dispatcher. - * @see #unregister(CommandRegistration) + * @see #unregister(SkriptBrigadierCommand) */ public static void processUnregistrations() { if (!Bukkit.isPrimaryThread()) { - Bukkit.getScheduler().runTask(plugin, RuntimeCommandRegistrar::processUnregistrations); + Bukkit.getScheduler().runTask(plugin, SkriptCommandRegistrar::processUnregistrations); return; } @@ -203,4 +188,16 @@ public static void processUnregistrations() { } } + /** + * 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 SkriptBrigadierCommand getCommand(String command) { + return Stream.concat(PENDING_REGISTRATIONS.stream(), REGISTERED_COMMANDS.keySet().stream()) + .filter(registration -> registration.node().getLiteral().equals(command)) + .findFirst() + .orElse(null); + } + } 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 index 37bd9dc6923..bf886b800da 100644 --- 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 @@ -10,8 +10,8 @@ import org.bukkit.event.Event; import org.jetbrains.annotations.Nullable; import org.skriptlang.skript.addon.SkriptAddon; -import org.skriptlang.skript.bukkit.command.brigadier.RuntimeCommandRegistrar; -import org.skriptlang.skript.bukkit.command.brigadier.RuntimeCommandRegistrar.CommandRegistration; +import org.skriptlang.skript.bukkit.command.brigadier.SkriptCommandRegistrar; +import org.skriptlang.skript.bukkit.command.brigadier.SkriptBrigadierCommand; import org.skriptlang.skript.bukkit.command.brigadier.ScriptCommandEvent; import org.skriptlang.skript.bukkit.command.elements.structures.util.SubCommandEntryData; import org.skriptlang.skript.bukkit.lang.eventvalue.EventValue; @@ -41,7 +41,7 @@ public static void register(SkriptAddon addon, SyntaxRegistry syntaxRegistry) { private static final AtomicBoolean SYNC_COMMANDS = new AtomicBoolean(); private SectionNode rootNode; - private CommandRegistration command; + private SkriptBrigadierCommand command; @Override public boolean init(Literal[] args, int matchedPattern, ParseResult parseResult, EntryContainer entryContainer) { @@ -51,6 +51,7 @@ public boolean init(Literal[] args, int matchedPattern, ParseResult parseResu @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; @@ -59,10 +60,18 @@ public boolean load() { Skript.error("A command must have a name."); return false; } - command = new CommandRegistration(node, result.aliases(), result.description(), result.prefix()); - // TODO validate whether command already exists - RuntimeCommandRegistrar.register(command); + // validation + SkriptBrigadierCommand existing = SkriptCommandRegistrar.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 SkriptBrigadierCommand(getParser().getCurrentScript(), node, result.aliases(), result.description(), result.prefix()); + SkriptCommandRegistrar.register(command); SYNC_COMMANDS.set(true); return true; @@ -71,21 +80,21 @@ public boolean load() { @Override public boolean postLoad() { if (SYNC_COMMANDS.compareAndSet(true, false)) { - RuntimeCommandRegistrar.processRegistrations(); + SkriptCommandRegistrar.processRegistrations(); } return true; } @Override public void unload() { - RuntimeCommandRegistrar.unregister(command); + SkriptCommandRegistrar.unregister(command); SYNC_COMMANDS.set(true); } @Override public void postUnload() { if (SYNC_COMMANDS.compareAndSet(true, false)) { - RuntimeCommandRegistrar.processUnregistrations(); + SkriptCommandRegistrar.processUnregistrations(); } } From 6e74508df847500ba27001a74c8503109e16872f Mon Sep 17 00:00:00 2001 From: Patrick Miller Date: Fri, 3 Jul 2026 15:46:39 -0400 Subject: [PATCH 26/41] Cleanup and tweaks --- .../skript/bukkit/command/CommandModule.java | 4 +- .../command/brigadier/ExecutorData.java | 285 ------------------ .../{brigadier => custom}/ArgumentData.java | 2 +- .../CommandParsingData.java | 12 +- .../command/custom/CooldownManager.java | 202 +++++++++++++ .../bukkit/command/custom/ExecutableBy.java | 82 +++++ .../ScriptArgumentType.java} | 46 +-- .../ScriptBrigadierCommand.java} | 4 +- .../ScriptCommandEvent.java | 8 +- .../ScriptCommandExecutor.java} | 24 +- .../ScriptCommandRegistrar.java} | 26 +- .../elements/effects/EffCancelCooldown.java | 6 +- .../elements/expressions/ExprArgument.java | 6 +- .../expressions/ExprCmdCooldownInfo.java | 8 +- .../elements/structures/StructCommand.java | 20 +- .../structures/util}/CommandCompiler.java | 7 +- .../structures/util/SubCommandEntryData.java | 42 ++- 17 files changed, 395 insertions(+), 389 deletions(-) delete mode 100644 src/main/java/org/skriptlang/skript/bukkit/command/brigadier/ExecutorData.java rename src/main/java/org/skriptlang/skript/bukkit/command/{brigadier => custom}/ArgumentData.java (91%) rename src/main/java/org/skriptlang/skript/bukkit/command/{brigadier => custom}/CommandParsingData.java (85%) create mode 100644 src/main/java/org/skriptlang/skript/bukkit/command/custom/CooldownManager.java create mode 100644 src/main/java/org/skriptlang/skript/bukkit/command/custom/ExecutableBy.java rename src/main/java/org/skriptlang/skript/bukkit/command/{brigadier/SkriptBrigadierArgument.java => custom/ScriptArgumentType.java} (77%) rename src/main/java/org/skriptlang/skript/bukkit/command/{brigadier/SkriptBrigadierCommand.java => custom/ScriptBrigadierCommand.java} (89%) rename src/main/java/org/skriptlang/skript/bukkit/command/{brigadier => custom}/ScriptCommandEvent.java (79%) rename src/main/java/org/skriptlang/skript/bukkit/command/{brigadier/SkriptCommandExecutor.java => custom/ScriptCommandExecutor.java} (82%) rename src/main/java/org/skriptlang/skript/bukkit/command/{brigadier/SkriptCommandRegistrar.java => custom/ScriptCommandRegistrar.java} (89%) rename src/main/java/org/skriptlang/skript/bukkit/command/{brigadier => elements/structures/util}/CommandCompiler.java (98%) diff --git a/src/main/java/org/skriptlang/skript/bukkit/command/CommandModule.java b/src/main/java/org/skriptlang/skript/bukkit/command/CommandModule.java index 5b9867dce7c..0473042f69b 100644 --- a/src/main/java/org/skriptlang/skript/bukkit/command/CommandModule.java +++ b/src/main/java/org/skriptlang/skript/bukkit/command/CommandModule.java @@ -4,7 +4,7 @@ import org.skriptlang.skript.addon.AddonModule; import org.skriptlang.skript.addon.HierarchicalAddonModule; import org.skriptlang.skript.addon.SkriptAddon; -import org.skriptlang.skript.bukkit.command.brigadier.SkriptCommandRegistrar; +import org.skriptlang.skript.bukkit.command.custom.ScriptCommandRegistrar; import org.skriptlang.skript.bukkit.command.elements.effects.EffCancelCooldown; import org.skriptlang.skript.bukkit.command.elements.expressions.ExprArgument; import org.skriptlang.skript.bukkit.command.elements.expressions.ExprCmdCooldownInfo; @@ -18,7 +18,7 @@ public CommandModule(AddonModule parentModule) { @Override protected void loadSelf(SkriptAddon addon) { - SkriptCommandRegistrar.init(Skript.getInstance()); + ScriptCommandRegistrar.init(Skript.getInstance()); register(addon, EffCancelCooldown::register, diff --git a/src/main/java/org/skriptlang/skript/bukkit/command/brigadier/ExecutorData.java b/src/main/java/org/skriptlang/skript/bukkit/command/brigadier/ExecutorData.java deleted file mode 100644 index 5e46b64af7a..00000000000 --- a/src/main/java/org/skriptlang/skript/bukkit/command/brigadier/ExecutorData.java +++ /dev/null @@ -1,285 +0,0 @@ -package org.skriptlang.skript.bukkit.command.brigadier; - -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.command.ConsoleCommandSender; -import org.bukkit.command.RemoteConsoleCommandSender; -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; -import java.util.function.Predicate; - -/** - * @param executableBy Describes what kind of {@link CommandSender} can execute the command. - * @param cooldownManager Handles cooldown management for cooldown command entries. - */ -public record ExecutorData( - @Nullable ExecutableBy executableBy, - @Nullable CooldownManager cooldownManager -) { - - /** - * 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 console. - */ - CONSOLE(sender -> sender instanceof ConsoleCommandSender || sender instanceof RemoteConsoleCommandSender), - - /** - * This command is executable by any {@link CommandSender}. - */ - ALL(ignored -> true), - - /** - * This command is not executable. - */ - NONE(ignored -> false); - - 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; - } - - /** - * Combines (ORs) this ExecutableBy with another. - * For example, {@link #PLAYERS}.with({@link #CONSOLE}) is {@link #ALL}. - * @param other Other to OR with. - * @return The resulting ExecutableBy. - */ - public ExecutableBy with(ExecutableBy other) { - if (this == NONE) { - return other; - } else if (other == NONE) { - return this; - } - return this == other ? this : ALL; - } - - /** - * @param other Subset ExecutableBy to consider. - * @return Whether this ExecutableBy is the same as or covers a superset of {@code other}. - */ - public boolean includes(ExecutableBy other) { - return this == ALL || this == other; - } - - @Override - public String toString() { - return switch (this) { - case PLAYERS -> "players"; - case CONSOLE -> "the console"; - case ALL -> "the console and players"; - case NONE -> "none"; - }; - } - - } - - /** - * Used to manage cooldowns for an individual command. - */ - public static 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/brigadier/ArgumentData.java b/src/main/java/org/skriptlang/skript/bukkit/command/custom/ArgumentData.java similarity index 91% rename from src/main/java/org/skriptlang/skript/bukkit/command/brigadier/ArgumentData.java rename to src/main/java/org/skriptlang/skript/bukkit/command/custom/ArgumentData.java index 3f266991862..bcd44feda74 100644 --- a/src/main/java/org/skriptlang/skript/bukkit/command/brigadier/ArgumentData.java +++ b/src/main/java/org/skriptlang/skript/bukkit/command/custom/ArgumentData.java @@ -1,4 +1,4 @@ -package org.skriptlang.skript.bukkit.command.brigadier; +package org.skriptlang.skript.bukkit.command.custom; import ch.njol.skript.classes.ClassInfo; import ch.njol.skript.lang.Expression; diff --git a/src/main/java/org/skriptlang/skript/bukkit/command/brigadier/CommandParsingData.java b/src/main/java/org/skriptlang/skript/bukkit/command/custom/CommandParsingData.java similarity index 85% rename from src/main/java/org/skriptlang/skript/bukkit/command/brigadier/CommandParsingData.java rename to src/main/java/org/skriptlang/skript/bukkit/command/custom/CommandParsingData.java index 137acf62ad9..7420a6b4e6c 100644 --- a/src/main/java/org/skriptlang/skript/bukkit/command/brigadier/CommandParsingData.java +++ b/src/main/java/org/skriptlang/skript/bukkit/command/custom/CommandParsingData.java @@ -1,7 +1,8 @@ -package org.skriptlang.skript.bukkit.command.brigadier; +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; @@ -15,6 +16,15 @@ */ public final class CommandParsingData extends Data { + /** + * @param executableBy Describes what kind of {@link CommandSender} can execute the command. + * @param cooldownManager Handles cooldown management for cooldown command entries. + */ + public record ExecutorData( + @Nullable ExecutableBy executableBy, + @Nullable CooldownManager cooldownManager + ) { } + private final LinkedList> arguments = new LinkedList<>(); private final Deque indices = new ArrayDeque<>(4); 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..f14201d955f --- /dev/null +++ b/src/main/java/org/skriptlang/skript/bukkit/command/custom/ExecutableBy.java @@ -0,0 +1,82 @@ +package org.skriptlang.skript.bukkit.command.custom; + +import org.bukkit.command.CommandSender; +import org.bukkit.command.ConsoleCommandSender; +import org.bukkit.command.RemoteConsoleCommandSender; +import org.bukkit.entity.Player; + +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 console. + */ + CONSOLE(sender -> sender instanceof ConsoleCommandSender || sender instanceof RemoteConsoleCommandSender), + + /** + * This command is executable by any {@link CommandSender}. + */ + ALL(ignored -> true), + + /** + * This command is not executable. + */ + NONE(ignored -> false); + + 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; + } + + /** + * Combines (ORs) this ExecutableBy with another. + * For example, {@link #PLAYERS}.with({@link #CONSOLE}) is {@link #ALL}. + * + * @param other Other to OR with. + * @return The resulting ExecutableBy. + */ + public ExecutableBy with(ExecutableBy other) { + if (this == NONE) { + return other; + } else if (other == NONE) { + return this; + } + return this == other ? this : ALL; + } + + /** + * @param other Subset ExecutableBy to consider. + * @return Whether this ExecutableBy is the same as or covers a superset of {@code other}. + */ + public boolean includes(ExecutableBy other) { + return this == ALL || this == other; + } + + @Override + public String toString() { + return switch (this) { + case PLAYERS -> "players"; + case CONSOLE -> "the console"; + case ALL -> "the console and players"; + case NONE -> "none"; + }; + } + +} diff --git a/src/main/java/org/skriptlang/skript/bukkit/command/brigadier/SkriptBrigadierArgument.java b/src/main/java/org/skriptlang/skript/bukkit/command/custom/ScriptArgumentType.java similarity index 77% rename from src/main/java/org/skriptlang/skript/bukkit/command/brigadier/SkriptBrigadierArgument.java rename to src/main/java/org/skriptlang/skript/bukkit/command/custom/ScriptArgumentType.java index 14bd31934ce..9179e62e74f 100644 --- a/src/main/java/org/skriptlang/skript/bukkit/command/brigadier/SkriptBrigadierArgument.java +++ b/src/main/java/org/skriptlang/skript/bukkit/command/custom/ScriptArgumentType.java @@ -1,8 +1,10 @@ -package org.skriptlang.skript.bukkit.command.brigadier; +package org.skriptlang.skript.bukkit.command.custom; +import ch.njol.skript.Skript; 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; @@ -13,6 +15,7 @@ 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; @@ -35,7 +38,7 @@ * @param The real type of the argument. */ @ApiStatus.Internal -public class SkriptBrigadierArgument implements CustomArgumentType.Converted { +public class ScriptArgumentType implements CustomArgumentType.Converted { /** * Pre-defined mappings of types that are acceptable to map to other native argument types. @@ -71,20 +74,16 @@ public class SkriptBrigadierArgument implements CustomArgumentType.Converted< Entity.class, data -> data.isSingle() ? ArgumentTypes.entity() : ArgumentTypes.entities() ); + 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))); - /** - * Placeholder result to be used for indicating that the default value of an argument should be resolved. - * Resolution of default values is only possible during general execution (after this argument is evaluated), - * as they may depend on the context of the command execution. - */ - public static final Object DEFAULT_VALUE_PLACEHOLDER = new Object(); - private final ArgumentData argument; private final StringArgumentType nativeType; - public SkriptBrigadierArgument(@NotNull ArgumentData argument, @NotNull StringArgumentType nativeType) { + public ScriptArgumentType(@NotNull ArgumentData argument, @NotNull StringArgumentType nativeType) { this.argument = argument; this.nativeType = nativeType; } @@ -94,19 +93,24 @@ public SkriptBrigadierArgument(@NotNull ArgumentData argument, @NotNull Strin assert argument.type().getParser() != null; input = input.replace('_', ' '); - //noinspection unchecked - Literal 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; - } - - if (result == null) { - if (argument.defaultValue() != null) { // use default value - return DEFAULT_VALUE_PLACEHOLDER; + 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()); + } } - throw ERROR_INVALID_INPUT.create(input, argument.type().getName().getSingular()); } + return argument.isSingle() ? result.getSingle() : result.getArray(); } diff --git a/src/main/java/org/skriptlang/skript/bukkit/command/brigadier/SkriptBrigadierCommand.java b/src/main/java/org/skriptlang/skript/bukkit/command/custom/ScriptBrigadierCommand.java similarity index 89% rename from src/main/java/org/skriptlang/skript/bukkit/command/brigadier/SkriptBrigadierCommand.java rename to src/main/java/org/skriptlang/skript/bukkit/command/custom/ScriptBrigadierCommand.java index 3023c1162c4..f794f125f44 100644 --- a/src/main/java/org/skriptlang/skript/bukkit/command/brigadier/SkriptBrigadierCommand.java +++ b/src/main/java/org/skriptlang/skript/bukkit/command/custom/ScriptBrigadierCommand.java @@ -1,4 +1,4 @@ -package org.skriptlang.skript.bukkit.command.brigadier; +package org.skriptlang.skript.bukkit.command.custom; import com.mojang.brigadier.tree.LiteralCommandNode; import io.papermc.paper.command.brigadier.CommandSourceStack; @@ -15,7 +15,7 @@ * @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 SkriptBrigadierCommand( +public record ScriptBrigadierCommand( Script script, LiteralCommandNode node, Collection aliases, diff --git a/src/main/java/org/skriptlang/skript/bukkit/command/brigadier/ScriptCommandEvent.java b/src/main/java/org/skriptlang/skript/bukkit/command/custom/ScriptCommandEvent.java similarity index 79% rename from src/main/java/org/skriptlang/skript/bukkit/command/brigadier/ScriptCommandEvent.java rename to src/main/java/org/skriptlang/skript/bukkit/command/custom/ScriptCommandEvent.java index c987b8e7072..68639e83fd3 100644 --- a/src/main/java/org/skriptlang/skript/bukkit/command/brigadier/ScriptCommandEvent.java +++ b/src/main/java/org/skriptlang/skript/bukkit/command/custom/ScriptCommandEvent.java @@ -1,4 +1,4 @@ -package org.skriptlang.skript.bukkit.command.brigadier; +package org.skriptlang.skript.bukkit.command.custom; import org.bukkit.command.CommandSender; import org.bukkit.event.Event; @@ -14,11 +14,11 @@ public class ScriptCommandEvent extends Event { private final CommandSender sender; - private final SkriptCommandExecutor executor; + private final ScriptCommandExecutor executor; final Map arguments = new HashMap<>(); - public ScriptCommandEvent(CommandSender sender, SkriptCommandExecutor executor) { + public ScriptCommandEvent(CommandSender sender, ScriptCommandExecutor executor) { this.sender = sender; this.executor = executor; } @@ -27,7 +27,7 @@ public CommandSender getSender() { return sender; } - public SkriptCommandExecutor getExecutor() { + public ScriptCommandExecutor getExecutor() { return executor; } diff --git a/src/main/java/org/skriptlang/skript/bukkit/command/brigadier/SkriptCommandExecutor.java b/src/main/java/org/skriptlang/skript/bukkit/command/custom/ScriptCommandExecutor.java similarity index 82% rename from src/main/java/org/skriptlang/skript/bukkit/command/brigadier/SkriptCommandExecutor.java rename to src/main/java/org/skriptlang/skript/bukkit/command/custom/ScriptCommandExecutor.java index 1c1ae159e65..eb8ae174e21 100644 --- a/src/main/java/org/skriptlang/skript/bukkit/command/brigadier/SkriptCommandExecutor.java +++ b/src/main/java/org/skriptlang/skript/bukkit/command/custom/ScriptCommandExecutor.java @@ -1,4 +1,4 @@ -package org.skriptlang.skript.bukkit.command.brigadier; +package org.skriptlang.skript.bukkit.command.custom; import ch.njol.skript.lang.Trigger; import ch.njol.skript.variables.Variables; @@ -12,7 +12,6 @@ import org.bukkit.event.Event; import org.jetbrains.annotations.ApiStatus; import org.jetbrains.annotations.Nullable; -import org.skriptlang.skript.bukkit.command.brigadier.ExecutorData.CooldownManager; import java.lang.reflect.Array; import java.util.List; @@ -24,7 +23,7 @@ * An executor for Brigadier commands. */ @ApiStatus.Internal -public class SkriptCommandExecutor { +public class ScriptCommandExecutor { private static void setVariable(String name, Object value, boolean isSingle, Event context) { if (isSingle) { @@ -42,7 +41,7 @@ private static void setVariable(String name, Object value, boolean isSingle, Eve private final List> arguments; private final @Nullable CooldownManager cooldownManager; - public SkriptCommandExecutor(Trigger trigger, List> arguments, @Nullable CooldownManager cooldownManager) { + public ScriptCommandExecutor(Trigger trigger, List> arguments, @Nullable CooldownManager cooldownManager) { this.trigger = trigger; this.arguments = arguments; this.cooldownManager = cooldownManager; @@ -56,10 +55,12 @@ public int execute(CommandContext context) throws CommandSyn CommandSender sender = context.getSource().getSender(); ScriptCommandEvent commandEvent = new ScriptCommandEvent(sender, this); + // final validations if (cooldownManager != null && !cooldownManager.checkExecution(commandEvent, sender)) { return Command.SINGLE_SUCCESS; } + // argument assembly Set providedArgs = context.getNodes().stream() .filter(node -> node.getNode() instanceof ArgumentCommandNode) .map(node -> node.getNode().getName()) @@ -67,25 +68,18 @@ public int execute(CommandContext context) throws CommandSyn for (ArgumentData argument : arguments) { Object value = null; if (providedArgs.contains(argument.name())) { - value = context.getArgument(argument.name(), Object.class); + 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()) { + 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 (value == SkriptBrigadierArgument.DEFAULT_VALUE_PLACEHOLDER) { - assert argument.defaultValue() != null; - if (argument.isSingle()) { - value = argument.defaultValue().getSingle(commandEvent); - } else { - value = argument.defaultValue().getArray(commandEvent); - } } - } else if (argument.defaultValue() != null) { + } else if (argument.defaultValue() != null) { // fallback to default value if (argument.isSingle()) { value = argument.defaultValue().getSingle(commandEvent); } else { @@ -100,6 +94,8 @@ public int execute(CommandContext context) throws CommandSyn } } } + + // execution trigger.execute(commandEvent); return Command.SINGLE_SUCCESS; diff --git a/src/main/java/org/skriptlang/skript/bukkit/command/brigadier/SkriptCommandRegistrar.java b/src/main/java/org/skriptlang/skript/bukkit/command/custom/ScriptCommandRegistrar.java similarity index 89% rename from src/main/java/org/skriptlang/skript/bukkit/command/brigadier/SkriptCommandRegistrar.java rename to src/main/java/org/skriptlang/skript/bukkit/command/custom/ScriptCommandRegistrar.java index dc5d5b406c2..18f3927c796 100644 --- a/src/main/java/org/skriptlang/skript/bukkit/command/brigadier/SkriptCommandRegistrar.java +++ b/src/main/java/org/skriptlang/skript/bukkit/command/custom/ScriptCommandRegistrar.java @@ -1,4 +1,4 @@ -package org.skriptlang.skript.bukkit.command.brigadier; +package org.skriptlang.skript.bukkit.command.custom; import ch.njol.skript.Skript; import io.papermc.paper.command.brigadier.Commands; @@ -24,13 +24,13 @@ * 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 SkriptCommandRegistrar { +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 Map> REGISTERED_COMMANDS = new ConcurrentHashMap<>(); + private static final Set PENDING_REGISTRATIONS = ConcurrentHashMap.newKeySet(); private static final Set PENDING_UNREGISTRATIONS = ConcurrentHashMap.newKeySet(); private static @Nullable MethodHandle SET_VALID; @@ -41,7 +41,7 @@ public final class SkriptCommandRegistrar { private static boolean useSafeReload; public static void init(JavaPlugin plugin) { - SkriptCommandRegistrar.plugin = plugin; + ScriptCommandRegistrar.plugin = plugin; plugin.getLifecycleManager().registerEventHandler(LifecycleEvents.COMMANDS, commands -> { commandRegistrar = commands.registrar(); @@ -73,17 +73,17 @@ public static void init(JavaPlugin plugin) { * @param command The command to register. * @see #processRegistrations() */ - public static void register(SkriptBrigadierCommand command) { + public static void register(ScriptBrigadierCommand command) { PENDING_REGISTRATIONS.add(command); } /** * Processes all pending registrations, synchronizing them with the server's command dispatcher. - * @see #register(SkriptBrigadierCommand) + * @see #register(ScriptBrigadierCommand) */ public static void processRegistrations() { if (!Bukkit.isPrimaryThread()) { - Bukkit.getScheduler().runTask(plugin, SkriptCommandRegistrar::processRegistrations); + Bukkit.getScheduler().runTask(plugin, ScriptCommandRegistrar::processRegistrations); return; } @@ -109,7 +109,7 @@ public static void processRegistrations() { private static void processRegistrationSet() { PluginMeta pluginMeta = plugin.getPluginMeta(); - for (SkriptBrigadierCommand command : PENDING_REGISTRATIONS) { + for (ScriptBrigadierCommand command : PENDING_REGISTRATIONS) { if (command.namespace() == null) { REGISTERED_COMMANDS.put(command, commandRegistrar.register(pluginMeta, command.node(), command.description(), command.aliases())); } else { @@ -153,17 +153,17 @@ public Object invoke(Object proxy, Method method, Object[] args) throws Throwabl * @param command The command to unregister. * @see #processUnregistrations() */ - public static void unregister(SkriptBrigadierCommand command) { + 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(SkriptBrigadierCommand) + * @see #unregister(ScriptBrigadierCommand) */ public static void processUnregistrations() { if (!Bukkit.isPrimaryThread()) { - Bukkit.getScheduler().runTask(plugin, SkriptCommandRegistrar::processUnregistrations); + Bukkit.getScheduler().runTask(plugin, ScriptCommandRegistrar::processUnregistrations); return; } @@ -193,7 +193,7 @@ public static void processUnregistrations() { * @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 SkriptBrigadierCommand getCommand(String command) { + 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() 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 index 17512e0a4a9..e3b1dc06b92 100644 --- 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 @@ -15,9 +15,9 @@ import ch.njol.skript.lang.Effect; import ch.njol.skript.lang.Expression; import ch.njol.util.Kleenean; -import org.skriptlang.skript.bukkit.command.brigadier.CommandParsingData; -import org.skriptlang.skript.bukkit.command.brigadier.ExecutorData; -import org.skriptlang.skript.bukkit.command.brigadier.ScriptCommandEvent; +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; 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 index bedb6f34a40..6fa963a558c 100644 --- 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 @@ -21,9 +21,9 @@ import org.bukkit.event.player.PlayerCommandPreprocessEvent; import org.bukkit.event.server.ServerCommandEvent; import org.jetbrains.annotations.Nullable; -import org.skriptlang.skript.bukkit.command.brigadier.ArgumentData; -import org.skriptlang.skript.bukkit.command.brigadier.ScriptCommandEvent; -import org.skriptlang.skript.bukkit.command.brigadier.CommandParsingData; +import org.skriptlang.skript.bukkit.command.custom.ArgumentData; +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; 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 index 1f9d3ef01b8..7b3f8a5e58f 100644 --- 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 @@ -20,10 +20,10 @@ import ch.njol.skript.util.Date; import ch.njol.skript.util.Timespan; import ch.njol.util.Kleenean; -import org.skriptlang.skript.bukkit.command.brigadier.CommandParsingData; -import org.skriptlang.skript.bukkit.command.brigadier.ExecutorData; -import org.skriptlang.skript.bukkit.command.brigadier.ExecutorData.CooldownManager; -import org.skriptlang.skript.bukkit.command.brigadier.ScriptCommandEvent; +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; 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 index bf886b800da..b9f96e563d5 100644 --- 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 @@ -10,9 +10,9 @@ import org.bukkit.event.Event; import org.jetbrains.annotations.Nullable; import org.skriptlang.skript.addon.SkriptAddon; -import org.skriptlang.skript.bukkit.command.brigadier.SkriptCommandRegistrar; -import org.skriptlang.skript.bukkit.command.brigadier.SkriptBrigadierCommand; -import org.skriptlang.skript.bukkit.command.brigadier.ScriptCommandEvent; +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; @@ -41,7 +41,7 @@ public static void register(SkriptAddon addon, SyntaxRegistry syntaxRegistry) { private static final AtomicBoolean SYNC_COMMANDS = new AtomicBoolean(); private SectionNode rootNode; - private SkriptBrigadierCommand command; + private ScriptBrigadierCommand command; @Override public boolean init(Literal[] args, int matchedPattern, ParseResult parseResult, EntryContainer entryContainer) { @@ -62,7 +62,7 @@ public boolean load() { } // validation - SkriptBrigadierCommand existing = SkriptCommandRegistrar.getCommand(node.getLiteral()); + 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'"); @@ -70,8 +70,8 @@ public boolean load() { } // registration - command = new SkriptBrigadierCommand(getParser().getCurrentScript(), node, result.aliases(), result.description(), result.prefix()); - SkriptCommandRegistrar.register(command); + command = new ScriptBrigadierCommand(getParser().getCurrentScript(), node, result.aliases(), result.description(), result.prefix()); + ScriptCommandRegistrar.register(command); SYNC_COMMANDS.set(true); return true; @@ -80,21 +80,21 @@ public boolean load() { @Override public boolean postLoad() { if (SYNC_COMMANDS.compareAndSet(true, false)) { - SkriptCommandRegistrar.processRegistrations(); + ScriptCommandRegistrar.processRegistrations(); } return true; } @Override public void unload() { - SkriptCommandRegistrar.unregister(command); + ScriptCommandRegistrar.unregister(command); SYNC_COMMANDS.set(true); } @Override public void postUnload() { if (SYNC_COMMANDS.compareAndSet(true, false)) { - SkriptCommandRegistrar.processUnregistrations(); + ScriptCommandRegistrar.processUnregistrations(); } } diff --git a/src/main/java/org/skriptlang/skript/bukkit/command/brigadier/CommandCompiler.java b/src/main/java/org/skriptlang/skript/bukkit/command/elements/structures/util/CommandCompiler.java similarity index 98% rename from src/main/java/org/skriptlang/skript/bukkit/command/brigadier/CommandCompiler.java rename to src/main/java/org/skriptlang/skript/bukkit/command/elements/structures/util/CommandCompiler.java index d9289bee504..8f303ea5da5 100644 --- a/src/main/java/org/skriptlang/skript/bukkit/command/brigadier/CommandCompiler.java +++ b/src/main/java/org/skriptlang/skript/bukkit/command/elements/structures/util/CommandCompiler.java @@ -1,4 +1,4 @@ -package org.skriptlang.skript.bukkit.command.brigadier; +package org.skriptlang.skript.bukkit.command.elements.structures.util; import ch.njol.skript.Skript; import ch.njol.skript.classes.ClassInfo; @@ -8,8 +8,8 @@ import ch.njol.skript.log.RetainingLogHandler; import ch.njol.skript.registrations.Classes; import ch.njol.skript.util.Utils; -import org.jetbrains.annotations.ApiStatus; import org.jetbrains.annotations.Nullable; +import org.skriptlang.skript.bukkit.command.custom.ArgumentData; import java.util.ArrayList; import java.util.Collection; @@ -26,8 +26,7 @@ * and arguments {@link ArgumentCommandElement}. * @see #compile(String, List) */ -@ApiStatus.Internal -public final class CommandCompiler { +final class CommandCompiler { /* * Tree Structures 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 index 25341ea1388..e15e0db3c32 100644 --- 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 @@ -19,19 +19,18 @@ import net.kyori.adventure.text.Component; import org.bukkit.command.CommandSender; import org.jetbrains.annotations.Nullable; -import org.skriptlang.skript.bukkit.command.brigadier.ArgumentData; -import org.skriptlang.skript.bukkit.command.brigadier.CommandCompiler; -import org.skriptlang.skript.bukkit.command.brigadier.CommandParsingData; -import org.skriptlang.skript.bukkit.command.brigadier.ExecutorData; -import org.skriptlang.skript.bukkit.command.brigadier.ExecutorData.CooldownManager; -import org.skriptlang.skript.bukkit.command.brigadier.ExecutorData.ExecutableBy; -import org.skriptlang.skript.bukkit.command.brigadier.ScriptCommandEvent; -import org.skriptlang.skript.bukkit.command.brigadier.CommandCompiler.ArgumentCommandElement; -import org.skriptlang.skript.bukkit.command.brigadier.CommandCompiler.CommandElement; -import org.skriptlang.skript.bukkit.command.brigadier.CommandCompiler.CompilationResult; -import org.skriptlang.skript.bukkit.command.brigadier.CommandCompiler.LiteralCommandElement; -import org.skriptlang.skript.bukkit.command.brigadier.SkriptBrigadierArgument; -import org.skriptlang.skript.bukkit.command.brigadier.SkriptCommandExecutor; +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.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; @@ -166,10 +165,6 @@ public SubCommandEntryData(String key, boolean optional, boolean multiple) { return null; } - // prepare arguments - parsingData.pushArguments(compilationResult.arguments()); - List> allArguments = parsingData.getArguments(); - // prepare entries // command aliases List aliases = entryContainer.getOptional("aliases", List.class, false); @@ -278,6 +273,9 @@ public SubCommandEntryData(String key, boolean optional, boolean multiple) { " 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 @@ -306,9 +304,9 @@ public SubCommandEntryData(String key, boolean optional, boolean multiple) { boolean hasExecute = execute != null; // setup executor - SkriptCommandExecutor executor; + ScriptCommandExecutor executor; if (hasExecute) { - executor = new SkriptCommandExecutor(execute, allArguments, cooldownManager); + executor = new ScriptCommandExecutor(execute, allArguments, cooldownManager); } else { executor = null; } @@ -351,7 +349,7 @@ public boolean canCreateWith(Node node) { private static ArgumentBuilder parse( CommandElement commandElement, - @Nullable SkriptCommandExecutor executor, + @Nullable ScriptCommandExecutor executor, @Nullable Predicate requires, Collection> subcommands ) { @@ -364,7 +362,7 @@ public boolean canCreateWith(Node node) { ArgumentData data = ((ArgumentCommandElement) commandElement).argument(); ArgumentType nativeType = null; - var nativeMapping = SkriptBrigadierArgument.ARGUMENT_TYPE_MAPPINGS.get(data.type().getC()); + var nativeMapping = ScriptArgumentType.ARGUMENT_TYPE_MAPPINGS.get(data.type().getC()); if (nativeMapping != null) { // native argument type may be available nativeType = nativeMapping.apply(data); } @@ -374,7 +372,7 @@ public boolean canCreateWith(Node node) { } else { nativeType = StringArgumentType.string(); } - nativeType = new SkriptBrigadierArgument<>(data, (StringArgumentType) nativeType); + nativeType = new ScriptArgumentType<>(data, (StringArgumentType) nativeType); } argument = Commands.argument(data.name(), nativeType); From 004968e72682159935298d8d5e0c73a0e0590a90 Mon Sep 17 00:00:00 2001 From: Patrick Miller Date: Sat, 4 Jul 2026 02:40:02 -0400 Subject: [PATCH 27/41] Add HelpTopic handling, restore "usage" entry --- .../custom/ScriptBrigadierCommand.java | 1 + .../custom/ScriptCommandRegistrar.java | 104 +++++++++++++++++- .../elements/structures/StructCommand.java | 3 +- .../structures/util/SubCommandEntryData.java | 18 ++- 4 files changed, 117 insertions(+), 9 deletions(-) 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 index f794f125f44..2113a7cfe5f 100644 --- a/src/main/java/org/skriptlang/skript/bukkit/command/custom/ScriptBrigadierCommand.java +++ b/src/main/java/org/skriptlang/skript/bukkit/command/custom/ScriptBrigadierCommand.java @@ -20,5 +20,6 @@ public record ScriptBrigadierCommand( LiteralCommandNode node, Collection aliases, @Nullable String description, + @Nullable String usage, @Nullable String namespace ) { } 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 index 18f3927c796..af3f1e3dea9 100644 --- a/src/main/java/org/skriptlang/skript/bukkit/command/custom/ScriptCommandRegistrar.java +++ b/src/main/java/org/skriptlang/skript/bukkit/command/custom/ScriptCommandRegistrar.java @@ -5,6 +5,13 @@ 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.Nullable; @@ -14,8 +21,10 @@ 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; @@ -33,15 +42,15 @@ public final class ScriptCommandRegistrar { 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 boolean useSafeReload; + private static final SkriptIndexHelpTopic indexHelpTopic = new SkriptIndexHelpTopic(); public static void init(JavaPlugin plugin) { - ScriptCommandRegistrar.plugin = plugin; plugin.getLifecycleManager().registerEventHandler(LifecycleEvents.COMMANDS, commands -> { commandRegistrar = commands.registrar(); @@ -49,8 +58,22 @@ public static void init(JavaPlugin plugin) { 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(); @@ -64,6 +87,9 @@ public static void init(JavaPlugin plugin) { } 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())); }); } @@ -109,6 +135,8 @@ public static void processRegistrations() { 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())); @@ -119,6 +147,9 @@ private static void processRegistrationSet() { 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(); } @@ -147,6 +178,28 @@ public Object invoke(Object proxy, Method method, Object[] args) throws Throwabl } + 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 -> helpMap.getHelpTopic("/" + label)) + .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()); + } + + // TODO may want to copy CommandAliasHelpTopic? Not normally used for Brigadier aliases though + HelpTopic newTopic = new GenericCommandHelpTopic(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()}. @@ -180,6 +233,15 @@ public static void processUnregistrations() { 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()); @@ -200,4 +262,42 @@ public static void processUnregistrations() { .orElse(null); } + 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(); + } + + } + } 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 index b9f96e563d5..0063df3801d 100644 --- 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 @@ -70,7 +70,8 @@ public boolean load() { } // registration - command = new ScriptBrigadierCommand(getParser().getCurrentScript(), node, result.aliases(), result.description(), result.prefix()); + command = new ScriptBrigadierCommand(getParser().getCurrentScript(), node, result.aliases(), + result.description(), result.usage(), result.prefix()); ScriptCommandRegistrar.register(command); SYNC_COMMANDS.set(true); 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 index e15e0db3c32..9c8c665c14b 100644 --- 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 @@ -56,6 +56,7 @@ public record Result( List> arguments, Collection aliases, @Nullable String description, + @Nullable String usage, @Nullable String prefix ) { } @@ -82,6 +83,8 @@ protected List getValue(String value) { } }) .addEntry("description", null, true) + // TODO this was previously a VariableString. need to handle that + .addEntry("usage", null, true) .addEntry("prefix", null, true) .addEntry("permission", null, true) .addEntryData(new KeyValueEntryData("executable by", null, true) { @@ -110,7 +113,6 @@ protected ExecutableBy getValue(String value) { .addEntryData(new TriggerEntryData("trigger", null, true)) .addEntryData(new SubCommandEntryData("subcommand", true, true)) // deprecated entries - .addEntry("usage", null, true) .addEntry("permission message", null, true) .build(); @@ -184,6 +186,14 @@ public SubCommandEntryData(String key, boolean optional, boolean multiple) { return null; } + // command usage + String usage = entryContainer.getOptional("usage", String.class, false); + if (!isRoot && usage != null) { + Skript.error("Only the root of a command may have a usage."); + return null; + } + + // command prefix (custom namespace) String prefix = entryContainer.getOptional("prefix", String.class, false); if (prefix != null) { if (!isRoot) { @@ -264,10 +274,6 @@ public SubCommandEntryData(String key, boolean optional, boolean multiple) { } // handle deprecated entries - if (entryContainer.hasEntry("usage")) { - ScriptWarning.printDeprecationWarning("The 'usage' entry has been deprecated for removal in a future release." + - " Incorrect command usage is now handled by the client's command validator."); - } 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."); @@ -331,7 +337,7 @@ public SubCommandEntryData(String key, boolean optional, boolean multiple) { parsingData.popArguments(); //noinspection unchecked - return new Result(result, aliases, description, prefix); + return new Result(result, aliases, description, usage, prefix); } @Override From 214e1e97e430129201bd71d791015b54956e10a9 Mon Sep 17 00:00:00 2001 From: Patrick Miller Date: Sun, 5 Jul 2026 00:15:54 -0400 Subject: [PATCH 28/41] Improve native argument validation --- .../command/custom/ScriptArgumentType.java | 75 +++++++++++++------ .../structures/util/CommandCompiler.java | 11 ++- .../structures/util/SubCommandEntryData.java | 5 +- .../tests/bukkit/command/StructCommand.sk | 33 ++++++++ 4 files changed, 98 insertions(+), 26 deletions(-) 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 index 9179e62e74f..fc86dea5299 100644 --- a/src/main/java/org/skriptlang/skript/bukkit/command/custom/ScriptArgumentType.java +++ b/src/main/java/org/skriptlang/skript/bukkit/command/custom/ScriptArgumentType.java @@ -41,37 +41,66 @@ public class ScriptArgumentType implements CustomArgumentType.Converted { /** - * Pre-defined mappings of types that are acceptable to map to other native argument types. + * 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 static final Map, Function, ArgumentType>> ARGUMENT_TYPE_MAPPINGS = Map.of( - Boolean.class, data -> data.isSingle() ? BoolArgumentType.bool() : null, - Long.class, data -> { - if (!data.isSingle()) { - // TODO should error here if min/max is used - return null; + 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; } - if (data.max() == null) { - if (data.min() == 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. + */ + public 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) data.min()); - } - return LongArgumentType.longArg((long) data.min(), (long) data.max()); - }, - Number.class, data -> { - if (!data.isSingle()) { - return null; + return LongArgumentType.longArg(Long.MIN_VALUE, max); + } else if (max == null) { + return LongArgumentType.longArg(min); } - if (data.max() == null) { - if (data.min() == null) { + 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) data.min()); + return DoubleArgumentType.doubleArg(Double.MIN_VALUE, max.doubleValue()); + } else if (max == null) { + return DoubleArgumentType.doubleArg(min.doubleValue()); } - return DoubleArgumentType.doubleArg((double) data.min(), (double) data.max()); - }, - Player.class, data -> data.isSingle() ? ArgumentTypes.player() : ArgumentTypes.players(), - Entity.class, data -> data.isSingle() ? ArgumentTypes.entity() : ArgumentTypes.entities() + 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()) ); private static final DynamicCommandExceptionType ERROR_PARSER_ERROR = new DynamicCommandExceptionType( 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 index 8f303ea5da5..892efa78f5e 100644 --- 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 @@ -10,6 +10,8 @@ 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; @@ -362,7 +364,7 @@ private static List appendToLiterals(Collection]+?)\\s*:\\s*)?(.+?)\\s*(?:=\\s*(" + SkriptParser.WILDCARD + "))?\\s*$"); private static final Pattern TYPE_PATTERN = - Pattern.compile("^(.+?)\\s*(?: from (.+?)(?: to (.+?))?)?$"); + 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); @@ -403,6 +405,13 @@ private static List appendToLiterals(Collection data = ((ArgumentCommandElement) commandElement).argument(); ArgumentType nativeType = null; - var nativeMapping = ScriptArgumentType.ARGUMENT_TYPE_MAPPINGS.get(data.type().getC()); + NativeArgumentData nativeMapping = ScriptArgumentType.ARGUMENT_TYPE_MAPPINGS.get(data.type().getC()); if (nativeMapping != null) { // native argument type may be available - nativeType = nativeMapping.apply(data); + nativeType = nativeMapping.mapper().apply(data); } if (nativeType == null) { if (children.isEmpty() && subcommands.isEmpty()) { // last argument can be greedy diff --git a/src/test/skript/tests/bukkit/command/StructCommand.sk b/src/test/skript/tests/bukkit/command/StructCommand.sk index b2433de8c8b..251858752c0 100644 --- a/src/test/skript/tests/bukkit/command/StructCommand.sk +++ b/src/test/skript/tests/bukkit/command/StructCommand.sk @@ -3,6 +3,7 @@ using local variable type hints options: command: skriptcommand +# expect error for invalid executable by for subcommand parse: results: {-StructCommand::entries::1::*} code: @@ -15,6 +16,7 @@ parse: trigger: stop +# expect error for duplicate argument names parse: results: {-StructCommand::args::1::*} code: @@ -22,6 +24,23 @@ parse: 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 {_x} is not a text parse: results: {-StructCommand::hints::1::*} code: @@ -29,6 +48,7 @@ parse: trigger: set {_a} to {_x} in lowercase +# expect error that {_x::*} is not a text parse: results: {-StructCommand::hints::2::*} code: @@ -47,6 +67,10 @@ 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 @@ -66,6 +90,8 @@ test "StructCommand": # parsing assert {-StructCommand::entries::1::*} is "It is not possible to restrict execution to the console as the parent command is only executable by players." 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 # 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::*}%)" @@ -81,6 +107,13 @@ test "StructCommand": 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" + execute command "test-args-ranged 4" + assert {-test-args-ranged::*} is 6 with "argument below range was accepted" + execute command "test-args-ranged 11" + 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" From 4cb520d9183728a56131ab3bb33744b21660da22 Mon Sep 17 00:00:00 2001 From: Patrick Miller Date: Mon, 6 Jul 2026 11:14:07 -0400 Subject: [PATCH 29/41] Refine ranged argument validation --- .../elements/structures/util/CommandCompiler.java | 14 ++++++++++---- .../skript/tests/bukkit/command/StructCommand.sk | 9 +++++++++ 2 files changed, 19 insertions(+), 4 deletions(-) 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 index 892efa78f5e..b766ab1e6ea 100644 --- 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 @@ -407,10 +407,16 @@ private static List appendToLiterals(Collection: + trigger: + stop + # expect error that {_x} is not a text parse: results: {-StructCommand::hints::1::*} @@ -92,6 +100,7 @@ test "StructCommand": 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::*}%)" From 8606b5b8d5f4b2c7bf23bf4d090b9243cf26a08e Mon Sep 17 00:00:00 2001 From: Patrick Miller Date: Mon, 6 Jul 2026 12:54:31 -0400 Subject: [PATCH 30/41] Fix help topic creation --- .../custom/ScriptCommandRegistrar.java | 30 +++++++++++++++++-- 1 file changed, 27 insertions(+), 3 deletions(-) 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 index af3f1e3dea9..71608d778e3 100644 --- a/src/main/java/org/skriptlang/skript/bukkit/command/custom/ScriptCommandRegistrar.java +++ b/src/main/java/org/skriptlang/skript/bukkit/command/custom/ScriptCommandRegistrar.java @@ -181,7 +181,19 @@ public Object invoke(Object proxy, Method method, Object[] args) throws Throwabl 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 -> helpMap.getHelpTopic("/" + label)) + .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()); } @@ -193,8 +205,7 @@ private static void registerHelp(HelpMap helpMap, CommandMap commandMap, ScriptB bukkitCommand.setUsage(command.usage()); } - // TODO may want to copy CommandAliasHelpTopic? Not normally used for Brigadier aliases though - HelpTopic newTopic = new GenericCommandHelpTopic(bukkitCommand); + HelpTopic newTopic = new SkriptGenericCommandHelpTopic(bukkitCommand); helpMap.addTopic(newTopic); indexHelpTopic.add(newTopic); } @@ -300,4 +311,17 @@ public void 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; + } + } + + } + } From c3ee50ef345b552f5c026be5603ef17ac2314b67 Mon Sep 17 00:00:00 2001 From: Patrick Miller Date: Mon, 6 Jul 2026 12:54:57 -0400 Subject: [PATCH 31/41] More testing and adjustments --- .../skript/expressions/ExprCommandInfo.java | 5 ++- .../structures/util/SubCommandEntryData.java | 19 ++++++---- .../tests/bukkit/command/StructCommand.sk | 37 ++++++++++++++++++- 3 files changed, 49 insertions(+), 12 deletions(-) diff --git a/src/main/java/ch/njol/skript/expressions/ExprCommandInfo.java b/src/main/java/ch/njol/skript/expressions/ExprCommandInfo.java index 39ac41c8ccc..f296fc5ddc8 100644 --- a/src/main/java/ch/njol/skript/expressions/ExprCommandInfo.java +++ b/src/main/java/ch/njol/skript/expressions/ExprCommandInfo.java @@ -12,6 +12,7 @@ import org.bukkit.command.Command; import org.bukkit.command.CommandMap; import org.bukkit.command.PluginCommand; +import org.bukkit.command.PluginIdentifiableCommand; import org.bukkit.command.defaults.BukkitCommand; import org.bukkit.event.Event; import org.bukkit.event.player.PlayerCommandPreprocessEvent; @@ -62,8 +63,8 @@ private enum InfoType { PERMISSION(Command::getPermission), PERMISSION_MESSAGE(Command::getPermissionMessage), PLUGIN(command -> { - if (command instanceof PluginCommand) { - return ((PluginCommand) command).getPlugin().getName(); + if (command instanceof PluginIdentifiableCommand pluginCommand) { + return pluginCommand.getPlugin().getName(); } else if (command instanceof BukkitCommand) { return "Bukkit"; } else if (command.getClass().getPackage().getName().startsWith("org.spigot")) { 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 index fc2327025ca..b40f26e7308 100644 --- 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 @@ -84,8 +84,7 @@ protected List getValue(String value) { } }) .addEntry("description", null, true) - // TODO this was previously a VariableString. need to handle that - .addEntry("usage", null, true) + .addEntryData(new VariableStringEntryData("usage", null, true)) .addEntry("prefix", null, true) .addEntry("permission", null, true) .addEntryData(new KeyValueEntryData("executable by", null, true) { @@ -188,10 +187,16 @@ public SubCommandEntryData(String key, boolean optional, boolean multiple) { } // command usage - String usage = entryContainer.getOptional("usage", String.class, false); - if (!isRoot && usage != null) { - Skript.error("Only the root of a command may have a usage."); - return null; + 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) @@ -319,7 +324,6 @@ public SubCommandEntryData(String key, boolean optional, boolean multiple) { } // attach subcommand pieces - // TODO verify error behavior... List> subcommands = entryContainer.getAll("subcommand", Result.class, false).stream() .flatMap(result -> result.arguments().stream()) @@ -398,7 +402,6 @@ public boolean canCreateWith(Node node) { // 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 - // TODO there is actually more complexity here to handle // 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()) { diff --git a/src/test/skript/tests/bukkit/command/StructCommand.sk b/src/test/skript/tests/bukkit/command/StructCommand.sk index c2eb47485da..6efbb595616 100644 --- a/src/test/skript/tests/bukkit/command/StructCommand.sk +++ b/src/test/skript/tests/bukkit/command/StructCommand.sk @@ -3,11 +3,17 @@ using local variable type hints options: command: skriptcommand -# expect error for invalid executable by for subcommand +# 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 @@ -16,6 +22,13 @@ parse: 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 error for duplicate argument names parse: results: {-StructCommand::args::1::*} @@ -64,6 +77,14 @@ parse: trigger: set {_a::*} to {_x::*} in lowercase +command /structcommand: + aliases: /structcommand2 + description: This is the description + usage: "This is the usage" + permission: command.structcommand + trigger: + stop + command /test-subcommand : subcommand : trigger: @@ -96,7 +117,10 @@ command /commandtest : test "StructCommand": # parsing - assert {-StructCommand::entries::1::*} is "It is not possible to restrict execution to the console as the parent command is only executable by players." + 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::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 @@ -105,6 +129,15 @@ test "StructCommand": 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::*}%)" + # entry validation + assert the main command name of the command "structcommand" is "structcommand" + assert the description of the command "structcommand" is "This is the description" + assert the label of the command "structcommand" is "structcommand" + #assert the usage of the command "structcommand" is "This is the usage" + assert the aliases of the command "structcommand" are "structcommand2" and "skript:structcommand2" + #assert the permission of the command "structcommand" is "skript.structcommand" + assert the plugin of the command "structcommand" is "Skript" + # execution execute command "test-subcommand 10" assert {-test-subcommand} is 10 with "root command execution failed" From b3f068dbf37d473e40dcc224add6fc8397b5c123 Mon Sep 17 00:00:00 2001 From: Patrick Miller Date: Mon, 6 Jul 2026 15:34:22 -0400 Subject: [PATCH 32/41] Port ExprCommandInfo --- .../skript/expressions/ExprCommandInfo.java | 182 --------------- .../java/ch/njol/skript/util/Patterns.java | 17 ++ .../skript/bukkit/command/CommandModule.java | 2 + .../custom/ScriptBrigadierCommand.java | 3 +- .../command/custom/ScriptCommandEvent.java | 8 +- .../command/custom/ScriptCommandExecutor.java | 5 +- .../elements/expressions/ExprCommandInfo.java | 215 ++++++++++++++++++ .../elements/structures/StructCommand.java | 2 +- .../structures/util/SubCommandEntryData.java | 5 +- .../tests/bukkit/command/StructCommand.sk | 8 +- 10 files changed, 255 insertions(+), 192 deletions(-) delete mode 100644 src/main/java/ch/njol/skript/expressions/ExprCommandInfo.java create mode 100644 src/main/java/org/skriptlang/skript/bukkit/command/elements/expressions/ExprCommandInfo.java 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 f296fc5ddc8..00000000000 --- a/src/main/java/ch/njol/skript/expressions/ExprCommandInfo.java +++ /dev/null @@ -1,182 +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.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.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 PluginIdentifiableCommand pluginCommand) { - return pluginCommand.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/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/org/skriptlang/skript/bukkit/command/CommandModule.java b/src/main/java/org/skriptlang/skript/bukkit/command/CommandModule.java index 0473042f69b..1efa719960e 100644 --- a/src/main/java/org/skriptlang/skript/bukkit/command/CommandModule.java +++ b/src/main/java/org/skriptlang/skript/bukkit/command/CommandModule.java @@ -8,6 +8,7 @@ import org.skriptlang.skript.bukkit.command.elements.effects.EffCancelCooldown; import org.skriptlang.skript.bukkit.command.elements.expressions.ExprArgument; import org.skriptlang.skript.bukkit.command.elements.expressions.ExprCmdCooldownInfo; +import org.skriptlang.skript.bukkit.command.elements.expressions.ExprCommandInfo; import org.skriptlang.skript.bukkit.command.elements.structures.StructCommand; public class CommandModule extends HierarchicalAddonModule { @@ -24,6 +25,7 @@ protected void loadSelf(SkriptAddon addon) { EffCancelCooldown::register, ExprArgument::register, ExprCmdCooldownInfo::register, + ExprCommandInfo::register, syntaxRegistry -> StructCommand.register(addon, syntaxRegistry) ); } 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 index 2113a7cfe5f..33d0472d35b 100644 --- a/src/main/java/org/skriptlang/skript/bukkit/command/custom/ScriptBrigadierCommand.java +++ b/src/main/java/org/skriptlang/skript/bukkit/command/custom/ScriptBrigadierCommand.java @@ -21,5 +21,6 @@ public record ScriptBrigadierCommand( Collection aliases, @Nullable String description, @Nullable String usage, - @Nullable String namespace + @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 index 68639e83fd3..165a954c92a 100644 --- a/src/main/java/org/skriptlang/skript/bukkit/command/custom/ScriptCommandEvent.java +++ b/src/main/java/org/skriptlang/skript/bukkit/command/custom/ScriptCommandEvent.java @@ -13,16 +13,22 @@ */ public class ScriptCommandEvent extends Event { + private final String label; private final CommandSender sender; private final ScriptCommandExecutor executor; final Map arguments = new HashMap<>(); - public ScriptCommandEvent(CommandSender sender, ScriptCommandExecutor executor) { + public ScriptCommandEvent(String label, CommandSender sender, ScriptCommandExecutor executor) { + this.label = label; this.sender = sender; this.executor = executor; } + public String getLabel() { + return label; + } + public CommandSender getSender() { return sender; } 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 index eb8ae174e21..f07589cbd95 100644 --- a/src/main/java/org/skriptlang/skript/bukkit/command/custom/ScriptCommandExecutor.java +++ b/src/main/java/org/skriptlang/skript/bukkit/command/custom/ScriptCommandExecutor.java @@ -6,6 +6,8 @@ import com.mojang.brigadier.context.CommandContext; import com.mojang.brigadier.exceptions.CommandSyntaxException; import com.mojang.brigadier.tree.ArgumentCommandNode; +import com.mojang.brigadier.tree.LiteralCommandNode; +import com.mojang.brigadier.tree.RootCommandNode; import io.papermc.paper.command.brigadier.CommandSourceStack; import io.papermc.paper.command.brigadier.argument.resolvers.ArgumentResolver; import org.bukkit.command.CommandSender; @@ -53,7 +55,8 @@ public ScriptCommandExecutor(Trigger trigger, List> arguments, @ public int execute(CommandContext context) throws CommandSyntaxException { CommandSender sender = context.getSource().getSender(); - ScriptCommandEvent commandEvent = new ScriptCommandEvent(sender, this); + ScriptCommandEvent commandEvent = + new ScriptCommandEvent(context.getNodes().getFirst().getNode().getName(), sender, this); // final validations if (cooldownManager != null && !cooldownManager.checkExecution(commandEvent, sender)) { 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..621fe1cd5b9 --- /dev/null +++ b/src/main/java/org/skriptlang/skript/bukkit/command/elements/expressions/ExprCommandInfo.java @@ -0,0 +1,215 @@ +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 = new Patterns<>( + "[the] main command [label|name] [of [[the] command[s] %-strings%]]", InfoType.NAME, + "command[s] %strings%'[s] main command [label|name]", InfoType.NAME, + "[the] description [of [[the] command[s] %-strings%]]", InfoType.DESCRIPTION, + "command[s] %strings%'[s] description", InfoType.DESCRIPTION, + "[the] label [of [[the] command[s] %-strings%]]", InfoType.LABEL, + "command[s] %strings%'[s] label", InfoType.LABEL, + "[the] usage [of [[the] command[s] %-strings%]]", InfoType.USAGE, + "command[s] %strings%'[s] usage", InfoType.USAGE, + "[(all|the|all [of] the)] aliases [of [[the] command[s] %-strings%]]", InfoType.ALIASES, + "command[s] %strings%'[s] aliases", InfoType.ALIASES, + "[the] permission [of [[the] command[s] %-strings%]]", InfoType.PERMISSION, + "command[s] %strings%'[s] permission", InfoType.PERMISSION, + "[the] permission message [of [[the] command[s] %-strings%]]", InfoType.PERMISSION_MESSAGE, + "command[s] %strings%'[s] permission message", InfoType.PERMISSION_MESSAGE, + "[the] plugin [owner] [of [[the] command[s] %-strings%]]", InfoType.PLUGIN, + "command[s] %strings%'[s] 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/structures/StructCommand.java b/src/main/java/org/skriptlang/skript/bukkit/command/elements/structures/StructCommand.java index 0063df3801d..8bf493eee8a 100644 --- 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 @@ -71,7 +71,7 @@ public boolean load() { // registration command = new ScriptBrigadierCommand(getParser().getCurrentScript(), node, result.aliases(), - result.description(), result.usage(), result.prefix()); + result.description(), result.usage(), result.prefix(), result.permission()); ScriptCommandRegistrar.register(command); SYNC_COMMANDS.set(true); 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 index b40f26e7308..ffb496783c2 100644 --- 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 @@ -58,7 +58,8 @@ public record Result( Collection aliases, @Nullable String description, @Nullable String usage, - @Nullable String prefix + @Nullable String prefix, + @Nullable String permission ) { } private static final Predicate TRUE_PREDICATE = ignored -> true; @@ -342,7 +343,7 @@ public SubCommandEntryData(String key, boolean optional, boolean multiple) { parsingData.popArguments(); //noinspection unchecked - return new Result(result, aliases, description, usage, prefix); + return new Result(result, aliases, description, usage, prefix, permission); } @Override diff --git a/src/test/skript/tests/bukkit/command/StructCommand.sk b/src/test/skript/tests/bukkit/command/StructCommand.sk index 6efbb595616..f33ad302333 100644 --- a/src/test/skript/tests/bukkit/command/StructCommand.sk +++ b/src/test/skript/tests/bukkit/command/StructCommand.sk @@ -80,8 +80,8 @@ parse: command /structcommand: aliases: /structcommand2 description: This is the description - usage: "This is the usage" - permission: command.structcommand + usage: This is the usage + permission: skript.structcommand trigger: stop @@ -133,9 +133,9 @@ test "StructCommand": assert the main command name of the command "structcommand" is "structcommand" assert the description of the command "structcommand" is "This is the description" assert the label of the command "structcommand" is "structcommand" - #assert the usage of the command "structcommand" is "This is the usage" + assert the usage of the command "structcommand" is "This is the usage" assert the aliases of the command "structcommand" are "structcommand2" and "skript:structcommand2" - #assert the permission of the command "structcommand" is "skript.structcommand" + assert the permission of the command "structcommand" is "skript.structcommand" assert the plugin of the command "structcommand" is "Skript" # execution From bbefbc8346a9c137092f69ddff94af28ba2d7c09 Mon Sep 17 00:00:00 2001 From: Patrick Miller Date: Mon, 6 Jul 2026 17:03:44 -0400 Subject: [PATCH 33/41] Continue porting syntax --- src/main/java/ch/njol/skript/Skript.java | 40 +++--- .../conditions/CondIsSkriptCommand.java | 34 ----- .../ch/njol/skript/effects/EffCommand.java | 95 ------------- .../skript/bukkit/command/CommandModule.java | 6 + .../command/custom/ScriptCommandEvent.java | 8 +- .../command/custom/ScriptCommandExecutor.java | 4 +- .../conditions/CondIsScriptCommand.java | 38 +++++ .../command/elements/effects/EffCommand.java | 134 ++++++++++++++++++ .../elements}/expressions/ExprCommand.java | 57 ++++---- .../elements/expressions/ExprCommandInfo.java | 42 +++--- .../bukkit/command/CondIsScriptCommand.sk | 7 + .../tests/bukkit/command/ExprCommand.sk | 9 ++ .../tests/bukkit/command/ExprCommandInfo.sk | 16 +++ .../tests/bukkit/command/StructCommand.sk | 26 +--- 14 files changed, 293 insertions(+), 223 deletions(-) delete mode 100644 src/main/java/ch/njol/skript/conditions/CondIsSkriptCommand.java delete mode 100644 src/main/java/ch/njol/skript/effects/EffCommand.java create mode 100644 src/main/java/org/skriptlang/skript/bukkit/command/elements/conditions/CondIsScriptCommand.java create mode 100644 src/main/java/org/skriptlang/skript/bukkit/command/elements/effects/EffCommand.java rename src/main/java/{ch/njol/skript => org/skriptlang/skript/bukkit/command/elements}/expressions/ExprCommand.java (62%) create mode 100644 src/test/skript/tests/bukkit/command/CondIsScriptCommand.sk create mode 100644 src/test/skript/tests/bukkit/command/ExprCommand.sk create mode 100644 src/test/skript/tests/bukkit/command/ExprCommandInfo.sk diff --git a/src/main/java/ch/njol/skript/Skript.java b/src/main/java/ch/njol/skript/Skript.java index 216bc6687d8..b936c573439 100644 --- a/src/main/java/ch/njol/skript/Skript.java +++ b/src/main/java/ch/njol/skript/Skript.java @@ -44,15 +44,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 +62,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 +80,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; @@ -1811,26 +1813,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/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/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/org/skriptlang/skript/bukkit/command/CommandModule.java b/src/main/java/org/skriptlang/skript/bukkit/command/CommandModule.java index 1efa719960e..1948ad042ac 100644 --- a/src/main/java/org/skriptlang/skript/bukkit/command/CommandModule.java +++ b/src/main/java/org/skriptlang/skript/bukkit/command/CommandModule.java @@ -5,9 +5,12 @@ 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.CondIsScriptCommand; import org.skriptlang.skript.bukkit.command.elements.effects.EffCancelCooldown; +import org.skriptlang.skript.bukkit.command.elements.effects.EffCommand; import org.skriptlang.skript.bukkit.command.elements.expressions.ExprArgument; import org.skriptlang.skript.bukkit.command.elements.expressions.ExprCmdCooldownInfo; +import org.skriptlang.skript.bukkit.command.elements.expressions.ExprCommand; import org.skriptlang.skript.bukkit.command.elements.expressions.ExprCommandInfo; import org.skriptlang.skript.bukkit.command.elements.structures.StructCommand; @@ -22,9 +25,12 @@ protected void loadSelf(SkriptAddon addon) { ScriptCommandRegistrar.init(Skript.getInstance()); register(addon, + CondIsScriptCommand::register, EffCancelCooldown::register, + EffCommand::register, ExprArgument::register, ExprCmdCooldownInfo::register, + ExprCommand::register, ExprCommandInfo::register, syntaxRegistry -> StructCommand.register(addon, syntaxRegistry) ); 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 index 165a954c92a..a651d8e2b99 100644 --- a/src/main/java/org/skriptlang/skript/bukkit/command/custom/ScriptCommandEvent.java +++ b/src/main/java/org/skriptlang/skript/bukkit/command/custom/ScriptCommandEvent.java @@ -14,13 +14,15 @@ public class ScriptCommandEvent extends Event { private final String label; + private final String rawInput; private final CommandSender sender; private final ScriptCommandExecutor executor; final Map arguments = new HashMap<>(); - public ScriptCommandEvent(String label, CommandSender sender, ScriptCommandExecutor executor) { + public ScriptCommandEvent(String label, String rawInput, CommandSender sender, ScriptCommandExecutor executor) { this.label = label; + this.rawInput = rawInput; this.sender = sender; this.executor = executor; } @@ -29,6 +31,10 @@ public String getLabel() { return label; } + public String getRawInput() { + return rawInput; + } + public CommandSender getSender() { return sender; } 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 index f07589cbd95..30e051beeba 100644 --- a/src/main/java/org/skriptlang/skript/bukkit/command/custom/ScriptCommandExecutor.java +++ b/src/main/java/org/skriptlang/skript/bukkit/command/custom/ScriptCommandExecutor.java @@ -6,8 +6,6 @@ import com.mojang.brigadier.context.CommandContext; import com.mojang.brigadier.exceptions.CommandSyntaxException; import com.mojang.brigadier.tree.ArgumentCommandNode; -import com.mojang.brigadier.tree.LiteralCommandNode; -import com.mojang.brigadier.tree.RootCommandNode; import io.papermc.paper.command.brigadier.CommandSourceStack; import io.papermc.paper.command.brigadier.argument.resolvers.ArgumentResolver; import org.bukkit.command.CommandSender; @@ -56,7 +54,7 @@ public ScriptCommandExecutor(Trigger trigger, List> arguments, @ public int execute(CommandContext context) throws CommandSyntaxException { CommandSender sender = context.getSource().getSender(); ScriptCommandEvent commandEvent = - new ScriptCommandEvent(context.getNodes().getFirst().getNode().getName(), sender, this); + new ScriptCommandEvent(context.getNodes().getFirst().getNode().getName(), context.getInput(), sender, this); // final validations if (cooldownManager != null && !cooldownManager.checkExecution(commandEvent, sender)) { 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/EffCommand.java b/src/main/java/org/skriptlang/skript/bukkit/command/elements/effects/EffCommand.java new file mode 100644 index 00000000000..0a9b952dc14 --- /dev/null +++ b/src/main/java/org/skriptlang/skript/bukkit/command/elements/effects/EffCommand.java @@ -0,0 +1,134 @@ +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()); + } + return false; + } + } + +} 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/ExprCommandInfo.java b/src/main/java/org/skriptlang/skript/bukkit/command/elements/expressions/ExprCommandInfo.java index 621fe1cd5b9..75c5a83443a 100644 --- 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 @@ -114,24 +114,30 @@ private enum InfoType { } - private static final Patterns PATTERNS = new Patterns<>( - "[the] main command [label|name] [of [[the] command[s] %-strings%]]", InfoType.NAME, - "command[s] %strings%'[s] main command [label|name]", InfoType.NAME, - "[the] description [of [[the] command[s] %-strings%]]", InfoType.DESCRIPTION, - "command[s] %strings%'[s] description", InfoType.DESCRIPTION, - "[the] label [of [[the] command[s] %-strings%]]", InfoType.LABEL, - "command[s] %strings%'[s] label", InfoType.LABEL, - "[the] usage [of [[the] command[s] %-strings%]]", InfoType.USAGE, - "command[s] %strings%'[s] usage", InfoType.USAGE, - "[(all|the|all [of] the)] aliases [of [[the] command[s] %-strings%]]", InfoType.ALIASES, - "command[s] %strings%'[s] aliases", InfoType.ALIASES, - "[the] permission [of [[the] command[s] %-strings%]]", InfoType.PERMISSION, - "command[s] %strings%'[s] permission", InfoType.PERMISSION, - "[the] permission message [of [[the] command[s] %-strings%]]", InfoType.PERMISSION_MESSAGE, - "command[s] %strings%'[s] permission message", InfoType.PERMISSION_MESSAGE, - "[the] plugin [owner] [of [[the] command[s] %-strings%]]", InfoType.PLUGIN, - "command[s] %strings%'[s] plugin [owner]", InfoType.PLUGIN - ); + 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, 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/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 index f33ad302333..01d662a82c8 100644 --- a/src/test/skript/tests/bukkit/command/StructCommand.sk +++ b/src/test/skript/tests/bukkit/command/StructCommand.sk @@ -1,4 +1,5 @@ using local variable type hints +using error catching options: command: skriptcommand @@ -77,14 +78,6 @@ parse: trigger: set {_a::*} to {_x::*} in lowercase -command /structcommand: - aliases: /structcommand2 - description: This is the description - usage: This is the usage - permission: skript.structcommand - trigger: - stop - command /test-subcommand : subcommand : trigger: @@ -129,15 +122,6 @@ test "StructCommand": 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::*}%)" - # entry validation - assert the main command name of the command "structcommand" is "structcommand" - assert the description of the command "structcommand" is "This is the description" - assert the label of the command "structcommand" is "structcommand" - assert the usage of the command "structcommand" is "This is the usage" - assert the aliases of the command "structcommand" are "structcommand2" and "skript:structcommand2" - assert the permission of the command "structcommand" is "skript.structcommand" - assert the plugin of the command "structcommand" is "Skript" - # execution execute command "test-subcommand 10" assert {-test-subcommand} is 10 with "root command execution failed" @@ -151,9 +135,13 @@ test "StructCommand": execute command "test-args-ranged 6" assert {-test-args-ranged::*} is 6 with "argument in range was not accepted" - execute command "test-args-ranged 4" + catch runtime errors: + execute command "test-args-ranged 4" + assert last caught runtime errors start with "Failed to execute command: " assert {-test-args-ranged::*} is 6 with "argument below range was accepted" - execute command "test-args-ranged 11" + catch runtime errors: + execute command "test-args-ranged 11" + 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" From 30c9be361eeec6700e040b91e98f6617703d3ad1 Mon Sep 17 00:00:00 2001 From: Patrick Miller Date: Mon, 6 Jul 2026 17:20:48 -0400 Subject: [PATCH 34/41] Account for dispatchCommand behavior --- .../skript/bukkit/command/elements/effects/EffCommand.java | 2 ++ src/test/skript/tests/bukkit/command/StructCommand.sk | 6 ++++-- 2 files changed, 6 insertions(+), 2 deletions(-) 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 index 0a9b952dc14..1a61bc0d084 100644 --- 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 @@ -126,6 +126,8 @@ public static boolean dispatchCommand(CommandSender sender, String command, Runt } 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/test/skript/tests/bukkit/command/StructCommand.sk b/src/test/skript/tests/bukkit/command/StructCommand.sk index 01d662a82c8..dc2d93fe12e 100644 --- a/src/test/skript/tests/bukkit/command/StructCommand.sk +++ b/src/test/skript/tests/bukkit/command/StructCommand.sk @@ -137,11 +137,13 @@ test "StructCommand": assert {-test-args-ranged::*} is 6 with "argument in range was not accepted" catch runtime errors: execute command "test-args-ranged 4" - assert last caught runtime errors start with "Failed to execute command: " + 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" - assert last caught runtime errors start with "Failed to execute command: " + 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" From 2c19ef638d8ca513628e7938c6c0f2d26d2ce8ff Mon Sep 17 00:00:00 2001 From: Patrick Miller Date: Mon, 6 Jul 2026 17:52:28 -0400 Subject: [PATCH 35/41] Port ExprAllCommands --- src/main/java/ch/njol/skript/Skript.java | 3 - .../skript/expressions/ExprAllCommands.java | 69 ------------------- .../skript/bukkit/command/CommandModule.java | 19 ++--- .../custom/ScriptCommandRegistrar.java | 7 ++ .../elements/expressions/ExprAllCommands.java | 65 +++++++++++++++++ .../tests/bukkit/command/ExprAllCommands.sk | 9 +++ 6 files changed, 92 insertions(+), 80 deletions(-) delete mode 100644 src/main/java/ch/njol/skript/expressions/ExprAllCommands.java create mode 100644 src/main/java/org/skriptlang/skript/bukkit/command/elements/expressions/ExprAllCommands.java create mode 100644 src/test/skript/tests/bukkit/command/ExprAllCommands.sk diff --git a/src/main/java/ch/njol/skript/Skript.java b/src/main/java/ch/njol/skript/Skript.java index b936c573439..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; @@ -578,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")); 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/org/skriptlang/skript/bukkit/command/CommandModule.java b/src/main/java/org/skriptlang/skript/bukkit/command/CommandModule.java index 1948ad042ac..450c1bcdcaa 100644 --- a/src/main/java/org/skriptlang/skript/bukkit/command/CommandModule.java +++ b/src/main/java/org/skriptlang/skript/bukkit/command/CommandModule.java @@ -1,18 +1,15 @@ 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.CondIsScriptCommand; -import org.skriptlang.skript.bukkit.command.elements.effects.EffCancelCooldown; -import org.skriptlang.skript.bukkit.command.elements.effects.EffCommand; -import org.skriptlang.skript.bukkit.command.elements.expressions.ExprArgument; -import org.skriptlang.skript.bukkit.command.elements.expressions.ExprCmdCooldownInfo; -import org.skriptlang.skript.bukkit.command.elements.expressions.ExprCommand; -import org.skriptlang.skript.bukkit.command.elements.expressions.ExprCommandInfo; -import org.skriptlang.skript.bukkit.command.elements.structures.StructCommand; +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 { @@ -20,6 +17,11 @@ public CommandModule(AddonModule parentModule) { super(parentModule); } + @Override + protected void initSelf(SkriptAddon addon) { + Commands.registerListeners(); + } + @Override protected void loadSelf(SkriptAddon addon) { ScriptCommandRegistrar.init(Skript.getInstance()); @@ -28,6 +30,7 @@ protected void loadSelf(SkriptAddon addon) { CondIsScriptCommand::register, EffCancelCooldown::register, EffCommand::register, + ExprAllCommands::register, ExprArgument::register, ExprCmdCooldownInfo::register, ExprCommand::register, 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 index 71608d778e3..c850b2bad20 100644 --- a/src/main/java/org/skriptlang/skript/bukkit/command/custom/ScriptCommandRegistrar.java +++ b/src/main/java/org/skriptlang/skript/bukkit/command/custom/ScriptCommandRegistrar.java @@ -273,6 +273,13 @@ public static void processUnregistrations() { .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() { 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/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" From fba8623303b8c7541fbd15f72cb20b5710ab7ba7 Mon Sep 17 00:00:00 2001 From: Patrick Miller Date: Mon, 6 Jul 2026 17:57:17 -0400 Subject: [PATCH 36/41] Start deprecating legacy code --- .../skript/bukkitutil/CommandReloader.java | 2 + .../java/ch/njol/skript/command/Argument.java | 3 + .../ch/njol/skript/command/CommandUsage.java | 2 + .../java/ch/njol/skript/command/Commands.java | 44 --- .../ch/njol/skript/command/ScriptCommand.java | 5 +- .../skript/command/ScriptCommandEvent.java | 5 + .../ch/njol/skript/command/package-info.java | 24 -- .../ch/njol/skript/effects/EffScriptFile.java | 1 - .../ch/njol/skript/lang/SkriptParser.java | 27 -- .../njol/skript/structures/StructCommand.java | 332 +----------------- 10 files changed, 27 insertions(+), 418 deletions(-) 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/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/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 e9349b31e32..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 From 773d78cc9ffe110ad159d92e856d289efdb2dd17 Mon Sep 17 00:00:00 2001 From: Patrick Miller Date: Mon, 6 Jul 2026 22:50:04 -0400 Subject: [PATCH 37/41] Add additional command event values --- .../skript/expressions/ExprCommandSender.java | 34 --------- .../skript/bukkit/command/CommandModule.java | 2 + .../command/custom/ScriptCommandEvent.java | 59 +++++++++++++-- .../command/custom/ScriptCommandExecutor.java | 6 +- .../elements/effects/EffCancelCooldown.java | 4 +- .../expressions/ExprCmdCooldownInfo.java | 4 +- .../expressions/ExprCommandExecutor.java | 75 +++++++++++++++++++ .../expressions/ExprCommandSender.java | 39 ++++++++++ .../elements/structures/StructCommand.java | 13 ++++ 9 files changed, 190 insertions(+), 46 deletions(-) delete mode 100644 src/main/java/ch/njol/skript/expressions/ExprCommandSender.java create mode 100644 src/main/java/org/skriptlang/skript/bukkit/command/elements/expressions/ExprCommandExecutor.java create mode 100644 src/main/java/org/skriptlang/skript/bukkit/command/elements/expressions/ExprCommandSender.java 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/org/skriptlang/skript/bukkit/command/CommandModule.java b/src/main/java/org/skriptlang/skript/bukkit/command/CommandModule.java index 450c1bcdcaa..8df15872ce0 100644 --- a/src/main/java/org/skriptlang/skript/bukkit/command/CommandModule.java +++ b/src/main/java/org/skriptlang/skript/bukkit/command/CommandModule.java @@ -34,7 +34,9 @@ protected void loadSelf(SkriptAddon addon) { ExprArgument::register, ExprCmdCooldownInfo::register, ExprCommand::register, + ExprCommandExecutor::register, ExprCommandInfo::register, + ExprCommandSender::register, syntaxRegistry -> StructCommand.register(addon, syntaxRegistry) ); } 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 index a651d8e2b99..4551fdaf8ef 100644 --- a/src/main/java/org/skriptlang/skript/bukkit/command/custom/ScriptCommandEvent.java +++ b/src/main/java/org/skriptlang/skript/bukkit/command/custom/ScriptCommandEvent.java @@ -1,10 +1,15 @@ 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; @@ -15,34 +20,78 @@ public class ScriptCommandEvent extends Event { private final String label; private final String rawInput; + private final ScriptCommandExecutor commandExecutor; private final CommandSender sender; - private final ScriptCommandExecutor executor; + private final @Nullable Entity executor; + private final Location location; final Map arguments = new HashMap<>(); - public ScriptCommandEvent(String label, String rawInput, CommandSender sender, ScriptCommandExecutor executor) { + public ScriptCommandEvent(String label, String rawInput, ScriptCommandExecutor commandExecutor, CommandSourceStack source) { this.label = label; this.rawInput = rawInput; - this.sender = sender; - this.executor = executor; + 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; } - public ScriptCommandExecutor getExecutor() { + /** + * @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); } 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 index 30e051beeba..48a952ab472 100644 --- a/src/main/java/org/skriptlang/skript/bukkit/command/custom/ScriptCommandExecutor.java +++ b/src/main/java/org/skriptlang/skript/bukkit/command/custom/ScriptCommandExecutor.java @@ -52,12 +52,12 @@ public ScriptCommandExecutor(Trigger trigger, List> arguments, @ } public int execute(CommandContext context) throws CommandSyntaxException { - CommandSender sender = context.getSource().getSender(); + CommandSourceStack source = context.getSource(); ScriptCommandEvent commandEvent = - new ScriptCommandEvent(context.getNodes().getFirst().getNode().getName(), context.getInput(), sender, this); + new ScriptCommandEvent(context.getNodes().getFirst().getNode().getName(), context.getInput(), this, source); // final validations - if (cooldownManager != null && !cooldownManager.checkExecution(commandEvent, sender)) { + if (cooldownManager != null && !cooldownManager.checkExecution(commandEvent, source.getSender())) { return Command.SINGLE_SUCCESS; } 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 index e3b1dc06b92..15ecfdd0095 100644 --- 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 @@ -64,8 +64,8 @@ public Class[] supportedEvents() { @Override protected void execute(Event event) { if (event instanceof ScriptCommandEvent commandEvent) { - assert commandEvent.getExecutor().getCooldownManager() != null; - commandEvent.getExecutor().getCooldownManager() + assert commandEvent.getCommandExecutor().getCooldownManager() != null; + commandEvent.getCommandExecutor().getCooldownManager() .setStartDate(event, commandEvent.getSender(), cancel ? null : Date.now()); } } 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 index 7b3f8a5e58f..0c9cdc19017 100644 --- 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 @@ -79,7 +79,7 @@ protected Object[] get(Event event) { return (Object[]) Array.newInstance(getReturnType(), 0); } - CooldownManager cooldownManager = commandEvent.getExecutor().getCooldownManager(); + CooldownManager cooldownManager = commandEvent.getCommandExecutor().getCooldownManager(); assert cooldownManager != null; return switch (type) { @@ -130,7 +130,7 @@ public void change(Event event, Object @Nullable [] delta, ChangeMode mode) { return; } - CooldownManager cooldownManager = commandEvent.getExecutor().getCooldownManager(); + CooldownManager cooldownManager = commandEvent.getCommandExecutor().getCooldownManager(); assert cooldownManager != null; Date startDate = cooldownManager.getStartDate(event, commandEvent.getSender()); 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/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/structures/StructCommand.java b/src/main/java/org/skriptlang/skript/bukkit/command/elements/structures/StructCommand.java index 8bf493eee8a..6851d75852a 100644 --- 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 @@ -4,8 +4,13 @@ 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; @@ -33,6 +38,14 @@ public static void register(SkriptAddon addon, SyntaxRegistry syntaxRegistry) { 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 = From 7ea6a1b819155e004021a2ccce3b3b05012f9659 Mon Sep 17 00:00:00 2001 From: Patrick Miller Date: Mon, 6 Jul 2026 23:35:58 -0400 Subject: [PATCH 38/41] Add ExecutableBy.BLOCKS option --- .../ch/njol/util/coll/CollectionUtils.java | 25 +++++++++++++ .../command/custom/CommandParsingData.java | 5 ++- .../bukkit/command/custom/ExecutableBy.java | 37 ++----------------- .../structures/util/SubCommandEntryData.java | 36 +++++++++++------- 4 files changed, 54 insertions(+), 49 deletions(-) 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/command/custom/CommandParsingData.java b/src/main/java/org/skriptlang/skript/bukkit/command/custom/CommandParsingData.java index 7420a6b4e6c..3286371979d 100644 --- a/src/main/java/org/skriptlang/skript/bukkit/command/custom/CommandParsingData.java +++ b/src/main/java/org/skriptlang/skript/bukkit/command/custom/CommandParsingData.java @@ -9,6 +9,7 @@ import java.util.Deque; import java.util.LinkedList; import java.util.List; +import java.util.Set; import java.util.function.Function; /** @@ -17,11 +18,11 @@ public final class CommandParsingData extends Data { /** - * @param executableBy Describes what kind of {@link CommandSender} can execute the command. + * @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 ExecutableBy executableBy, + @Nullable Set executableBy, @Nullable CooldownManager cooldownManager ) { } 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 index f14201d955f..f2abdad6c82 100644 --- a/src/main/java/org/skriptlang/skript/bukkit/command/custom/ExecutableBy.java +++ b/src/main/java/org/skriptlang/skript/bukkit/command/custom/ExecutableBy.java @@ -1,5 +1,6 @@ package org.skriptlang.skript.bukkit.command.custom; +import org.bukkit.command.BlockCommandSender; import org.bukkit.command.CommandSender; import org.bukkit.command.ConsoleCommandSender; import org.bukkit.command.RemoteConsoleCommandSender; @@ -23,14 +24,9 @@ public enum ExecutableBy { CONSOLE(sender -> sender instanceof ConsoleCommandSender || sender instanceof RemoteConsoleCommandSender), /** - * This command is executable by any {@link CommandSender}. + * This command is only executable by blocks (e.g, command blocks). */ - ALL(ignored -> true), - - /** - * This command is not executable. - */ - NONE(ignored -> false); + BLOCKS(sender -> sender instanceof BlockCommandSender); private final Predicate predicate; @@ -45,37 +41,12 @@ public Predicate predicate() { return predicate; } - /** - * Combines (ORs) this ExecutableBy with another. - * For example, {@link #PLAYERS}.with({@link #CONSOLE}) is {@link #ALL}. - * - * @param other Other to OR with. - * @return The resulting ExecutableBy. - */ - public ExecutableBy with(ExecutableBy other) { - if (this == NONE) { - return other; - } else if (other == NONE) { - return this; - } - return this == other ? this : ALL; - } - - /** - * @param other Subset ExecutableBy to consider. - * @return Whether this ExecutableBy is the same as or covers a superset of {@code other}. - */ - public boolean includes(ExecutableBy other) { - return this == ALL || this == other; - } - @Override public String toString() { return switch (this) { case PLAYERS -> "players"; case CONSOLE -> "the console"; - case ALL -> "the console and players"; - case NONE -> "none"; + case BLOCKS -> "blocks"; }; } 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 index ffb496783c2..0f91328a67c 100644 --- 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 @@ -11,6 +11,7 @@ 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; @@ -46,7 +47,9 @@ 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; @@ -88,20 +91,22 @@ protected List getValue(String value) { .addEntryData(new VariableStringEntryData("usage", null, true)) .addEntry("prefix", null, true) .addEntry("permission", null, true) - .addEntryData(new KeyValueEntryData("executable by", null, true) { - private final Pattern pattern = Pattern.compile("\\s*,\\s*|\\s+(and|or)\\s+"); + .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 ExecutableBy getValue(String value) { - ExecutableBy executableBy = ExecutableBy.NONE; + protected Set getValue(String value) { + EnumSet executableBy = EnumSet.noneOf(ExecutableBy.class); for (String type : pattern.split(value)) { if (type.equalsIgnoreCase("console") || type.equalsIgnoreCase("the console")) { - executableBy = executableBy.with(ExecutableBy.CONSOLE); + executableBy.add(ExecutableBy.CONSOLE); } else if (type.equalsIgnoreCase("players") || type.equalsIgnoreCase("player")) { - executableBy = executableBy.with(ExecutableBy.PLAYERS); + executableBy.add(ExecutableBy.PLAYERS); + } else if (type.equalsIgnoreCase("blocks") || type.equalsIgnoreCase("block")) { + executableBy.add(ExecutableBy.BLOCKS); } else { Skript.error("Invalid command sender type: " + type); - return ExecutableBy.NONE; + return Set.of(); } } return executableBy; @@ -228,18 +233,21 @@ public SubCommandEntryData(String key, boolean optional, boolean multiple) { } // executable by requirement - ExecutableBy executableBy = entryContainer.getOptional("executable by", ExecutableBy.class, false); + Set executableBy = entryContainer.getOptional("executable by", Set.class, false); if (executableBy != null) { - if (executableBy == ExecutableBy.NONE) { // parsing failed + if (executableBy.isEmpty()) { // parsing failed return null; } - ExecutableBy parent = parsingData.getExecutorData(ExecutorData::executableBy); - if (parent != null && !parent.includes(executableBy)) { - Skript.error("It is not possible to restrict execution to " + executableBy + - " as the parent command is only executable by " + parent + "."); + Set parent = parsingData.getExecutorData(ExecutorData::executableBy); + if (parent != null && !parent.containsAll(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.predicate()); + requires = requires.and(executableBy.stream() + .map(ExecutableBy::predicate) + .reduce(Predicate::or) + .orElseThrow()); } // prepare final requirements predicate From a0a4c3fcb3fa05d2ca27aa9a80ac0c88f331163b Mon Sep 17 00:00:00 2001 From: Patrick Miller Date: Tue, 7 Jul 2026 00:32:22 -0400 Subject: [PATCH 39/41] Add additional native argument types --- .../command/custom/ScriptArgumentType.java | 43 ++++++++++++++++++- .../structures/util/CommandCompiler.java | 2 +- .../structures/util/SubCommandEntryData.java | 2 +- 3 files changed, 43 insertions(+), 4 deletions(-) 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 index fc86dea5299..9d84b5cdbd9 100644 --- a/src/main/java/org/skriptlang/skript/bukkit/command/custom/ScriptArgumentType.java +++ b/src/main/java/org/skriptlang/skript/bukkit/command/custom/ScriptArgumentType.java @@ -1,6 +1,8 @@ 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; @@ -20,14 +22,22 @@ 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 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; @@ -69,7 +79,7 @@ public NativeArgumentData(Function, ArgumentType> mapper) { /** * Pre-defined mappings of types that are acceptable to map to other native argument types. */ - public static final Map, NativeArgumentData> ARGUMENT_TYPE_MAPPINGS = Map.of( + 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(); @@ -100,9 +110,38 @@ Number.class, new NativeArgumentData(false, true, data -> { 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()) + 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)); 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 index b766ab1e6ea..85ed4ba67c5 100644 --- 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 @@ -406,7 +406,7 @@ private static List appendToLiterals(Collection data = ((ArgumentCommandElement) commandElement).argument(); ArgumentType nativeType = null; - NativeArgumentData nativeMapping = ScriptArgumentType.ARGUMENT_TYPE_MAPPINGS.get(data.type().getC()); + NativeArgumentData nativeMapping = ScriptArgumentType.getNativeData(data.type()); if (nativeMapping != null) { // native argument type may be available nativeType = nativeMapping.mapper().apply(data); } From 813cc99efef871818e67f99fa9e395184d816494 Mon Sep 17 00:00:00 2001 From: Patrick Miller Date: Tue, 7 Jul 2026 14:27:13 -0400 Subject: [PATCH 40/41] Add ExecutableBy.OPERATORS --- .../bukkit/command/custom/ExecutableBy.java | 19 +++++++++++++++++++ .../structures/util/SubCommandEntryData.java | 13 ++++++++----- .../tests/bukkit/command/StructCommand.sk | 14 ++++++++++++++ 3 files changed, 41 insertions(+), 5 deletions(-) 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 index f2abdad6c82..741c18fafe0 100644 --- a/src/main/java/org/skriptlang/skript/bukkit/command/custom/ExecutableBy.java +++ b/src/main/java/org/skriptlang/skript/bukkit/command/custom/ExecutableBy.java @@ -1,11 +1,13 @@ 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; /** @@ -18,6 +20,11 @@ public enum ExecutableBy { */ PLAYERS(sender -> sender instanceof Player), + /** + * This command is only executable by operators. + */ + OPERATORS(CommandSender::isOp), + /** * This command is only executable by console. */ @@ -45,9 +52,21 @@ public Predicate predicate() { 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/elements/structures/util/SubCommandEntryData.java b/src/main/java/org/skriptlang/skript/bukkit/command/elements/structures/util/SubCommandEntryData.java index 637fecb6653..5307ca68b6d 100644 --- 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 @@ -98,11 +98,14 @@ protected List getValue(String value) { protected Set getValue(String value) { EnumSet executableBy = EnumSet.noneOf(ExecutableBy.class); for (String type : pattern.split(value)) { - if (type.equalsIgnoreCase("console") || type.equalsIgnoreCase("the console")) { - executableBy.add(ExecutableBy.CONSOLE); - } else if (type.equalsIgnoreCase("players") || type.equalsIgnoreCase("player")) { + // "player" kept for compatibility + if (type.equalsIgnoreCase("players") || type.equalsIgnoreCase("player")) { executableBy.add(ExecutableBy.PLAYERS); - } else if (type.equalsIgnoreCase("blocks") || type.equalsIgnoreCase("block")) { + } 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); @@ -239,7 +242,7 @@ public SubCommandEntryData(String key, boolean optional, boolean multiple) { return null; } Set parent = parsingData.getExecutorData(ExecutorData::executableBy); - if (parent != null && !parent.containsAll(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; diff --git a/src/test/skript/tests/bukkit/command/StructCommand.sk b/src/test/skript/tests/bukkit/command/StructCommand.sk index dc2d93fe12e..002869fa49d 100644 --- a/src/test/skript/tests/bukkit/command/StructCommand.sk +++ b/src/test/skript/tests/bukkit/command/StructCommand.sk @@ -30,6 +30,19 @@ parse: 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::*} @@ -114,6 +127,7 @@ test "StructCommand": 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 From ea560c611eefb346cec3ddd4ce9e8c9d8fa053af Mon Sep 17 00:00:00 2001 From: Patrick Miller Date: Wed, 8 Jul 2026 14:23:07 -0400 Subject: [PATCH 41/41] Simple tab completion system --- .../skript/bukkit/command/CommandModule.java | 1 + .../command/custom/ScriptArgumentType.java | 26 +--- .../custom/ScriptCommandRegistrar.java | 2 + .../elements/expressions/ExprArgument.java | 15 +- .../expressions/ExprCommandSuggestions.java | 137 ++++++++++++++++++ .../util/CommandSuggestionEvent.java | 30 ++++ .../util/ScriptSuggestionProvider.java | 79 ++++++++++ .../structures/util/SubCommandEntryData.java | 43 +++++- 8 files changed, 307 insertions(+), 26 deletions(-) create mode 100644 src/main/java/org/skriptlang/skript/bukkit/command/elements/expressions/ExprCommandSuggestions.java create mode 100644 src/main/java/org/skriptlang/skript/bukkit/command/elements/structures/util/CommandSuggestionEvent.java create mode 100644 src/main/java/org/skriptlang/skript/bukkit/command/elements/structures/util/ScriptSuggestionProvider.java diff --git a/src/main/java/org/skriptlang/skript/bukkit/command/CommandModule.java b/src/main/java/org/skriptlang/skript/bukkit/command/CommandModule.java index 8df15872ce0..f45c7f09211 100644 --- a/src/main/java/org/skriptlang/skript/bukkit/command/CommandModule.java +++ b/src/main/java/org/skriptlang/skript/bukkit/command/CommandModule.java @@ -37,6 +37,7 @@ protected void loadSelf(SkriptAddon addon) { ExprCommandExecutor::register, ExprCommandInfo::register, ExprCommandSender::register, + ExprCommandSuggestions::register, syntaxRegistry -> StructCommand.register(addon, syntaxRegistry) ); } 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 index 9d84b5cdbd9..e87fe59e3d0 100644 --- a/src/main/java/org/skriptlang/skript/bukkit/command/custom/ScriptArgumentType.java +++ b/src/main/java/org/skriptlang/skript/bukkit/command/custom/ScriptArgumentType.java @@ -33,6 +33,7 @@ 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; @@ -193,26 +194,11 @@ public ScriptArgumentType(@NotNull ArgumentData argument, @NotNull StringArgu if (supplier == null) { return Suggestions.empty(); } - - // treat as a valid match for - // treat <"foo b> as a valid match for - supplier.get().forEachRemaining(value -> { - if (value == null) { - return; - } - String name = Classes.toString(value).toLowerCase(Locale.ENGLISH) - .replace(' ', '_'); - String remaining = builder.getRemainingLowerCase(); - if (!remaining.isEmpty() && remaining.charAt(0) == '"') { - remaining = remaining.substring(1); - } - if (remaining.contains(" ")) { - remaining = remaining.replace(' ', '_'); - } - if (name.startsWith(remaining)) { - builder.suggest(name); - } - }); + 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/ScriptCommandRegistrar.java b/src/main/java/org/skriptlang/skript/bukkit/command/custom/ScriptCommandRegistrar.java index c850b2bad20..d8e722eaa66 100644 --- a/src/main/java/org/skriptlang/skript/bukkit/command/custom/ScriptCommandRegistrar.java +++ b/src/main/java/org/skriptlang/skript/bukkit/command/custom/ScriptCommandRegistrar.java @@ -13,6 +13,7 @@ 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; @@ -50,6 +51,7 @@ public final class ScriptCommandRegistrar { private static final SkriptIndexHelpTopic indexHelpTopic = new SkriptIndexHelpTopic(); + @ApiStatus.Internal public static void init(JavaPlugin plugin) { plugin.getLifecycleManager().registerEventHandler(LifecycleEvents.COMMANDS, commands -> { commandRegistrar = commands.registrar(); 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 index 6fa963a558c..219c1c289d1 100644 --- 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 @@ -22,6 +22,7 @@ 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; @@ -69,14 +70,14 @@ private enum ArgumentType { private ArgumentType type; private int ordinal = -1; // Available in ORDINAL and sometimes CLASSINFO - private @Nullable ArgumentData argument; + @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); + boolean scriptCommand = parser.isCurrentEvent(ScriptCommandEvent.class, CommandSuggestionEvent.class); type = switch (matchedPattern) { case 0 -> ArgumentType.LAST; @@ -192,11 +193,19 @@ public boolean init(Expression[] exprs, int matchedPattern, Kleenean isDelaye @Override public Class[] supportedEvents() { - return CollectionUtils.array(ScriptCommandEvent.class, PlayerCommandPreprocessEvent.class, ServerCommandEvent.class); + // 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()) { 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/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 index 5307ca68b6d..bdc046eed86 100644 --- 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 @@ -15,6 +15,7 @@ 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; @@ -90,6 +91,7 @@ protected List getValue(String value) { .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+"); @@ -327,6 +329,17 @@ public SubCommandEntryData(String key, boolean optional, boolean multiple) { } 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) { @@ -347,7 +360,7 @@ public SubCommandEntryData(String key, boolean optional, boolean multiple) { //noinspection rawtypes var result = (List) compilationResult.root().children().stream() - .map(child -> parse(child, executor, commandRequires, subcommands)) + .map(child -> parse(child, executor, commandRequires, suggestionProvider, subcommands)) .toList(); parsingData.popExecutorData(); @@ -370,10 +383,20 @@ public boolean canCreateWith(Node node) { 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(); @@ -384,6 +407,8 @@ public boolean canCreateWith(Node node) { } 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 @@ -398,18 +423,30 @@ public boolean canCreateWith(Node node) { nativeType = new ScriptArgumentType<>(data, (StringArgumentType) nativeType); } - argument = Commands.argument(data.name(), 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; } - argument.then(parse(element, executor, requires, subcommands)); + // 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