package net.minestom.server.command.builder; import com.google.gson.JsonArray; import com.google.gson.JsonObject; import net.minestom.server.command.CommandSender; import net.minestom.server.command.builder.arguments.Argument; import net.minestom.server.command.builder.arguments.ArgumentLiteral; import net.minestom.server.command.builder.arguments.ArgumentType; import net.minestom.server.command.builder.arguments.ArgumentWord; import net.minestom.server.command.builder.condition.CommandCondition; import net.minestom.server.utils.StringUtils; import org.jetbrains.annotations.ApiStatus; import org.jetbrains.annotations.NotNull; import org.jetbrains.annotations.Nullable; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import java.util.*; import java.util.function.BiConsumer; import java.util.function.BiFunction; import java.util.function.Consumer; import java.util.function.Supplier; import java.util.stream.Stream; /** * Represents a command which has suggestion/auto-completion. *

* The command works using a list of valid syntaxes. * For instance we could build the command * "/health set Notch 50" into multiple argument types "/health [set/add/remove] [username] [integer]" *

* All the default argument types can be found in {@link ArgumentType} * and the syntax be created/registered using {@link #addSyntax(CommandExecutor, Argument[])}. *

* If the command is executed with an incorrect syntax or without any argument, the default {@link CommandExecutor} will be called, * you can set it using {@link #setDefaultExecutor(CommandExecutor)}. *

* Before any syntax to be successfully executed the {@link CommandSender} needs to validated * the {@link CommandCondition} sets with {@link #setCondition(CommandCondition)} (ignored if null). *

* Some {@link Argument} could also require additional condition (eg: a number which need to be between 2 values), * in this case, if the whole syntax is correct but not the argument condition, * you can listen to its error code using {@link #setArgumentCallback(ArgumentCallback, Argument)} or {@link Argument#setCallback(ArgumentCallback)}. */ public class Command { public final static Logger LOGGER = LoggerFactory.getLogger(Command.class); private final String name; private final String[] aliases; private final String[] names; private CommandExecutor defaultExecutor; private CommandCondition condition; private final List subcommands; private final List syntaxes; /** * Creates a {@link Command} with a name and one or multiple aliases. * * @param name the name of the command * @param aliases the command aliases * @see #Command(String) */ public Command(@NotNull String name, @Nullable String... aliases) { this.name = name; this.aliases = aliases; this.names = Stream.concat(Arrays.stream(aliases), Stream.of(name)).toArray(String[]::new); this.subcommands = new ArrayList<>(); this.syntaxes = new ArrayList<>(); } /** * Creates a {@link Command} with a name and no alias. * * @param name the name of the command * @see #Command(String, String...) */ public Command(@NotNull String name) { this(name, new String[0]); } /** * Gets the {@link CommandCondition}. *

* It is called after the parsing and just before the execution no matter the syntax used and can be used to check permissions or * the {@link CommandSender} type. *

* Worth mentioning that the condition is also used to know if the command known from a player (at connection). * * @return the command condition, null if not any */ @Nullable public CommandCondition getCondition() { return condition; } /** * Sets the {@link CommandCondition}. * * @param commandCondition the new command condition, null to do not call anything * @see #getCondition() */ public void setCondition(@Nullable CommandCondition commandCondition) { this.condition = commandCondition; } /** * Sets an {@link ArgumentCallback}. *

* The argument callback is called when there's an error in the argument. * * @param callback the callback for the argument * @param argument the argument which get the callback */ public void setArgumentCallback(@NotNull ArgumentCallback callback, @NotNull Argument argument) { argument.setCallback(callback); } public void addSubcommand(@NotNull Command command) { this.subcommands.add(command); } @NotNull public List getSubcommands() { return Collections.unmodifiableList(subcommands); } /** * Adds a new syntax in the command. *

* A syntax is simply a list of arguments and an executor called when successfully parsed. * * @param commandCondition the condition to use the syntax * @param executor the executor to call when the syntax is successfully received * @param args all the arguments of the syntax, the length needs to be higher than 0 * @return the created {@link CommandSyntax syntaxes}, * there can be multiple of them when optional arguments are used */ @NotNull public Collection addConditionalSyntax(@Nullable CommandCondition commandCondition, @NotNull CommandExecutor executor, @NotNull Argument... args) { // Check optional argument(s) boolean hasOptional = false; { for (Argument argument : args) { if (argument.isOptional()) { hasOptional = true; } if (hasOptional && !argument.isOptional()) { LOGGER.warn("Optional arguments are followed by a non-optional one, the default values will be ignored."); hasOptional = false; break; } } } if (!hasOptional) { final CommandSyntax syntax = new CommandSyntax(commandCondition, executor, args); this.syntaxes.add(syntax); return List.of(syntax); } else { List optionalSyntaxes = new ArrayList<>(); // the 'args' array starts by all the required arguments, followed by the optional ones List> requiredArguments = new ArrayList<>(); Map> defaultValuesMap = new HashMap<>(); boolean optionalBranch = false; int i = 0; for (Argument argument : args) { final boolean isLast = ++i == args.length; if (argument.isOptional()) { // Set default value defaultValuesMap.put(argument.getId(), (Supplier) argument.getDefaultValue()); if (!optionalBranch && !requiredArguments.isEmpty()) { // First optional argument, create a syntax with current cached arguments final CommandSyntax syntax = new CommandSyntax(commandCondition, executor, defaultValuesMap, requiredArguments.toArray(new Argument[0])); optionalSyntaxes.add(syntax); optionalBranch = true; } else { // New optional argument, save syntax with current cached arguments and save default value final CommandSyntax syntax = new CommandSyntax(commandCondition, executor, defaultValuesMap, requiredArguments.toArray(new Argument[0])); optionalSyntaxes.add(syntax); } } requiredArguments.add(argument); if (isLast) { // Create the last syntax final CommandSyntax syntax = new CommandSyntax(commandCondition, executor, defaultValuesMap, requiredArguments.toArray(new Argument[0])); optionalSyntaxes.add(syntax); } } this.syntaxes.addAll(optionalSyntaxes); return optionalSyntaxes; } } /** * Adds a new syntax without condition. * * @see #addConditionalSyntax(CommandCondition, CommandExecutor, Argument[]) */ public @NotNull Collection addSyntax(@NotNull CommandExecutor executor, @NotNull Argument... args) { return addConditionalSyntax(null, executor, args); } /** * Creates a syntax from a formatted string. *

* Currently in beta as the format is not final. * * @param executor the syntax executor * @param format the syntax format * @return the newly created {@link CommandSyntax syntaxes}. */ @ApiStatus.Experimental public @NotNull Collection addSyntax(@NotNull CommandExecutor executor, @NotNull String format) { return addSyntax(executor, ArgumentType.generate(format)); } /** * Gets the main command's name. * * @return the main command's name */ public @NotNull String getName() { return name; } /** * Gets the command's aliases. * * @return the command aliases, can be null or empty */ public @Nullable String[] getAliases() { return aliases; } /** * Gets all the possible names for this command. *

* Include {@link #getName()} and {@link #getAliases()}. * * @return this command names */ public @NotNull String[] getNames() { return names; } /** * Gets the default {@link CommandExecutor} which is called when there is no argument * or if no corresponding syntax has been found. * * @return the default executor, null if not any * @see #setDefaultExecutor(CommandExecutor) */ @Nullable public CommandExecutor getDefaultExecutor() { return defaultExecutor; } /** * Sets the default {@link CommandExecutor}. * * @param executor the new default executor, null to remove it * @see #getDefaultExecutor() */ public void setDefaultExecutor(@Nullable CommandExecutor executor) { this.defaultExecutor = executor; } /** * Gets all the syntaxes of this command. * * @return a collection containing all this command syntaxes * @see #addSyntax(CommandExecutor, Argument[]) */ public @NotNull Collection getSyntaxes() { return syntaxes; } /** * Called when a {@link CommandSender} executes this command before any syntax callback. *

* WARNING: the {@link CommandCondition} is not executed, and all the {@link CommandSyntax} are not checked, * this is called every time a {@link CommandSender} send a command which start by {@link #getName()} or {@link #getAliases()}. *

* Can be used if you wish to still suggest the player syntaxes but want to parse things mostly by yourself. * * @param sender the {@link CommandSender} * @param context the UNCHECKED context of the command, some can be null even when unexpected * @param command the raw UNCHECKED received command */ public void globalListener(@NotNull CommandSender sender, @NotNull CommandContext context, @NotNull String command) { } @ApiStatus.Experimental public @NotNull Set getSyntaxesStrings() { Set syntaxes = new HashSet<>(); Consumer syntaxConsumer = syntaxString -> { for (String name : getNames()) { final String syntax = name + StringUtils.SPACE + syntaxString; syntaxes.add(syntax); } }; this.subcommands.forEach(subcommand -> subcommand.getSyntaxesStrings().forEach(syntaxConsumer)); this.syntaxes.forEach(commandSyntax -> syntaxConsumer.accept(commandSyntax.getSyntaxString())); return syntaxes; } @ApiStatus.Experimental public @NotNull String getSyntaxesTree() { Node commandNode = new Node(); commandNode.names.addAll(Arrays.asList(getNames())); // current node, literal = returned node BiFunction, Node> findNode = (currentNode, literals) -> { for (Node node : currentNode.nodes) { final var names = node.names; // Verify if at least one literal is shared final boolean shared = names.stream().anyMatch(literals::contains); if (shared) { names.addAll(literals); return node; } } // Create a new node Node node = new Node(); node.names.addAll(literals); currentNode.nodes.add(node); return node; }; BiConsumer syntaxProcessor = (syntax, node) -> { List arguments = new ArrayList<>(); BiConsumer> addArguments = (n, args) -> { if (!args.isEmpty()) { n.arguments.add(args); } }; // true if all following arguments are not part of // the branching plant (literals) boolean branched = false; for (Argument argument : syntax.getArguments()) { if (!branched) { if (argument instanceof ArgumentLiteral) { final String literal = argument.getId(); addArguments.accept(node, arguments); arguments = new ArrayList<>(); node = findNode.apply(node, Set.of(literal)); continue; } else if (argument instanceof ArgumentWord argumentWord) { if (argumentWord.hasRestrictions()) { addArguments.accept(node, arguments); arguments = new ArrayList<>(); node = findNode.apply(node, Set.of(argumentWord.getRestrictions())); continue; } } } branched = true; arguments.add(argument.toString()); } addArguments.accept(node, arguments); }; // Subcommands this.subcommands.forEach(command -> { final Node node = findNode.apply(commandNode, Set.of(command.getNames())); command.getSyntaxes().forEach(syntax -> syntaxProcessor.accept(syntax, node)); }); // Syntaxes this.syntaxes.forEach(syntax -> syntaxProcessor.accept(syntax, commandNode)); JsonObject jsonObject = new JsonObject(); processNode(commandNode, jsonObject); return jsonObject.toString(); } public static boolean isValidName(@NotNull Command command, @NotNull String name) { for (String commandName : command.getNames()) { if (commandName.equals(name)) { return true; } } return false; } private void processNode(@NotNull Node node, @NotNull JsonObject jsonObject) { BiConsumer> processor = (s, consumer) -> { JsonArray array = new JsonArray(); consumer.accept(array); if (array.size() != 0) { jsonObject.add(s, array); } }; // Names processor.accept("names", array -> node.names.forEach(array::add)); // Nodes processor.accept("nodes", array -> node.nodes.forEach(n -> { JsonObject nodeObject = new JsonObject(); processNode(n, nodeObject); array.add(nodeObject); })); // Arguments processor.accept("arguments", array -> node.arguments.forEach(arguments -> array.add(String.join(StringUtils.SPACE, arguments)))); } private static final class Node { private final Set names = new HashSet<>(); private final Set nodes = new HashSet<>(); private final List> arguments = new ArrayList<>(); } }