Files
brigadier/src/main/java/com/mojang/brigadier/CommandDispatcher.java
T
2018-01-16 17:10:54 +01:00

434 lines
19 KiB
Java

package com.mojang.brigadier;
import com.google.common.collect.Iterables;
import com.google.common.collect.Lists;
import com.google.common.collect.Maps;
import com.google.common.collect.Sets;
import com.mojang.brigadier.builder.LiteralArgumentBuilder;
import com.mojang.brigadier.context.CommandContext;
import com.mojang.brigadier.context.CommandContextBuilder;
import com.mojang.brigadier.context.StringRange;
import com.mojang.brigadier.exceptions.CommandSyntaxException;
import com.mojang.brigadier.exceptions.ParameterizedCommandExceptionType;
import com.mojang.brigadier.exceptions.SimpleCommandExceptionType;
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.List;
import java.util.Map;
import java.util.Set;
import java.util.concurrent.CompletableFuture;
import java.util.function.Predicate;
import java.util.stream.Collectors;
public class CommandDispatcher<S> {
public static final SimpleCommandExceptionType ERROR_UNKNOWN_COMMAND = new SimpleCommandExceptionType("command.unknown.command", "Unknown command");
public static final SimpleCommandExceptionType ERROR_UNKNOWN_ARGUMENT = new SimpleCommandExceptionType("command.unknown.argument", "Incorrect argument for command");
public static final SimpleCommandExceptionType ERROR_EXPECTED_ARGUMENT_SEPARATOR = new SimpleCommandExceptionType("command.expected.separator", "Expected whitespace to end one argument, but found trailing data");
public static final ParameterizedCommandExceptionType ERROR_PARSE_EXCEPTION = new ParameterizedCommandExceptionType("command.exception", "Could not parse command: ${message}", "message");
public static final String 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<S> root;
private final Predicate<CommandNode<S>> hasCommand = new Predicate<CommandNode<S>>() {
@Override
public boolean test(final CommandNode<S> input) {
return input != null && (input.getCommand() != null || input.getChildren().stream().anyMatch(hasCommand));
}
};
private ResultConsumer<S> consumer = (c, s, r) -> {
};
public CommandDispatcher(final RootCommandNode<S> root) {
this.root = root;
}
public CommandDispatcher() {
this(new RootCommandNode<>());
}
public LiteralCommandNode<S> register(final LiteralArgumentBuilder<S> command) {
final LiteralCommandNode<S> build = command.build();
root.addChild(build);
return build;
}
public void setConsumer(final ResultConsumer<S> consumer) {
this.consumer = consumer;
}
public int execute(final String input, final S source) throws CommandSyntaxException {
final ParseResults<S> parse = parse(input, source);
return execute(parse);
}
public int execute(final ParseResults<S> 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 ERROR_UNKNOWN_COMMAND.createWithContext(parse.getReader());
} else {
throw ERROR_UNKNOWN_ARGUMENT.createWithContext(parse.getReader());
}
}
int result = 0;
int successfulForks = 0;
boolean forked = false;
boolean foundCommand = false;
final String command = parse.getReader().getString();
final CommandContext<S> original = parse.getContext().build(command);
List<CommandContext<S>> contexts = Collections.singletonList(original);
ArrayList<CommandContext<S>> next = null;
while (contexts != null) {
final int size = contexts.size();
for (int i = 0; i < size; i++) {
final CommandContext<S> context = contexts.get(i);
final CommandContext<S> child = context.getChild();
if (child != null) {
if (!child.getNodes().isEmpty()) {
foundCommand = true;
final RedirectModifier<S> modifier = context.getRedirectModifier();
if (modifier == null) {
if (next == null) {
next = new ArrayList<>(1);
}
next.add(child.copyFor(context.getSource()));
} else {
final Collection<S> results = modifier.apply(context);
if (!results.isEmpty()) {
if (results.size() > 1) {
forked = true;
}
if (next == null) {
next = new ArrayList<>(results.size());
}
for (final S source : results) {
next.add(child.copyFor(source));
}
}
}
}
} 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 ERROR_UNKNOWN_COMMAND.createWithContext(parse.getReader());
}
return forked ? successfulForks : result;
}
public ParseResults<S> parse(final String command, final S source) {
final StringReader reader = new StringReader(command);
final CommandContextBuilder<S> context = new CommandContextBuilder<>(this, source, 0);
return parseNodes(root, reader, context);
}
private static class PartialParse<S> {
public final CommandContextBuilder<S> context;
public final ParseResults<S> parse;
private PartialParse(final CommandContextBuilder<S> context, final ParseResults<S> parse) {
this.context = context;
this.parse = parse;
}
}
private ParseResults<S> parseNodes(final CommandNode<S> node, final StringReader originalReader, final CommandContextBuilder<S> contextSoFar) {
final S source = contextSoFar.getSource();
Map<CommandNode<S>, CommandSyntaxException> errors = null;
final List<PartialParse<S>> potentials = Lists.newArrayList();
final int cursor = originalReader.getCursor();
for (final CommandNode<S> child : node.getRelevantNodes(originalReader)) {
if (!child.canUse(source)) {
continue;
}
final CommandContextBuilder<S> context = contextSoFar.copy();
final StringReader reader = new StringReader(originalReader);
try {
try {
child.parse(reader, context);
} catch (final RuntimeException ex) {
throw ERROR_PARSE_EXCEPTION.createWithContext(reader, ex.getMessage());
}
if (reader.canRead()) {
if (reader.peek() != ARGUMENT_SEPARATOR_CHAR) {
throw ERROR_EXPECTED_ARGUMENT_SEPARATOR.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<S> childContext = new CommandContextBuilder<>(this, source, reader.getCursor());
childContext.withNode(child.getRedirect(), StringRange.between(cursor, reader.getCursor() - 1));
final ParseResults<S> parse = parseNodes(child.getRedirect(), reader, childContext);
context.withChild(parse.getContext());
return new ParseResults<>(context, parse.getReader(), parse.getExceptions());
} else {
final ParseResults<S> parse = parseNodes(child, reader, context);
potentials.add(new PartialParse<>(context, parse));
}
} else {
potentials.add(new PartialParse<>(context, new ParseResults<>(context, reader, Collections.emptyMap())));
}
}
if (!potentials.isEmpty()) {
final PartialParse<S> likely;
if (potentials.size() > 1) {
final List<PartialParse<S>> sorted = Lists.newArrayList(potentials);
sorted.sort((a, b) -> {
if (!a.parse.getReader().canRead() && b.parse.getReader().canRead()) {
return -1;
}
if (a.parse.getReader().canRead() && !b.parse.getReader().canRead()) {
return 1;
}
if (a.parse.getExceptions().isEmpty() && !b.parse.getExceptions().isEmpty()) {
return -1;
}
if (!a.parse.getExceptions().isEmpty() && b.parse.getExceptions().isEmpty()) {
return 1;
}
return 0;
});
likely = sorted.get(0);
} else {
likely = potentials.get(0);
}
return likely.parse;
}
return new ParseResults<>(contextSoFar, originalReader, errors == null ? Collections.emptyMap() : errors);
}
public String[] getAllUsage(final CommandNode<S> node, final S source, final boolean restricted) {
final ArrayList<String> result = Lists.newArrayList();
getAllUsage(node, source, result, "", restricted);
return result.toArray(new String[result.size()]);
}
private void getAllUsage(final CommandNode<S> node, final S source, final ArrayList<String> 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<S> child : node.getChildren()) {
getAllUsage(child, source, result, prefix.isEmpty() ? child.getUsageText() : prefix + ARGUMENT_SEPARATOR + child.getUsageText(), restricted);
}
}
}
public Map<CommandNode<S>, String> getSmartUsage(final CommandNode<S> node, final S source) {
final Map<CommandNode<S>, String> result = Maps.newLinkedHashMap();
final boolean optional = node.getCommand() != null;
for (final CommandNode<S> 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<S> 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<CommandNode<S>> 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<String> childUsage = Sets.newLinkedHashSet();
for (final CommandNode<S> 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<S> 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;
}
public CompletableFuture<Suggestions> getCompletionSuggestions(final ParseResults<S> parse) {
final CommandContextBuilder<S> rootContext = parse.getContext();
final CommandContextBuilder<S> context = rootContext.getLastChild();
final CommandNode<S> parent;
final int start;
if (context.getNodes().isEmpty()) {
parent = root;
start = 0;
} else if (parse.getReader().canRead()) {
final Map.Entry<CommandNode<S>, StringRange> entry = Iterables.getLast(context.getNodes().entrySet());
parent = entry.getKey();
start = entry.getValue().getEnd() + 1;
} else if (context.getNodes().size() > 1) {
final Map.Entry<CommandNode<S>, StringRange> entry = Iterables.get(context.getNodes().entrySet(), context.getNodes().size() - 2);
parent = entry.getKey();
start = entry.getValue().getEnd() + 1;
} else if (rootContext != context && context.getNodes().size() > 0) {
final Map.Entry<CommandNode<S>, StringRange> entry = Iterables.getLast(context.getNodes().entrySet());
parent = entry.getKey();
start = entry.getValue().getEnd() + 1;
} else {
parent = root;
start = 0;
}
@SuppressWarnings("unchecked") final CompletableFuture<Suggestions>[] futures = new CompletableFuture[parent.getChildren().size()];
int i = 0;
for (final CommandNode<S> node : parent.getChildren()) {
try {
futures[i++] = node.listSuggestions(context.build(parse.getReader().getString()), new SuggestionsBuilder(parse.getReader().getString(), start));
} catch (final CommandSyntaxException e) {
futures[i++] = Suggestions.empty();
}
}
final CompletableFuture<Suggestions> result = new CompletableFuture<>();
CompletableFuture.allOf(futures).thenRun(() -> {
final List<Suggestions> suggestions = Lists.newArrayList();
for (final CompletableFuture<Suggestions> future : futures) {
suggestions.add(future.join());
}
result.complete(Suggestions.merge(parse.getReader().getString(), suggestions));
});
return result;
}
public RootCommandNode<S> getRoot() {
return root;
}
public Collection<String> getPath(final CommandNode<S> target) {
final List<List<CommandNode<S>>> nodes = new ArrayList<>();
addPaths(root, nodes, new ArrayList<>());
for (final List<CommandNode<S>> list : nodes) {
if (list.get(list.size() - 1) == target) {
final List<String> result = new ArrayList<>(list.size());
for (final CommandNode<S> node : list) {
if (node != root) {
result.add(node.getName());
}
}
return result;
}
}
return Collections.emptyList();
}
public CommandNode<S> findNode(final Collection<String> path) {
CommandNode<S> node = root;
for (final String name : path) {
node = node.getChild(name);
if (node == null) {
return null;
}
}
return node;
}
private void addPaths(final CommandNode<S> node, final List<List<CommandNode<S>>> result, final List<CommandNode<S>> parents) {
final List<CommandNode<S>> current = new ArrayList<>(parents);
current.add(node);
result.add(current);
for (final CommandNode<S> child : node.getChildren()) {
addPaths(child, result, current);
}
}
}