All Downloads are FREE. Search and download functionalities are using the official Maven repository.

com.mojang.brigadier.CommandDispatcher Maven / Gradle / Ivy

There is a newer version: 1.09.0
Show newest version
// Copyright (c) Microsoft Corporation. All rights reserved.
// Licensed under the MIT license.

package com.mojang.brigadier;

import com.mojang.brigadier.builder.LiteralArgumentBuilder;
import com.mojang.brigadier.context.CommandContext;
import com.mojang.brigadier.context.CommandContextBuilder;
import com.mojang.brigadier.context.SuggestionContext;
import com.mojang.brigadier.exceptions.CommandSyntaxException;
import com.mojang.brigadier.suggestion.Suggestions;
import com.mojang.brigadier.suggestion.SuggestionsBuilder;
import com.mojang.brigadier.tree.CommandNode;
import com.mojang.brigadier.tree.LiteralCommandNode;
import com.mojang.brigadier.tree.RootCommandNode;

import java.util.ArrayList;
import java.util.Collection;
import java.util.Collections;
import java.util.LinkedHashMap;
import java.util.LinkedHashSet;
import java.util.List;
import java.util.Locale;
import java.util.Map;
import java.util.Set;
import java.util.concurrent.CompletableFuture;
import java.util.function.Predicate;
import java.util.stream.Collectors;


/**
 * The core command dispatcher, for registering, parsing, and executing commands.
 *
 * @param  a custom "source" type, such as a user or originator of a command
 */
public class CommandDispatcher {
    /**
     * The string required to separate individual arguments in an input string
     *
     * @see #ARGUMENT_SEPARATOR_CHAR
     */
    public static final String ARGUMENT_SEPARATOR = " ";

    /**
     * The char required to separate individual arguments in an input string
     *
     * @see #ARGUMENT_SEPARATOR
     */
    public static final char ARGUMENT_SEPARATOR_CHAR = ' ';

    private static final String USAGE_OPTIONAL_OPEN = "[";
    private static final String USAGE_OPTIONAL_CLOSE = "]";
    private static final String USAGE_REQUIRED_OPEN = "(";
    private static final String USAGE_REQUIRED_CLOSE = ")";
    private static final String USAGE_OR = "|";

    private final RootCommandNode root;

    private final Predicate> hasCommand = new Predicate>() {
        @Override
        public boolean test(final CommandNode input) {
            return input != null && (input.getCommand() != null || input.getChildren().stream().anyMatch(hasCommand));
        }
    };
    private ResultConsumer consumer = (c, s, r) -> {
    };

    /**
     * Create a new {@link CommandDispatcher} with the specified root node.
     *
     * 

This is often useful to copy existing or pre-defined command trees.

* * @param root the existing {@link RootCommandNode} to use as the basis for this tree */ public CommandDispatcher(final RootCommandNode root) { this.root = root; } /** * Creates a new {@link CommandDispatcher} with an empty command tree. */ public CommandDispatcher() { this(new RootCommandNode<>()); } /** * Utility method for registering new commands. * *

This is a shortcut for calling {@link RootCommandNode#addChild(CommandNode)} after building the provided {@code command}.

* *

As {@link RootCommandNode} can only hold literals, this method will only allow literal arguments.

* * @param command a literal argument builder to add to this command tree * @return the node added to this tree */ public LiteralCommandNode register(final LiteralArgumentBuilder command) { final LiteralCommandNode build = command.build(); root.addChild(build); return build; } /** * Sets a callback to be informed of the result of every command. * * @param consumer the new result consumer to be called */ public void setConsumer(final ResultConsumer consumer) { this.consumer = consumer; } /** * Parses and executes a given command. * *

This is a shortcut to first {@link #parse(StringReader, Object)} and then {@link #execute(ParseResults)}.

* *

It is recommended to parse and execute as separate steps, as parsing is often the most expensive step, and easiest to cache.

* *

If this command returns a value, then it successfully executed something. If it could not parse the command, or the execution was a failure, * then an exception will be thrown. Most exceptions will be of type {@link CommandSyntaxException}, but it is possible that a {@link RuntimeException} * may bubble up from the result of a command. The meaning behind the returned result is arbitrary, and will depend * entirely on what command was performed.

* *

If the command passes through a node that is {@link CommandNode#isFork()} then it will be 'forked'. * A forked command will not bubble up any {@link CommandSyntaxException}s, and the 'result' returned will turn into * 'amount of successful commands executes'.

* *

After each and any command is ran, a registered callback given to {@link #setConsumer(ResultConsumer)} * will be notified of the result and success of the command. You can use that method to gather more meaningful * results than this method will return, especially when a command forks.

* * @param input a command string to parse & execute * @param source a custom "source" object, usually representing the originator of this command * @return a numeric result from a "command" that was performed * @throws CommandSyntaxException if the command failed to parse or execute * @throws RuntimeException if the command failed to execute and was not handled gracefully * @see #parse(String, Object) * @see #parse(StringReader, Object) * @see #execute(ParseResults) * @see #execute(StringReader, Object) */ public int execute(final String input, final S source) throws CommandSyntaxException { return execute(new StringReader(input), source); } /** * Parses and executes a given command. * *

This is a shortcut to first {@link #parse(StringReader, Object)} and then {@link #execute(ParseResults)}.

* *

It is recommended to parse and execute as separate steps, as parsing is often the most expensive step, and easiest to cache.

* *

If this command returns a value, then it successfully executed something. If it could not parse the command, or the execution was a failure, * then an exception will be thrown. Most exceptions will be of type {@link CommandSyntaxException}, but it is possible that a {@link RuntimeException} * may bubble up from the result of a command. The meaning behind the returned result is arbitrary, and will depend * entirely on what command was performed.

* *

If the command passes through a node that is {@link CommandNode#isFork()} then it will be 'forked'. * A forked command will not bubble up any {@link CommandSyntaxException}s, and the 'result' returned will turn into * 'amount of successful commands executes'.

* *

After each and any command is ran, a registered callback given to {@link #setConsumer(ResultConsumer)} * will be notified of the result and success of the command. You can use that method to gather more meaningful * results than this method will return, especially when a command forks.

* * @param input a command string to parse & execute * @param source a custom "source" object, usually representing the originator of this command * @return a numeric result from a "command" that was performed * @throws CommandSyntaxException if the command failed to parse or execute * @throws RuntimeException if the command failed to execute and was not handled gracefully * @see #parse(String, Object) * @see #parse(StringReader, Object) * @see #execute(ParseResults) * @see #execute(String, Object) */ public int execute(final StringReader input, final S source) throws CommandSyntaxException { final ParseResults parse = parse(input, source); return execute(parse); } /** * Executes a given pre-parsed command. * *

If this command returns a value, then it successfully executed something. If the execution was a failure, * then an exception will be thrown. * Most exceptions will be of type {@link CommandSyntaxException}, but it is possible that a {@link RuntimeException} * may bubble up from the result of a command. The meaning behind the returned result is arbitrary, and will depend * entirely on what command was performed.

* *

If the command passes through a node that is {@link CommandNode#isFork()} then it will be 'forked'. * A forked command will not bubble up any {@link CommandSyntaxException}s, and the 'result' returned will turn into * 'amount of successful commands executes'.

* *

After each and any command is ran, a registered callback given to {@link #setConsumer(ResultConsumer)} * will be notified of the result and success of the command. You can use that method to gather more meaningful * results than this method will return, especially when a command forks.

* * @param parse the result of a successful {@link #parse(StringReader, Object)} * @return a numeric result from a "command" that was performed. * @throws CommandSyntaxException if the command failed to parse or execute * @throws RuntimeException if the command failed to execute and was not handled gracefully * @see #parse(String, Object) * @see #parse(StringReader, Object) * @see #execute(String, Object) * @see #execute(StringReader, Object) */ public int execute(final ParseResults parse) throws CommandSyntaxException { if (parse.getReader().canRead()) { if (parse.getExceptions().size() == 1) { throw parse.getExceptions().values().iterator().next(); } else if (parse.getContext().getRange().isEmpty()) { throw CommandSyntaxException.BUILT_IN_EXCEPTIONS.dispatcherUnknownCommand().createWithContext(parse.getReader()); } else { throw CommandSyntaxException.BUILT_IN_EXCEPTIONS.dispatcherUnknownArgument().createWithContext(parse.getReader()); } } int result = 0; int successfulForks = 0; boolean forked = false; boolean foundCommand = false; final String command = parse.getReader().getString(); final CommandContext original = parse.getContext().build(command); List> contexts = Collections.singletonList(original); ArrayList> next = null; while (contexts != null) { final int size = contexts.size(); for (int i = 0; i < size; i++) { final CommandContext context = contexts.get(i); final CommandContext child = context.getChild(); if (child != null) { forked |= context.isForked(); if (child.hasNodes()) { foundCommand = true; final RedirectModifier modifier = context.getRedirectModifier(); if (modifier == null) { if (next == null) { next = new ArrayList<>(1); } next.add(child.copyFor(context.getSource())); } else { try { final Collection results = modifier.apply(context); if (!results.isEmpty()) { if (next == null) { next = new ArrayList<>(results.size()); } for (final S source : results) { next.add(child.copyFor(source)); } } } catch (final CommandSyntaxException ex) { consumer.onCommandComplete(context, false, 0); if (!forked) { throw ex; } } } } } else if (context.getCommand() != null) { foundCommand = true; try { final int value = context.getCommand().run(context); result += value; consumer.onCommandComplete(context, true, value); successfulForks++; } catch (final CommandSyntaxException ex) { consumer.onCommandComplete(context, false, 0); if (!forked) { throw ex; } } } } contexts = next; next = null; } if (!foundCommand) { consumer.onCommandComplete(original, false, 0); throw CommandSyntaxException.BUILT_IN_EXCEPTIONS.dispatcherUnknownCommand().createWithContext(parse.getReader()); } return forked ? successfulForks : result; } /** * Parses a given command. * *

The result of this method can be cached, and it is advised to do so where appropriate. Parsing is often the * most expensive step, and this allows you to essentially "precompile" a command if it will be ran often.

* *

If the command passes through a node that is {@link CommandNode#isFork()} then the resulting context will be marked as 'forked'. * Forked contexts may contain child contexts, which may be modified by the {@link RedirectModifier} attached to the fork.

* *

Parsing a command can never fail, you will always be provided with a new {@link ParseResults}. * However, that does not mean that it will always parse into a valid command. You should inspect the returned results * to check for validity. If its {@link ParseResults#getReader()} {@link StringReader#canRead()} then it did not finish * parsing successfully. You can use that position as an indicator to the user where the command stopped being valid. * You may inspect {@link ParseResults#getExceptions()} if you know the parse failed, as it will explain why it could * not find any valid commands. It may contain multiple exceptions, one for each "potential node" that it could have visited, * explaining why it did not go down that node.

* *

When you eventually call {@link #execute(ParseResults)} with the result of this method, the above error checking * will occur. You only need to inspect it yourself if you wish to handle that yourself.

* * @param command a command string to parse * @param source a custom "source" object, usually representing the originator of this command * @return the result of parsing this command * @see #parse(StringReader, Object) * @see #execute(ParseResults) * @see #execute(String, Object) */ public ParseResults parse(final String command, final S source) { return parse(new StringReader(command), source); } /** * Parses a given command. * *

The result of this method can be cached, and it is advised to do so where appropriate. Parsing is often the * most expensive step, and this allows you to essentially "precompile" a command if it will be ran often.

* *

If the command passes through a node that is {@link CommandNode#isFork()} then the resulting context will be marked as 'forked'. * Forked contexts may contain child contexts, which may be modified by the {@link RedirectModifier} attached to the fork.

* *

Parsing a command can never fail, you will always be provided with a new {@link ParseResults}. * However, that does not mean that it will always parse into a valid command. You should inspect the returned results * to check for validity. If its {@link ParseResults#getReader()} {@link StringReader#canRead()} then it did not finish * parsing successfully. You can use that position as an indicator to the user where the command stopped being valid. * You may inspect {@link ParseResults#getExceptions()} if you know the parse failed, as it will explain why it could * not find any valid commands. It may contain multiple exceptions, one for each "potential node" that it could have visited, * explaining why it did not go down that node.

* *

When you eventually call {@link #execute(ParseResults)} with the result of this method, the above error checking * will occur. You only need to inspect it yourself if you wish to handle that yourself.

* * @param command a command string to parse * @param source a custom "source" object, usually representing the originator of this command * @return the result of parsing this command * @see #parse(String, Object) * @see #execute(ParseResults) * @see #execute(String, Object) */ public ParseResults parse(final StringReader command, final S source) { final CommandContextBuilder context = new CommandContextBuilder<>(this, source, root, command.getCursor()); return parseNodes(root, command, context); } private ParseResults parseNodes(final CommandNode node, final StringReader originalReader, final CommandContextBuilder contextSoFar) { final S source = contextSoFar.getSource(); Map, CommandSyntaxException> errors = null; List> potentials = null; final int cursor = originalReader.getCursor(); for (final CommandNode child : node.getRelevantNodes(originalReader)) { if (!child.canUse(source)) { continue; } final CommandContextBuilder context = contextSoFar.copy(); final StringReader reader = new StringReader(originalReader); try { try { child.parse(reader, context); } catch (final RuntimeException ex) { throw CommandSyntaxException.BUILT_IN_EXCEPTIONS.dispatcherParseException().createWithContext(reader, ex.getMessage()); } if (reader.canRead()) { if (reader.peek() != ARGUMENT_SEPARATOR_CHAR) { throw CommandSyntaxException.BUILT_IN_EXCEPTIONS.dispatcherExpectedArgumentSeparator().createWithContext(reader); } } } catch (final CommandSyntaxException ex) { if (errors == null) { errors = new LinkedHashMap<>(); } errors.put(child, ex); reader.setCursor(cursor); continue; } context.withCommand(child.getCommand()); if (reader.canRead(child.getRedirect() == null ? 2 : 1)) { reader.skip(); if (child.getRedirect() != null) { final CommandContextBuilder childContext = new CommandContextBuilder<>(this, source, child.getRedirect(), reader.getCursor()); final ParseResults parse = parseNodes(child.getRedirect(), reader, childContext); context.withChild(parse.getContext()); return new ParseResults<>(context, parse.getReader(), parse.getExceptions()); } else { final ParseResults parse = parseNodes(child, reader, context); if (potentials == null) { potentials = new ArrayList<>(1); } potentials.add(parse); } } else { if (potentials == null) { potentials = new ArrayList<>(1); } potentials.add(new ParseResults<>(context, reader, Collections.emptyMap())); } } if (potentials != null) { if (potentials.size() > 1) { potentials.sort((a, b) -> { if (!a.getReader().canRead() && b.getReader().canRead()) { return -1; } if (a.getReader().canRead() && !b.getReader().canRead()) { return 1; } if (a.getExceptions().isEmpty() && !b.getExceptions().isEmpty()) { return -1; } if (!a.getExceptions().isEmpty() && b.getExceptions().isEmpty()) { return 1; } return 0; }); } return potentials.get(0); } return new ParseResults<>(contextSoFar, originalReader, errors == null ? Collections.emptyMap() : errors); } /** * Gets all possible executable commands following the given node. * *

You may use {@link #getRoot()} as a target to get all usage data for the entire command tree.

* *

The returned syntax will be in "simple" form: {@code } and {@code literal}. "Optional" nodes will be * listed as multiple entries: the parent node, and the child nodes. * For example, a required literal "foo" followed by an optional param "int" will be two nodes:

*
    *
  • {@code foo}
  • *
  • {@code foo }
  • *
* *

The path to the specified node will not be prepended to the output, as there can theoretically be many * ways to reach a given node. It will only give you paths relative to the specified node, not absolute from root.

* * @param node target node to get child usage strings for * @param source a custom "source" object, usually representing the originator of this command * @param restricted if true, commands that the {@code source} cannot access will not be mentioned * @return array of full usage strings under the target node */ public String[] getAllUsage(final CommandNode node, final S source, final boolean restricted) { final ArrayList result = new ArrayList<>(); getAllUsage(node, source, result, "", restricted); return result.toArray(new String[result.size()]); } private void getAllUsage(final CommandNode node, final S source, final ArrayList result, final String prefix, final boolean restricted) { if (restricted && !node.canUse(source)) { return; } if (node.getCommand() != null) { result.add(prefix); } if (node.getRedirect() != null) { final String redirect = node.getRedirect() == root ? "..." : "-> " + node.getRedirect().getUsageText(); result.add(prefix.isEmpty() ? node.getUsageText() + ARGUMENT_SEPARATOR + redirect : prefix + ARGUMENT_SEPARATOR + redirect); } else if (!node.getChildren().isEmpty()) { for (final CommandNode child : node.getChildren()) { getAllUsage(child, source, result, prefix.isEmpty() ? child.getUsageText() : prefix + ARGUMENT_SEPARATOR + child.getUsageText(), restricted); } } } /** * Gets the possible executable commands from a specified node. * *

You may use {@link #getRoot()} as a target to get usage data for the entire command tree.

* *

The returned syntax will be in "smart" form: {@code }, {@code literal}, {@code [optional]} and {@code (either|or)}. * These forms may be mixed and matched to provide as much information about the child nodes as it can, without being too verbose. * For example, a required literal "foo" followed by an optional param "int" can be compressed into one string:

*
    *
  • {@code foo []}
  • *
* *

The path to the specified node will not be prepended to the output, as there can theoretically be many * ways to reach a given node. It will only give you paths relative to the specified node, not absolute from root.

* *

The returned usage will be restricted to only commands that the provided {@code source} can use.

* * @param node target node to get child usage strings for * @param source a custom "source" object, usually representing the originator of this command * @return array of full usage strings under the target node */ public Map, String> getSmartUsage(final CommandNode node, final S source) { final Map, String> result = new LinkedHashMap<>(); final boolean optional = node.getCommand() != null; for (final CommandNode child : node.getChildren()) { final String usage = getSmartUsage(child, source, optional, false); if (usage != null) { result.put(child, usage); } } return result; } private String getSmartUsage(final CommandNode node, final S source, final boolean optional, final boolean deep) { if (!node.canUse(source)) { return null; } final String self = optional ? USAGE_OPTIONAL_OPEN + node.getUsageText() + USAGE_OPTIONAL_CLOSE : node.getUsageText(); final boolean childOptional = node.getCommand() != null; final String open = childOptional ? USAGE_OPTIONAL_OPEN : USAGE_REQUIRED_OPEN; final String close = childOptional ? USAGE_OPTIONAL_CLOSE : USAGE_REQUIRED_CLOSE; if (!deep) { if (node.getRedirect() != null) { final String redirect = node.getRedirect() == root ? "..." : "-> " + node.getRedirect().getUsageText(); return self + ARGUMENT_SEPARATOR + redirect; } else { final Collection> children = node.getChildren().stream().filter(c -> c.canUse(source)).collect(Collectors.toList()); if (children.size() == 1) { final String usage = getSmartUsage(children.iterator().next(), source, childOptional, childOptional); if (usage != null) { return self + ARGUMENT_SEPARATOR + usage; } } else if (children.size() > 1) { final Set childUsage = new LinkedHashSet<>(); for (final CommandNode child : children) { final String usage = getSmartUsage(child, source, childOptional, true); if (usage != null) { childUsage.add(usage); } } if (childUsage.size() == 1) { final String usage = childUsage.iterator().next(); return self + ARGUMENT_SEPARATOR + (childOptional ? USAGE_OPTIONAL_OPEN + usage + USAGE_OPTIONAL_CLOSE : usage); } else if (childUsage.size() > 1) { final StringBuilder builder = new StringBuilder(open); int count = 0; for (final CommandNode child : children) { if (count > 0) { builder.append(USAGE_OR); } builder.append(child.getUsageText()); count++; } if (count > 0) { builder.append(close); return self + ARGUMENT_SEPARATOR + builder.toString(); } } } } } return self; } /** * Gets suggestions for a parsed input string on what comes next. * *

As it is ultimately up to custom argument types to provide suggestions, it may be an asynchronous operation, * for example getting in-game data or player names etc. As such, this method returns a future and no guarantees * are made to when or how the future completes.

* *

The suggestions provided will be in the context of the end of the parsed input string, but may suggest * new or replacement strings for earlier in the input string. For example, if the end of the string was * {@code foobar} but an argument preferred it to be {@code minecraft:foobar}, it will suggest a replacement for that * whole segment of the input.

* * @param parse the result of a {@link #parse(StringReader, Object)} * @return a future that will eventually resolve into a {@link Suggestions} object */ public CompletableFuture getCompletionSuggestions(final ParseResults parse) { return getCompletionSuggestions(parse, parse.getReader().getTotalLength()); } public CompletableFuture getCompletionSuggestions(final ParseResults parse, int cursor) { final CommandContextBuilder context = parse.getContext(); final SuggestionContext nodeBeforeCursor = context.findSuggestionContext(cursor); final CommandNode parent = nodeBeforeCursor.parent; final int start = Math.min(nodeBeforeCursor.startPos, cursor); final String fullInput = parse.getReader().getString(); final String truncatedInput = fullInput.substring(0, cursor); final String truncatedInputLowerCase = truncatedInput.toLowerCase(Locale.ROOT); @SuppressWarnings("unchecked") final CompletableFuture[] futures = new CompletableFuture[parent.getChildren().size()]; int i = 0; for (final CommandNode node : parent.getChildren()) { CompletableFuture future = Suggestions.empty(); try { future = node.listSuggestions(context.build(truncatedInput), new SuggestionsBuilder(truncatedInput, truncatedInputLowerCase, start)); } catch (final CommandSyntaxException ignored) { } futures[i++] = future; } final CompletableFuture result = new CompletableFuture<>(); CompletableFuture.allOf(futures).thenRun(() -> { final List suggestions = new ArrayList<>(); for (final CompletableFuture future : futures) { suggestions.add(future.join()); } result.complete(Suggestions.merge(fullInput, suggestions)); }); return result; } /** * Gets the root of this command tree. * *

This is often useful as a target of a {@link com.mojang.brigadier.builder.ArgumentBuilder#redirect(CommandNode)}, * {@link #getAllUsage(CommandNode, Object, boolean)} or {@link #getSmartUsage(CommandNode, Object)}. * You may also use it to clone the command tree via {@link #CommandDispatcher(RootCommandNode)}.

* * @return root of the command tree */ public RootCommandNode getRoot() { return root; } /** * Finds a valid path to a given node on the command tree. * *

There may theoretically be multiple paths to a node on the tree, especially with the use of forking or redirecting. * As such, this method makes no guarantees about which path it finds. It will not look at forks or redirects, * and find the first instance of the target node on the tree.

* *

The only guarantee made is that for the same command tree and the same version of this library, the result of * this method will always be a valid input for {@link #findNode(Collection)}, which should return the same node * as provided to this method.

* * @param target the target node you are finding a path for * @return a path to the resulting node, or an empty list if it was not found */ public Collection getPath(final CommandNode target) { final List>> nodes = new ArrayList<>(); addPaths(root, nodes, new ArrayList<>()); for (final List> list : nodes) { if (list.get(list.size() - 1) == target) { final List result = new ArrayList<>(list.size()); for (final CommandNode node : list) { if (node != root) { result.add(node.getName()); } } return result; } } return Collections.emptyList(); } /** * Finds a node by its path * *

Paths may be generated with {@link #getPath(CommandNode)}, and are guaranteed (for the same tree, and the * same version of this library) to always produce the same valid node by this method.

* *

If a node could not be found at the specified path, then {@code null} will be returned.

* * @param path a generated path to a node * @return the node at the given path, or null if not found */ public CommandNode findNode(final Collection path) { CommandNode node = root; for (final String name : path) { node = node.getChild(name); if (node == null) { return null; } } return node; } /** * Scans the command tree for potential ambiguous commands. * *

This is a shortcut for {@link CommandNode#findAmbiguities(AmbiguityConsumer)} on {@link #getRoot()}.

* *

Ambiguities are detected by testing every {@link CommandNode#getExamples()} on one node verses every sibling * node. This is not fool proof, and relies a lot on the providers of the used argument types to give good examples.

* * @param consumer a callback to be notified of potential ambiguities */ public void findAmbiguities(final AmbiguityConsumer consumer) { root.findAmbiguities(consumer); } private void addPaths(final CommandNode node, final List>> result, final List> parents) { final List> current = new ArrayList<>(parents); current.add(node); result.add(current); for (final CommandNode child : node.getChildren()) { addPaths(child, result, current); } } }