diff --git a/proxy/src/main/java/com/velocitypowered/proxy/command/CommandGraphInjector.java b/proxy/src/main/java/com/velocitypowered/proxy/command/CommandGraphInjector.java
new file mode 100644
index 000000000..0d692891a
--- /dev/null
+++ b/proxy/src/main/java/com/velocitypowered/proxy/command/CommandGraphInjector.java
@@ -0,0 +1,142 @@
+/*
+ * Copyright (C) 2018 Velocity Contributors
+ *
+ * This program 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.
+ *
+ * This program 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 this program. If not, see .
+ */
+
+package com.velocitypowered.proxy.command;
+
+import com.google.common.base.Preconditions;
+import com.mojang.brigadier.CommandDispatcher;
+import com.mojang.brigadier.StringReader;
+import com.mojang.brigadier.builder.ArgumentBuilder;
+import com.mojang.brigadier.context.CommandContextBuilder;
+import com.mojang.brigadier.context.StringRange;
+import com.mojang.brigadier.tree.CommandNode;
+import com.mojang.brigadier.tree.LiteralCommandNode;
+import com.mojang.brigadier.tree.RootCommandNode;
+import com.velocitypowered.proxy.command.brigadier.VelocityArgumentCommandNode;
+import java.util.concurrent.locks.Lock;
+import org.checkerframework.checker.lock.qual.GuardedBy;
+import org.checkerframework.checker.nullness.qual.Nullable;
+
+/**
+ * Copies the nodes of a {@link RootCommandNode} to a possibly non-empty
+ * destination {@link RootCommandNode}, respecting the requirements satisfied
+ * by a given command source.
+ *
+ * @param the type of the source to inject the nodes for
+ */
+public final class CommandGraphInjector {
+
+ private static final StringRange ALIAS_RANGE = StringRange.at(0);
+ private static final StringReader ALIAS_READER = new StringReader("");
+
+ private final @GuardedBy("lock") CommandDispatcher dispatcher;
+ private final Lock lock;
+
+ CommandGraphInjector(final CommandDispatcher dispatcher, final Lock lock) {
+ this.dispatcher = Preconditions.checkNotNull(dispatcher, "dispatcher");
+ this.lock = Preconditions.checkNotNull(lock, "lock");
+ }
+
+ // The term "source" is ambiguous here. We use "origin" when referring to
+ // the root node we are copying nodes from to the destination node.
+
+ /**
+ * Adds the node from the root node of this injector to the given root node,
+ * respecting the requirements satisfied by the given source.
+ *
+ *
Prior to adding a literal with the same name as one previously contained
+ * in the destination node, the old node is removed from the destination node.
+ *
+ * @param dest the root node to add the permissible nodes to
+ * @param source the command source to inject the nodes for
+ */
+ public void inject(final RootCommandNode dest, final S source) {
+ lock.lock();
+ try {
+ final RootCommandNode origin = this.dispatcher.getRoot();
+ final CommandContextBuilder rootContext =
+ new CommandContextBuilder<>(this.dispatcher, source, origin, 0);
+
+ // Filter alias nodes
+ for (final CommandNode node : origin.getChildren()) {
+ if (!node.canUse(source)) {
+ continue;
+ }
+
+ final CommandContextBuilder context = rootContext.copy()
+ .withNode(node, ALIAS_RANGE);
+ if (!node.canUse(context, ALIAS_READER)) {
+ continue;
+ }
+
+ final LiteralCommandNode asLiteral = (LiteralCommandNode) node;
+ final LiteralCommandNode copy = asLiteral.createBuilder().build();
+ final VelocityArgumentCommandNode argsNode =
+ VelocityCommands.getArgumentsNode(asLiteral);
+ if (argsNode == null) {
+ // This literal is associated to a BrigadierCommand, filter normally.
+ this.copyChildren(node, copy, source);
+ } else {
+ // Copy all children nodes (arguments node and hints)
+ for (final CommandNode child : node.getChildren()) {
+ copy.addChild(child);
+ }
+ }
+ this.addAlias(copy, dest);
+ }
+ } finally {
+ lock.unlock();
+ }
+ }
+
+ private @Nullable CommandNode filterNode(final CommandNode node, final S source) {
+ // We only check the non-context requirement when filtering alias nodes.
+ // Otherwise, we would need to manually craft context builder and reader instances,
+ // which is both incorrect and inefficient. The reason why we can do so for alias
+ // literals is due to the empty string being a valid and expected input by
+ // the context-aware requirement (when suggesting the literal name).
+ if (!node.canUse(source)) {
+ return null;
+ }
+ final ArgumentBuilder builder = node.createBuilder();
+ if (node.getRedirect() != null) {
+ // Redirects to non-Brigadier commands are not supported. Luckily,
+ // we don't expose the root node to API users, so they can't access
+ // nodes associated to other commands.
+ final CommandNode target = this.filterNode(node.getRedirect(), source);
+ builder.forward(target, builder.getRedirectModifier(), builder.isFork());
+ }
+ final CommandNode result = builder.build();
+ this.copyChildren(node, result, source);
+ return result;
+ }
+
+ private void copyChildren(final CommandNode parent, final CommandNode dest,
+ final S source) {
+ for (final CommandNode child : parent.getChildren()) {
+ final CommandNode filtered = this.filterNode(child, source);
+ if (filtered != null) {
+ dest.addChild(filtered);
+ }
+ }
+ }
+
+ private void addAlias(final LiteralCommandNode node, final RootCommandNode dest) {
+ dest.removeChildByName(node.getName());
+ dest.addChild(node);
+ }
+}
diff --git a/proxy/src/main/java/com/velocitypowered/proxy/command/CommandInvocationFactory.java b/proxy/src/main/java/com/velocitypowered/proxy/command/CommandInvocationFactory.java
deleted file mode 100644
index 05a4d3b90..000000000
--- a/proxy/src/main/java/com/velocitypowered/proxy/command/CommandInvocationFactory.java
+++ /dev/null
@@ -1,40 +0,0 @@
-/*
- * Copyright (C) 2018 Velocity Contributors
- *
- * This program 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.
- *
- * This program 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 this program. If not, see .
- */
-
-package com.velocitypowered.proxy.command;
-
-import com.mojang.brigadier.context.CommandContext;
-import com.velocitypowered.api.command.CommandInvocation;
-import com.velocitypowered.api.command.CommandSource;
-
-/**
- * Creates command invocation contexts for the given {@link CommandSource}
- * and command line arguments.
- *
- * @param the type of the built invocation
- */
-@FunctionalInterface
-public interface CommandInvocationFactory> {
-
- /**
- * Returns an invocation context for the given Brigadier context.
- *
- * @param context the command context
- * @return the built invocation context
- */
- I create(final CommandContext context);
-}
diff --git a/proxy/src/main/java/com/velocitypowered/proxy/command/CommandNodeFactory.java b/proxy/src/main/java/com/velocitypowered/proxy/command/CommandNodeFactory.java
deleted file mode 100644
index 5f68066f8..000000000
--- a/proxy/src/main/java/com/velocitypowered/proxy/command/CommandNodeFactory.java
+++ /dev/null
@@ -1,94 +0,0 @@
-/*
- * Copyright (C) 2018 Velocity Contributors
- *
- * This program 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.
- *
- * This program 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 this program. If not, see .
- */
-
-package com.velocitypowered.proxy.command;
-
-import com.mojang.brigadier.context.CommandContext;
-import com.mojang.brigadier.tree.LiteralCommandNode;
-import com.velocitypowered.api.command.BrigadierCommand;
-import com.velocitypowered.api.command.Command;
-import com.velocitypowered.api.command.CommandInvocation;
-import com.velocitypowered.api.command.CommandSource;
-import com.velocitypowered.api.command.InvocableCommand;
-import com.velocitypowered.api.command.RawCommand;
-import com.velocitypowered.api.command.SimpleCommand;
-import com.velocitypowered.proxy.util.BrigadierUtils;
-
-@FunctionalInterface
-public interface CommandNodeFactory {
-
- InvocableCommandNodeFactory SIMPLE =
- new InvocableCommandNodeFactory() {
- @Override
- protected SimpleCommand.Invocation createInvocation(
- final CommandContext context) {
- return VelocitySimpleCommandInvocation.FACTORY.create(context);
- }
- };
-
- InvocableCommandNodeFactory RAW =
- new InvocableCommandNodeFactory() {
- @Override
- protected RawCommand.Invocation createInvocation(
- final CommandContext context) {
- return VelocityRawCommandInvocation.FACTORY.create(context);
- }
- };
-
- /**
- * Returns a Brigadier node for the execution of the given command.
- *
- * @param alias the command alias
- * @param command the command to execute
- * @return the command node
- */
- LiteralCommandNode create(String alias, T command);
-
- abstract class InvocableCommandNodeFactory>
- implements CommandNodeFactory> {
-
- @Override
- public LiteralCommandNode create(
- final String alias, final InvocableCommand command) {
- return BrigadierUtils.buildRawArgumentsLiteral(alias,
- context -> {
- I invocation = createInvocation(context);
- if (!command.hasPermission(invocation)) {
- return BrigadierCommand.FORWARD;
- }
- command.execute(invocation);
- return 1;
- },
- (context, builder) -> {
- I invocation = createInvocation(context);
-
- if (!command.hasPermission(invocation)) {
- return builder.buildFuture();
- }
- return command.suggestAsync(invocation).thenApply(values -> {
- for (String value : values) {
- builder.suggest(value);
- }
-
- return builder.build();
- });
- });
- }
-
- protected abstract I createInvocation(final CommandContext context);
- }
-}
diff --git a/proxy/src/main/java/com/velocitypowered/proxy/command/SuggestionsProvider.java b/proxy/src/main/java/com/velocitypowered/proxy/command/SuggestionsProvider.java
new file mode 100644
index 000000000..24ef2430a
--- /dev/null
+++ b/proxy/src/main/java/com/velocitypowered/proxy/command/SuggestionsProvider.java
@@ -0,0 +1,371 @@
+/*
+ * Copyright (C) 2018 Velocity Contributors
+ *
+ * This program 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.
+ *
+ * This program 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 this program. If not, see .
+ */
+
+package com.velocitypowered.proxy.command;
+
+import com.google.common.base.Preconditions;
+import com.mojang.brigadier.CommandDispatcher;
+import com.mojang.brigadier.ParseResults;
+import com.mojang.brigadier.StringReader;
+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.suggestion.Suggestions;
+import com.mojang.brigadier.suggestion.SuggestionsBuilder;
+import com.mojang.brigadier.tree.CommandNode;
+import com.mojang.brigadier.tree.LiteralCommandNode;
+import com.spotify.futures.CompletableFutures;
+import com.velocitypowered.api.command.Command;
+import com.velocitypowered.api.command.CommandMeta;
+import com.velocitypowered.api.command.CommandSource;
+import com.velocitypowered.proxy.command.brigadier.VelocityArgumentCommandNode;
+import java.util.ArrayList;
+import java.util.Collection;
+import java.util.Collections;
+import java.util.List;
+import java.util.Locale;
+import java.util.concurrent.CompletableFuture;
+import java.util.concurrent.locks.Lock;
+import org.apache.logging.log4j.LogManager;
+import org.apache.logging.log4j.Logger;
+import org.checkerframework.checker.lock.qual.GuardedBy;
+
+/**
+ * Provides suggestions for a given command input.
+ *
+ * Similar to {@link CommandDispatcher#getCompletionSuggestions(ParseResults)}, except it
+ * avoids fully parsing the given input and performs exactly one requirement predicate check
+ * per considered node.
+ *
+ * @param the type of the command source
+ */
+final class SuggestionsProvider {
+
+ private static final Logger LOGGER = LogManager.getLogger(SuggestionsProvider.class);
+
+ private static final StringRange ALIAS_SUGGESTION_RANGE = StringRange.at(0);
+
+ private final @GuardedBy("lock") CommandDispatcher dispatcher;
+ private final Lock lock;
+
+ SuggestionsProvider(final CommandDispatcher dispatcher, final Lock lock) {
+ this.dispatcher = Preconditions.checkNotNull(dispatcher, "dispatcher");
+ this.lock = Preconditions.checkNotNull(lock, "lock");
+ }
+
+ /**
+ * Provides suggestions for the given input and source.
+ *
+ * @param input the partial input
+ * @param source the command source invoking the command
+ * @return a future that completes with the suggestions
+ */
+ public CompletableFuture provideSuggestions(final String input, final S source) {
+ final CommandContextBuilder context = new CommandContextBuilder<>(
+ this.dispatcher, source, this.dispatcher.getRoot(), 0);
+ return this.provideSuggestions(new StringReader(input), context);
+ }
+
+ /**
+ * Provides suggestions for the given input and context.
+ *
+ * @param reader the input reader
+ * @param context an empty context
+ * @return a future that completes with the suggestions
+ */
+ private CompletableFuture provideSuggestions(
+ final StringReader reader, final CommandContextBuilder context) {
+ lock.lock();
+ try {
+ final StringRange aliasRange = this.consumeAlias(reader);
+ final String alias = aliasRange.get(reader).toLowerCase(Locale.ENGLISH);
+ final LiteralCommandNode literal =
+ (LiteralCommandNode) context.getRootNode().getChild(alias);
+
+ final boolean hasArguments = reader.canRead();
+ if (hasArguments) {
+ if (literal == null) {
+ // Input has arguments for non-registered alias
+ return Suggestions.empty();
+ }
+ context.withNode(literal, aliasRange);
+ reader.skip(); // separator
+ return this.provideArgumentsSuggestions(literal, reader, context);
+ } else {
+ return this.provideAliasSuggestions(reader, context);
+ }
+ } finally {
+ lock.unlock();
+ }
+ }
+
+ private StringRange consumeAlias(final StringReader reader) {
+ final int firstSep = reader.getString().indexOf(
+ CommandDispatcher.ARGUMENT_SEPARATOR_CHAR, reader.getCursor());
+ final StringRange range = StringRange.between(
+ reader.getCursor(), firstSep == -1 ? reader.getTotalLength() : firstSep);
+ reader.setCursor(range.getEnd());
+ return range;
+ }
+
+ /**
+ * Returns whether a literal node with the given lowercase name should be considered for
+ * suggestions given the specified input.
+ *
+ * @param name the lowercase literal name
+ * @param input the partial input
+ * @return true if the literal should be considered; false otherwise
+ */
+ private static boolean shouldConsider(final String name, final String input) {
+ return name.regionMatches(false, 0, input, 0, input.length());
+ }
+
+ /**
+ * Returns alias suggestions for the given input.
+ *
+ * @param reader the input reader
+ * @param contextSoFar an empty context
+ * @return a future that completes with the suggestions
+ */
+ private CompletableFuture provideAliasSuggestions(
+ final StringReader reader, final CommandContextBuilder contextSoFar) {
+ final S source = contextSoFar.getSource();
+ // Lowercase the alias here so all comparisons can be case-sensitive (cheaper)
+ // TODO Is this actually faster? It may incur an allocation
+ final String input = reader.getRead().toLowerCase(Locale.ENGLISH);
+
+ final Collection> aliases = contextSoFar.getRootNode().getChildren();
+ @SuppressWarnings("unchecked")
+ final CompletableFuture[] futures = new CompletableFuture[aliases.size()];
+ int i = 0;
+ for (final CommandNode node : aliases) {
+ CompletableFuture future = Suggestions.empty();
+ final String alias = node.getName();
+
+ if (shouldConsider(alias, input) && node.canUse(source)) {
+ final CommandContextBuilder context = contextSoFar.copy()
+ .withNode(node, ALIAS_SUGGESTION_RANGE);
+ if (node.canUse(context, reader)) {
+ // LiteralCommandNode#listSuggestions is case insensitive
+ final SuggestionsBuilder builder = new SuggestionsBuilder(input, 0);
+ future = builder.suggest(alias).buildFuture();
+ }
+ }
+ futures[i++] = future;
+ }
+ return this.merge(input, futures);
+ }
+
+ /**
+ * Merges the suggestions provided by the {@link Command} associated to the given
+ * alias node and the hints given during registration for the given input.
+ *
+ * The context is not mutated by this method. The reader's cursor may be modified.
+ *
+ * @param alias the alias node
+ * @param reader the input reader
+ * @param contextSoFar the context, containing {@code alias}
+ * @return a future that completes with the suggestions
+ */
+ private CompletableFuture provideArgumentsSuggestions(
+ final LiteralCommandNode alias, final StringReader reader,
+ final CommandContextBuilder contextSoFar) {
+ final S source = contextSoFar.getSource();
+ final String fullInput = reader.getString();
+ final VelocityArgumentCommandNode argsNode = VelocityCommands.getArgumentsNode(alias);
+ if (argsNode == null) {
+ // This is a BrigadierCommand, fallback to regular suggestions
+ reader.setCursor(0);
+ final ParseResults parse = this.dispatcher.parse(reader, source);
+ try {
+ return this.dispatcher.getCompletionSuggestions(parse);
+ } catch (final Throwable e) {
+ // Ugly, ugly swallowing of everything Throwable, because plugins are naughty.
+ LOGGER.error("Command node cannot provide suggestions for " + fullInput, e);
+ return Suggestions.empty();
+ }
+ }
+
+ if (!argsNode.canUse(source)) {
+ return Suggestions.empty();
+ }
+
+ final int start = reader.getCursor();
+ final CommandContextBuilder context = contextSoFar.copy();
+ try {
+ argsNode.parse(reader, context); // reads remaining input
+ } catch (final CommandSyntaxException e) {
+ throw new RuntimeException(e);
+ }
+
+ if (!argsNode.canUse(context, reader)) {
+ return Suggestions.empty();
+ }
+
+ // Ask the command for suggestions via the arguments node
+ reader.setCursor(start);
+ final CompletableFuture cmdSuggestions =
+ this.getArgumentsNodeSuggestions(argsNode, reader, context);
+ final boolean hasHints = alias.getChildren().size() > 1;
+ if (!hasHints) {
+ return this.merge(fullInput, cmdSuggestions);
+ }
+
+ // Parse the hint nodes to get remaining suggestions
+ reader.setCursor(start);
+ final CompletableFuture hintSuggestions =
+ this.getHintSuggestions(alias, reader, contextSoFar);
+ return this.merge(fullInput, cmdSuggestions, hintSuggestions);
+ }
+
+ /**
+ * Returns the suggestions provided by the {@link Command} associated to
+ * the specified arguments node for the given input.
+ *
+ * The reader and context are not mutated by this method.
+ *
+ * @param node the arguments node of the command
+ * @param reader the input reader
+ * @param context the context, containing an alias node and {@code node}
+ * @return a future that completes with the suggestions
+ */
+ private CompletableFuture getArgumentsNodeSuggestions(
+ final VelocityArgumentCommandNode node, final StringReader reader,
+ final CommandContextBuilder context) {
+ final int start = reader.getCursor();
+ final String fullInput = reader.getString();
+ final CommandContext built = context.build(fullInput);
+ try {
+ return node.listSuggestions(built, new SuggestionsBuilder(fullInput, start));
+ } catch (final Throwable e) {
+ // Again, plugins are naughty
+ LOGGER.error("Arguments node cannot provide suggestions", e);
+ return Suggestions.empty();
+ }
+ }
+
+ /**
+ * Returns the suggestions provided by the matched hint nodes for the given input.
+ *
+ * The reader and context are not mutated by this method.
+ *
+ * @param alias the alias node
+ * @param reader the input reader
+ * @param context the context, containing {@code alias}
+ * @return a future that completes with the suggestions
+ */
+ private CompletableFuture getHintSuggestions(
+ final LiteralCommandNode alias, final StringReader reader,
+ final CommandContextBuilder context) {
+ final ParseResults parse = this.parseHints(alias, reader, context);
+ try {
+ return this.dispatcher.getCompletionSuggestions(parse);
+ } catch (final Throwable e) {
+ // Yet again, plugins are naughty.
+ LOGGER.error("Hint node cannot provide suggestions", e);
+ return Suggestions.empty();
+ }
+ }
+
+ /**
+ * Parses the hint nodes under the given node, which is either an alias node of
+ * a {@link Command} or another hint node.
+ *
+ * The reader and context are not mutated by this method.
+ *
+ * @param node the node to parse
+ * @param originalReader the input reader
+ * @param contextSoFar the context, containing the alias node of the command
+ * @return the parse results containing the parsed hint nodes
+ * @see VelocityCommandMeta#copyHints(CommandMeta) for the conditions under which the returned
+ * hints can be suggested to a {@link CommandSource}.
+ */
+ private ParseResults parseHints(final CommandNode node, final StringReader originalReader,
+ final CommandContextBuilder contextSoFar) {
+ // This is a stripped-down version of CommandDispatcher#parseNodes that doesn't
+ // check the requirements are satisfied and ignores redirects, neither of which
+ // are used by hint nodes. Parsing errors are ignored.
+ List> potentials = null;
+ for (final CommandNode child : node.getRelevantNodes(originalReader)) {
+ if (VelocityCommands.isArgumentsNode(child)) {
+ continue;
+ }
+ final CommandContextBuilder context = contextSoFar.copy();
+ final StringReader reader = new StringReader(originalReader);
+ try {
+ // We intentionally don't catch all unchecked exceptions
+ child.parse(reader, context);
+ if (reader.canRead() && reader.peek() != CommandDispatcher.ARGUMENT_SEPARATOR_CHAR) {
+ continue;
+ }
+ } catch (final CommandSyntaxException e) {
+ continue;
+ }
+ if (reader.canRead(2)) { // separator + string
+ reader.skip(); // separator
+ final ParseResults parse = this.parseHints(child, reader, context);
+ if (potentials == null) {
+ potentials = new ArrayList<>(1);
+ }
+ potentials.add(parse);
+ }
+ }
+ 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;
+ }
+ return 0;
+ });
+ }
+ return potentials.get(0);
+ }
+ return new ParseResults<>(contextSoFar, originalReader, Collections.emptyMap());
+ }
+
+ /**
+ * Returns a future that is completed with the result of merging the {@link Suggestions}
+ * the given futures complete with. The results of the futures that complete exceptionally
+ * are ignored.
+ *
+ * @param fullInput the command input
+ * @param futures the futures that complete with the suggestions
+ * @return the future that completes with the merged suggestions
+ */
+ @SafeVarargs
+ private CompletableFuture merge(
+ final String fullInput, final CompletableFuture... futures) {
+ // https://github.com/Mojang/brigadier/pull/81
+ return CompletableFuture.allOf(futures).handle((unused, throwable) -> {
+ final List suggestions = new ArrayList<>(futures.length);
+ for (final CompletableFuture future : futures) {
+ if (future.isCompletedExceptionally()) {
+ final Throwable exception = CompletableFutures.getException(future);
+ LOGGER.error("Node cannot provide suggestions", exception);
+ } else {
+ suggestions.add(future.join());
+ }
+ }
+ return Suggestions.merge(fullInput, suggestions);
+ });
+ }
+}
diff --git a/proxy/src/main/java/com/velocitypowered/proxy/command/VelocityCommandManager.java b/proxy/src/main/java/com/velocitypowered/proxy/command/VelocityCommandManager.java
index 48295a2bb..b2951590b 100644
--- a/proxy/src/main/java/com/velocitypowered/proxy/command/VelocityCommandManager.java
+++ b/proxy/src/main/java/com/velocitypowered/proxy/command/VelocityCommandManager.java
@@ -18,40 +18,63 @@
package com.velocitypowered.proxy.command;
import com.google.common.base.Preconditions;
+import com.google.common.collect.ImmutableList;
import com.google.common.collect.Lists;
import com.mojang.brigadier.CommandDispatcher;
import com.mojang.brigadier.ParseResults;
import com.mojang.brigadier.exceptions.CommandSyntaxException;
import com.mojang.brigadier.suggestion.Suggestion;
-import com.mojang.brigadier.tree.CommandNode;
-import com.mojang.brigadier.tree.LiteralCommandNode;
+import com.mojang.brigadier.tree.RootCommandNode;
+import com.spotify.futures.CompletableFutures;
import com.velocitypowered.api.command.BrigadierCommand;
import com.velocitypowered.api.command.Command;
import com.velocitypowered.api.command.CommandManager;
import com.velocitypowered.api.command.CommandMeta;
import com.velocitypowered.api.command.CommandSource;
-import com.velocitypowered.api.command.RawCommand;
-import com.velocitypowered.api.command.SimpleCommand;
import com.velocitypowered.api.event.command.CommandExecuteEvent;
import com.velocitypowered.api.event.command.CommandExecuteEvent.CommandResult;
+import com.velocitypowered.proxy.command.registrar.BrigadierCommandRegistrar;
+import com.velocitypowered.proxy.command.registrar.CommandRegistrar;
+import com.velocitypowered.proxy.command.registrar.RawCommandRegistrar;
+import com.velocitypowered.proxy.command.registrar.SimpleCommandRegistrar;
import com.velocitypowered.proxy.event.VelocityEventManager;
-import com.velocitypowered.proxy.util.BrigadierUtils;
-import java.util.Iterator;
import java.util.List;
import java.util.Locale;
import java.util.concurrent.CompletableFuture;
+import java.util.concurrent.locks.ReadWriteLock;
+import java.util.concurrent.locks.ReentrantReadWriteLock;
import net.kyori.adventure.identity.Identity;
import net.kyori.adventure.text.Component;
import net.kyori.adventure.text.format.NamedTextColor;
+import org.checkerframework.checker.lock.qual.GuardedBy;
+import org.jetbrains.annotations.VisibleForTesting;
public class VelocityCommandManager implements CommandManager {
- private final CommandDispatcher dispatcher;
- private final VelocityEventManager eventManager;
+ private final @GuardedBy("lock") CommandDispatcher dispatcher;
+ private final ReadWriteLock lock;
+ private final VelocityEventManager eventManager;
+ private final List> registrars;
+ private final SuggestionsProvider suggestionsProvider;
+ private final CommandGraphInjector injector;
+
+ /**
+ * Constructs a command manager.
+ *
+ * @param eventManager the event manager
+ */
public VelocityCommandManager(final VelocityEventManager eventManager) {
- this.eventManager = Preconditions.checkNotNull(eventManager);
+ this.lock = new ReentrantReadWriteLock();
this.dispatcher = new CommandDispatcher<>();
+ this.eventManager = Preconditions.checkNotNull(eventManager);
+ final RootCommandNode root = this.dispatcher.getRoot();
+ this.registrars = ImmutableList.of(
+ new BrigadierCommandRegistrar(root, this.lock.writeLock()),
+ new SimpleCommandRegistrar(root, this.lock.writeLock()),
+ new RawCommandRegistrar(root, this.lock.writeLock()));
+ this.suggestionsProvider = new SuggestionsProvider<>(this.dispatcher, this.lock.readLock());
+ this.injector = new CommandGraphInjector<>(this.dispatcher, this.lock.readLock());
}
@Override
@@ -77,42 +100,44 @@ public class VelocityCommandManager implements CommandManager {
Preconditions.checkNotNull(meta, "meta");
Preconditions.checkNotNull(command, "command");
- Iterator aliasIterator = meta.getAliases().iterator();
- String primaryAlias = aliasIterator.next();
-
- LiteralCommandNode node = null;
- if (command instanceof BrigadierCommand) {
- node = ((BrigadierCommand) command).getNode();
- } else if (command instanceof SimpleCommand) {
- node = CommandNodeFactory.SIMPLE.create(primaryAlias, (SimpleCommand) command);
- } else if (command instanceof RawCommand) {
- node = CommandNodeFactory.RAW.create(primaryAlias, (RawCommand) command);
- } else {
- throw new IllegalArgumentException("Unknown command implementation for "
- + command.getClass().getName());
- }
-
- if (!(command instanceof BrigadierCommand)) {
- for (CommandNode hint : meta.getHints()) {
- node.addChild(BrigadierUtils.wrapForHinting(hint, node.getCommand()));
+ // TODO Warn if command implements multiple registrable interfaces?
+ for (final CommandRegistrar> registrar : this.registrars) {
+ if (this.tryRegister(registrar, command, meta)) {
+ return; // success
}
}
+ throw new IllegalArgumentException(
+ command + " does not implement a registrable Command subinterface");
+ }
- dispatcher.getRoot().addChild(node);
- while (aliasIterator.hasNext()) {
- String currentAlias = aliasIterator.next();
- CommandNode existingNode = dispatcher.getRoot()
- .getChild(currentAlias.toLowerCase(Locale.ENGLISH));
- if (existingNode != null) {
- dispatcher.getRoot().getChildren().remove(existingNode);
- }
- dispatcher.getRoot().addChild(BrigadierUtils.buildRedirect(currentAlias, node));
+ /**
+ * Attempts to register the given command if it implements the
+ * {@linkplain CommandRegistrar#registrableSuperInterface() registrable superinterface}
+ * of the given registrar.
+ *
+ * @param registrar the registrar to register the command
+ * @param command the command to register
+ * @param meta the command metadata
+ * @param the type of the command
+ * @return true if the command implements the registrable superinterface of the registrar;
+ * false otherwise.
+ * @throws IllegalArgumentException if the registrar cannot register the command
+ */
+ private boolean tryRegister(final CommandRegistrar registrar,
+ final Command command, final CommandMeta meta) {
+ final Class superInterface = registrar.registrableSuperInterface();
+ if (!superInterface.isInstance(command)) {
+ return false;
}
+ registrar.register(meta, superInterface.cast(command));
+ return true;
}
@Override
public void unregister(final String alias) {
Preconditions.checkNotNull(alias, "alias");
+ // The literals of secondary aliases will preserve the children of
+ // the removed literal in the graph.
dispatcher.getRoot().removeChildByName(alias.toLowerCase(Locale.ENGLISH));
}
@@ -134,9 +159,11 @@ public class VelocityCommandManager implements CommandManager {
Preconditions.checkNotNull(source, "source");
Preconditions.checkNotNull(cmdLine, "cmdLine");
- ParseResults results = parse(cmdLine, source, true);
+ final String normalizedInput = VelocityCommands.normalizeInput(cmdLine, true);
try {
- return dispatcher.execute(results) != BrigadierCommand.FORWARD;
+ // The parse can fail if the requirement predicates throw
+ final ParseResults parse = this.parse(normalizedInput, source);
+ return dispatcher.execute(parse) != BrigadierCommand.FORWARD;
} catch (final CommandSyntaxException e) {
boolean isSyntaxError = !e.getType().equals(
CommandSyntaxException.BUILT_IN_EXCEPTIONS.dispatcherUnknownCommand());
@@ -190,22 +217,39 @@ public class VelocityCommandManager implements CommandManager {
Preconditions.checkNotNull(source, "source");
Preconditions.checkNotNull(cmdLine, "cmdLine");
- ParseResults parse = parse(cmdLine, source, false);
- return dispatcher.getCompletionSuggestions(parse)
- .thenApply(suggestions -> Lists.transform(suggestions.getList(), Suggestion::getText));
+ final String normalizedInput = VelocityCommands.normalizeInput(cmdLine, false);
+ try {
+ return suggestionsProvider.provideSuggestions(normalizedInput, source)
+ .thenApply(suggestions ->
+ Lists.transform(suggestions.getList(), Suggestion::getText));
+ } catch (final Throwable e) {
+ // Again, plugins are naughty
+ return CompletableFuture.failedFuture(
+ new RuntimeException("Unable to provide suggestions for " + cmdLine + " for " + source, e));
+ }
}
- private ParseResults parse(final String cmdLine, final CommandSource source,
- final boolean trim) {
- String normalized = BrigadierUtils.normalizeInput(cmdLine, trim);
- return dispatcher.parse(normalized, source);
+ /**
+ * Parses the given command input.
+ *
+ * @param input the normalized command input, without the leading slash ('/')
+ * @param source the command source to parse the command for
+ * @return the parse results
+ */
+ private ParseResults parse(final String input, final CommandSource source) {
+ lock.readLock().lock();
+ try {
+ return dispatcher.parse(input, source);
+ } finally {
+ lock.readLock().unlock();
+ }
}
/**
* Returns whether the given alias is registered on this manager.
*
* @param alias the command alias to check
- * @return {@code true} if the alias is registered
+ * @return true if the alias is registered; false otherwise
*/
@Override
public boolean hasCommand(final String alias) {
@@ -213,7 +257,12 @@ public class VelocityCommandManager implements CommandManager {
return dispatcher.getRoot().getChild(alias.toLowerCase(Locale.ENGLISH)) != null;
}
- public CommandDispatcher getDispatcher() {
- return dispatcher;
+ @VisibleForTesting // this constitutes unsafe publication
+ RootCommandNode getRoot() {
+ return dispatcher.getRoot();
+ }
+
+ public CommandGraphInjector getInjector() {
+ return injector;
}
}
\ No newline at end of file
diff --git a/proxy/src/main/java/com/velocitypowered/proxy/command/VelocityCommandMeta.java b/proxy/src/main/java/com/velocitypowered/proxy/command/VelocityCommandMeta.java
index c45b51080..b3449e4f3 100644
--- a/proxy/src/main/java/com/velocitypowered/proxy/command/VelocityCommandMeta.java
+++ b/proxy/src/main/java/com/velocitypowered/proxy/command/VelocityCommandMeta.java
@@ -20,15 +20,18 @@ package com.velocitypowered.proxy.command;
import com.google.common.base.Preconditions;
import com.google.common.collect.ImmutableList;
import com.google.common.collect.ImmutableSet;
+import com.mojang.brigadier.builder.ArgumentBuilder;
import com.mojang.brigadier.tree.CommandNode;
+import com.velocitypowered.api.command.Command;
import com.velocitypowered.api.command.CommandMeta;
import com.velocitypowered.api.command.CommandSource;
import java.util.Collection;
import java.util.List;
import java.util.Locale;
import java.util.Set;
+import java.util.stream.Stream;
-final class VelocityCommandMeta implements CommandMeta {
+public final class VelocityCommandMeta implements CommandMeta {
static final class Builder implements CommandMeta.Builder {
@@ -46,9 +49,9 @@ final class VelocityCommandMeta implements CommandMeta {
public CommandMeta.Builder aliases(final String... aliases) {
Preconditions.checkNotNull(aliases, "aliases");
for (int i = 0, length = aliases.length; i < length; i++) {
- final String alias1 = aliases[i];
- Preconditions.checkNotNull(alias1, "alias at index %s", i);
- this.aliases.add(alias1.toLowerCase(Locale.ENGLISH));
+ final String alias = aliases[i];
+ Preconditions.checkNotNull(alias, "alias at index %s", i);
+ this.aliases.add(alias.toLowerCase(Locale.ENGLISH));
}
return this;
}
@@ -56,16 +59,56 @@ final class VelocityCommandMeta implements CommandMeta {
@Override
public CommandMeta.Builder hint(final CommandNode node) {
Preconditions.checkNotNull(node, "node");
- hints.add(node);
+ if (node.getCommand() != null) {
+ throw new IllegalArgumentException("Cannot use executable node for hinting");
+ }
+ if (node.getRedirect() != null) {
+ throw new IllegalArgumentException("Cannot use a node with a redirect for hinting");
+ }
+ this.hints.add(node);
return this;
}
@Override
public CommandMeta build() {
- return new VelocityCommandMeta(aliases.build(), hints.build());
+ return new VelocityCommandMeta(this.aliases.build(), this.hints.build());
}
}
+ /**
+ * Creates a node to use for hinting the arguments of a {@link Command}. Hint nodes are
+ * sent to 1.13+ clients and the proxy uses them for providing suggestions.
+ *
+ * A hint node is used to provide suggestions if and only if the requirements of
+ * the corresponding {@link CommandNode} are satisfied. The requirement predicate
+ * of the returned node always returns {@code false}.
+ *
+ * @param hint the node containing hinting metadata
+ * @return the hinting command node
+ */
+ private static CommandNode copyForHinting(final CommandNode hint) {
+ // We need to perform a deep copy of the hint to prevent the user
+ // from modifying the nodes and adding a Command or a redirect.
+ final ArgumentBuilder builder = hint.createBuilder()
+ // Requirement checking is performed by SuggestionProvider
+ .requires(source -> false);
+ for (final CommandNode child : hint.getChildren()) {
+ builder.then(copyForHinting(child));
+ }
+ return builder.build();
+ }
+
+ /**
+ * Returns a stream of copies of every hint contained in the given metadata object.
+ *
+ * @param meta the command metadata
+ * @return a stream of hinting nodes
+ */
+ // This is a static method because most methods take a CommandMeta.
+ public static Stream> copyHints(final CommandMeta meta) {
+ return meta.getHints().stream().map(VelocityCommandMeta::copyForHinting);
+ }
+
private final Set aliases;
private final List> hints;
@@ -77,11 +120,35 @@ final class VelocityCommandMeta implements CommandMeta {
@Override
public Collection getAliases() {
- return aliases;
+ return this.aliases;
}
@Override
public Collection> getHints() {
- return hints;
+ return this.hints;
+ }
+
+ @Override
+ public boolean equals(final Object o) {
+ if (this == o) {
+ return true;
+ }
+ if (o == null || getClass() != o.getClass()) {
+ return false;
+ }
+
+ final VelocityCommandMeta that = (VelocityCommandMeta) o;
+
+ if (!this.aliases.equals(that.aliases)) {
+ return false;
+ }
+ return this.hints.equals(that.hints);
+ }
+
+ @Override
+ public int hashCode() {
+ int result = this.aliases.hashCode();
+ result = 31 * result + this.hints.hashCode();
+ return result;
}
}
diff --git a/proxy/src/main/java/com/velocitypowered/proxy/command/VelocityCommands.java b/proxy/src/main/java/com/velocitypowered/proxy/command/VelocityCommands.java
new file mode 100644
index 000000000..262812f22
--- /dev/null
+++ b/proxy/src/main/java/com/velocitypowered/proxy/command/VelocityCommands.java
@@ -0,0 +1,191 @@
+/*
+ * Copyright (C) 2018 Velocity Contributors
+ *
+ * This program 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.
+ *
+ * This program 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 this program. If not, see .
+ */
+
+package com.velocitypowered.proxy.command;
+
+import com.google.common.base.Preconditions;
+import com.mojang.brigadier.CommandDispatcher;
+import com.mojang.brigadier.builder.LiteralArgumentBuilder;
+import com.mojang.brigadier.context.CommandContext;
+import com.mojang.brigadier.context.CommandContextBuilder;
+import com.mojang.brigadier.context.ParsedArgument;
+import com.mojang.brigadier.context.ParsedCommandNode;
+import com.mojang.brigadier.tree.CommandNode;
+import com.mojang.brigadier.tree.LiteralCommandNode;
+import com.mojang.brigadier.tree.RootCommandNode;
+import com.velocitypowered.api.command.Command;
+import com.velocitypowered.api.command.CommandManager;
+import com.velocitypowered.api.command.CommandSource;
+import com.velocitypowered.api.command.InvocableCommand;
+import com.velocitypowered.proxy.command.brigadier.VelocityArgumentCommandNode;
+import java.util.List;
+import java.util.Locale;
+import java.util.Map;
+import org.checkerframework.checker.nullness.qual.Nullable;
+
+/**
+ * Provides utility methods common to most {@link Command} implementations.
+ * In particular, {@link InvocableCommand} implementations use the same logic for
+ * creating and parsing the alias and arguments command nodes, which is contained
+ * in this class.
+ */
+public final class VelocityCommands {
+
+ // Normalization
+
+ /**
+ * Normalizes the given command input.
+ *
+ * @param input the raw command input, without the leading slash ('/')
+ * @param trim whether to remove leading and trailing whitespace from the input
+ * @return the normalized command input
+ */
+ static String normalizeInput(final String input, final boolean trim) {
+ final String command = trim ? input.trim() : input;
+ int firstSep = command.indexOf(CommandDispatcher.ARGUMENT_SEPARATOR_CHAR);
+ if (firstSep != -1) {
+ // Aliases are case-insensitive, arguments are not
+ return command.substring(0, firstSep).toLowerCase(Locale.ENGLISH)
+ + command.substring(firstSep);
+ } else {
+ return command.toLowerCase(Locale.ENGLISH);
+ }
+ }
+
+ // Parsing
+
+ /**
+ * Returns the parsed alias, used to execute the command.
+ *
+ * @param nodes the list of parsed nodes, as returned by {@link CommandContext#getNodes()} or
+ * {@link CommandContextBuilder#getNodes()}
+ * @return the command alias
+ */
+ public static String readAlias(final List extends ParsedCommandNode>> nodes) {
+ if (nodes.isEmpty()) {
+ throw new IllegalArgumentException("Cannot read alias from empty node list");
+ }
+ return nodes.get(0).getNode().getName();
+ }
+
+ public static final String ARGS_NODE_NAME = "arguments";
+
+ /**
+ * Returns the parsed arguments that come after the command alias, or {@code fallback} if
+ * no arguments were provided.
+ *
+ * @param arguments the map of parsed arguments, as returned by
+ * {@link CommandContext#getArguments()} or {@link CommandContextBuilder#getArguments()}
+ * @param type the type class of the arguments
+ * @param fallback the value to return if no arguments were provided
+ * @param the type of the arguments
+ * @return the command arguments
+ */
+ public static V readArguments(final Map> arguments,
+ final Class type, final V fallback) {
+ final ParsedArgument, ?> argument = arguments.get(ARGS_NODE_NAME);
+ if (argument == null) {
+ return fallback; // either no arguments were given or this isn't an InvocableCommand
+ }
+ final Object result = argument.getResult();
+ try {
+ return type.cast(result);
+ } catch (final ClassCastException e) {
+ throw new IllegalArgumentException("Parsed argument is of type " + result.getClass()
+ + ", expected " + type, e);
+ }
+ }
+
+ // Alias nodes
+
+ /**
+ * Returns whether a literal node with the given name can be added to
+ * the {@link RootCommandNode} associated to a {@link CommandManager}.
+ *
+ * This is an internal method and should not be used in user-facing
+ * methods. Instead, they should lowercase the given aliases themselves.
+ *
+ * @param alias the alias to check
+ * @return true if the alias can be registered; false otherwise
+ */
+ public static boolean isValidAlias(final String alias) {
+ return alias.equals(alias.toLowerCase(Locale.ENGLISH));
+ }
+
+ /**
+ * Creates a copy of the given literal with the specified name.
+ *
+ * @param original the literal node to copy
+ * @param newName the name of the returned literal node
+ * @return a copy of the literal with the given name
+ */
+ public static LiteralCommandNode shallowCopy(
+ final LiteralCommandNode original, final String newName) {
+ // Brigadier resolves the redirect of a node if further input can be parsed.
+ // Let be a literal node having a redirect to a literal. Then,
+ // the context returned by CommandDispatcher#parseNodes when given the input
+ // string " " does not contain a child context with as its root node.
+ // Thus, the vanilla client asks the children of for suggestions, instead
+ // of those of (https://github.com/Mojang/brigadier/issues/46).
+ // Perform a shallow copy of the literal instead.
+ Preconditions.checkNotNull(original, "original");
+ Preconditions.checkNotNull(newName, "secondaryAlias");
+ final LiteralArgumentBuilder builder = LiteralArgumentBuilder
+ .literal(newName)
+ .requires(original.getRequirement())
+ .requiresWithContext(original.getContextRequirement())
+ .forward(original.getRedirect(), original.getRedirectModifier(), original.isFork())
+ .executes(original.getCommand());
+ for (final CommandNode child : original.getChildren()) {
+ builder.then(child);
+ }
+ return builder.build();
+ }
+
+ // Arguments node
+
+ /**
+ * Returns the arguments node for the command represented by the given alias node,
+ * if present; otherwise returns {@code null}.
+ *
+ * @param alias the alias node
+ * @param the type of the command source
+ * @return the arguments node, or null if not present
+ */
+ static @Nullable VelocityArgumentCommandNode getArgumentsNode(
+ final LiteralCommandNode alias) {
+ final CommandNode node = alias.getChild(ARGS_NODE_NAME);
+ if (node instanceof VelocityArgumentCommandNode) {
+ return (VelocityArgumentCommandNode) node;
+ }
+ return null;
+ }
+
+ /**
+ * Returns whether the given node is an arguments node.
+ *
+ * @param node the node to check
+ * @return true if the node is an arguments node; false otherwise
+ */
+ public static boolean isArgumentsNode(final CommandNode> node) {
+ return node instanceof VelocityArgumentCommandNode && node.getName().equals(ARGS_NODE_NAME);
+ }
+
+ private VelocityCommands() {
+ throw new AssertionError();
+ }
+}
diff --git a/proxy/src/main/java/com/velocitypowered/proxy/command/VelocityRawCommandInvocation.java b/proxy/src/main/java/com/velocitypowered/proxy/command/VelocityRawCommandInvocation.java
deleted file mode 100644
index 3e41c4436..000000000
--- a/proxy/src/main/java/com/velocitypowered/proxy/command/VelocityRawCommandInvocation.java
+++ /dev/null
@@ -1,54 +0,0 @@
-/*
- * Copyright (C) 2018 Velocity Contributors
- *
- * This program 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.
- *
- * This program 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 this program. If not, see .
- */
-
-package com.velocitypowered.proxy.command;
-
-import com.google.common.base.Preconditions;
-import com.mojang.brigadier.context.CommandContext;
-import com.velocitypowered.api.command.CommandSource;
-import com.velocitypowered.api.command.RawCommand;
-import com.velocitypowered.proxy.util.BrigadierUtils;
-
-final class VelocityRawCommandInvocation extends AbstractCommandInvocation
- implements RawCommand.Invocation {
-
- static final Factory FACTORY = new Factory();
-
- static class Factory implements CommandInvocationFactory {
-
- @Override
- public RawCommand.Invocation create(final CommandContext context) {
- return new VelocityRawCommandInvocation(
- context.getSource(),
- BrigadierUtils.getAlias(context),
- BrigadierUtils.getRawArguments(context));
- }
- }
-
- private final String alias;
-
- private VelocityRawCommandInvocation(final CommandSource source,
- final String alias, final String arguments) {
- super(source, arguments);
- this.alias = Preconditions.checkNotNull(alias);
- }
-
- @Override
- public String alias() {
- return alias;
- }
-}
diff --git a/proxy/src/main/java/com/velocitypowered/proxy/command/VelocitySimpleCommandInvocation.java b/proxy/src/main/java/com/velocitypowered/proxy/command/VelocitySimpleCommandInvocation.java
deleted file mode 100644
index 74d851c88..000000000
--- a/proxy/src/main/java/com/velocitypowered/proxy/command/VelocitySimpleCommandInvocation.java
+++ /dev/null
@@ -1,52 +0,0 @@
-/*
- * Copyright (C) 2018 Velocity Contributors
- *
- * This program 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.
- *
- * This program 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 this program. If not, see .
- */
-
-package com.velocitypowered.proxy.command;
-
-import com.mojang.brigadier.context.CommandContext;
-import com.velocitypowered.api.command.CommandSource;
-import com.velocitypowered.api.command.SimpleCommand;
-import com.velocitypowered.proxy.util.BrigadierUtils;
-
-final class VelocitySimpleCommandInvocation extends AbstractCommandInvocation
- implements SimpleCommand.Invocation {
-
- static final Factory FACTORY = new Factory();
-
- static class Factory implements CommandInvocationFactory {
-
- @Override
- public SimpleCommand.Invocation create(final CommandContext context) {
- final String[] arguments = BrigadierUtils.getSplitArguments(context);
- final String alias = BrigadierUtils.getAlias(context);
- return new VelocitySimpleCommandInvocation(context.getSource(), alias, arguments);
- }
- }
-
- private final String alias;
-
- VelocitySimpleCommandInvocation(final CommandSource source, final String alias,
- final String[] arguments) {
- super(source, arguments);
- this.alias = alias;
- }
-
- @Override
- public String alias() {
- return this.alias;
- }
-}
diff --git a/proxy/src/main/java/com/velocitypowered/proxy/command/brigadier/StringArrayArgumentType.java b/proxy/src/main/java/com/velocitypowered/proxy/command/brigadier/StringArrayArgumentType.java
new file mode 100644
index 000000000..eefeec46e
--- /dev/null
+++ b/proxy/src/main/java/com/velocitypowered/proxy/command/brigadier/StringArrayArgumentType.java
@@ -0,0 +1,63 @@
+/*
+ * Copyright (C) 2018 Velocity Contributors
+ *
+ * This program 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.
+ *
+ * This program 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 this program. If not, see .
+ */
+
+package com.velocitypowered.proxy.command.brigadier;
+
+import com.google.common.base.Splitter;
+import com.mojang.brigadier.CommandDispatcher;
+import com.mojang.brigadier.StringReader;
+import com.mojang.brigadier.arguments.ArgumentType;
+import com.mojang.brigadier.exceptions.CommandSyntaxException;
+import java.util.Arrays;
+import java.util.Collection;
+import java.util.List;
+
+/**
+ * An argument type that parses the remaining contents of a {@link StringReader},
+ * splitting the input into words and placing the results in a string array.
+ */
+public final class StringArrayArgumentType implements ArgumentType {
+
+ public static final StringArrayArgumentType INSTANCE = new StringArrayArgumentType();
+ public static final String[] EMPTY = new String[0];
+
+ private static final Splitter WORD_SPLITTER =
+ Splitter.on(CommandDispatcher.ARGUMENT_SEPARATOR_CHAR);
+ private static final List EXAMPLES = Arrays.asList("word", "some words");
+
+ private StringArrayArgumentType() {}
+
+ @Override
+ public String[] parse(final StringReader reader) throws CommandSyntaxException {
+ final String text = reader.getRemaining();
+ reader.setCursor(reader.getTotalLength());
+ if (text.isEmpty()) {
+ return EMPTY;
+ }
+ return WORD_SPLITTER.splitToList(text).toArray(EMPTY);
+ }
+
+ @Override
+ public String toString() {
+ return "stringArray()";
+ }
+
+ @Override
+ public Collection getExamples() {
+ return EXAMPLES;
+ }
+}
diff --git a/proxy/src/main/java/com/velocitypowered/proxy/command/brigadier/VelocityArgumentBuilder.java b/proxy/src/main/java/com/velocitypowered/proxy/command/brigadier/VelocityArgumentBuilder.java
new file mode 100644
index 000000000..62e8c8b93
--- /dev/null
+++ b/proxy/src/main/java/com/velocitypowered/proxy/command/brigadier/VelocityArgumentBuilder.java
@@ -0,0 +1,88 @@
+/*
+ * Copyright (C) 2018 Velocity Contributors
+ *
+ * This program 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.
+ *
+ * This program 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 this program. If not, see .
+ */
+
+package com.velocitypowered.proxy.command.brigadier;
+
+import com.google.common.base.Preconditions;
+import com.mojang.brigadier.arguments.ArgumentType;
+import com.mojang.brigadier.builder.ArgumentBuilder;
+import com.mojang.brigadier.suggestion.SuggestionProvider;
+import com.mojang.brigadier.tree.CommandNode;
+import org.checkerframework.checker.nullness.qual.Nullable;
+
+/**
+ * A builder for creating {@link VelocityArgumentCommandNode}s.
+ *
+ * @param the type of the command source
+ * @param the type of the argument to parse
+ */
+public final class VelocityArgumentBuilder
+ extends ArgumentBuilder> {
+
+ /**
+ * Creates a builder for creating {@link VelocityArgumentCommandNode}s with
+ * the given name and type.
+ *
+ * @param name the name of the node
+ * @param type the type of the argument to parse
+ * @param the type of the command source
+ * @param the type of the argument to parse
+ * @return a builder
+ */
+ public static VelocityArgumentBuilder velocityArgument(final String name,
+ final ArgumentType type) {
+ Preconditions.checkNotNull(name, "name");
+ Preconditions.checkNotNull(type, "type");
+ return new VelocityArgumentBuilder<>(name, type);
+ }
+
+ private final String name;
+ private final ArgumentType type;
+ private SuggestionProvider suggestionsProvider = null;
+
+ private VelocityArgumentBuilder(final String name, final ArgumentType type) {
+ this.name = name;
+ this.type = type;
+ }
+
+ public VelocityArgumentBuilder suggests(final @Nullable SuggestionProvider provider) {
+ this.suggestionsProvider = provider;
+ return this;
+ }
+
+ @Override
+ public VelocityArgumentBuilder then(final ArgumentBuilder argument) {
+ throw new UnsupportedOperationException("Cannot add children to a greedy node");
+ }
+
+ @Override
+ public VelocityArgumentBuilder then(final CommandNode argument) {
+ throw new UnsupportedOperationException("Cannot add children to a greedy node");
+ }
+
+ @Override
+ protected VelocityArgumentBuilder getThis() {
+ return this;
+ }
+
+ @Override
+ public VelocityArgumentCommandNode build() {
+ return new VelocityArgumentCommandNode<>(this.name, this.type, getCommand(), getRequirement(),
+ getContextRequirement(), getRedirect(), getRedirectModifier(), isFork(),
+ this.suggestionsProvider);
+ }
+}
diff --git a/proxy/src/main/java/com/velocitypowered/proxy/command/brigadier/VelocityArgumentCommandNode.java b/proxy/src/main/java/com/velocitypowered/proxy/command/brigadier/VelocityArgumentCommandNode.java
new file mode 100644
index 000000000..d0bb4f6ec
--- /dev/null
+++ b/proxy/src/main/java/com/velocitypowered/proxy/command/brigadier/VelocityArgumentCommandNode.java
@@ -0,0 +1,138 @@
+/*
+ * Copyright (C) 2018 Velocity Contributors
+ *
+ * This program 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.
+ *
+ * This program 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 this program. If not, see .
+ */
+
+package com.velocitypowered.proxy.command.brigadier;
+
+import com.google.common.base.Preconditions;
+import com.mojang.brigadier.Command;
+import com.mojang.brigadier.ImmutableStringReader;
+import com.mojang.brigadier.RedirectModifier;
+import com.mojang.brigadier.StringReader;
+import com.mojang.brigadier.arguments.ArgumentType;
+import com.mojang.brigadier.arguments.StringArgumentType;
+import com.mojang.brigadier.builder.RequiredArgumentBuilder;
+import com.mojang.brigadier.context.CommandContext;
+import com.mojang.brigadier.context.CommandContextBuilder;
+import com.mojang.brigadier.context.ParsedArgument;
+import com.mojang.brigadier.exceptions.CommandSyntaxException;
+import com.mojang.brigadier.suggestion.SuggestionProvider;
+import com.mojang.brigadier.suggestion.Suggestions;
+import com.mojang.brigadier.suggestion.SuggestionsBuilder;
+import com.mojang.brigadier.tree.ArgumentCommandNode;
+import com.mojang.brigadier.tree.CommandNode;
+import java.util.Collection;
+import java.util.concurrent.CompletableFuture;
+import java.util.function.BiPredicate;
+import java.util.function.Predicate;
+
+/**
+ * An argument node that uses the given (possibly custom) {@link ArgumentType}
+ * for parsing, while maintaining compatibility with the vanilla client.
+ * The argument type must be greedy and accept any input.
+ *
+ * @param the type of the command source
+ * @param the type of the argument to parse
+ */
+public class VelocityArgumentCommandNode extends ArgumentCommandNode {
+
+ private final ArgumentType type;
+
+ VelocityArgumentCommandNode(
+ final String name, final ArgumentType type, final Command command,
+ final Predicate requirement,
+ final BiPredicate, ImmutableStringReader> contextRequirement,
+ final CommandNode redirect, final RedirectModifier modifier, final boolean forks,
+ final SuggestionProvider customSuggestions) {
+ super(name, StringArgumentType.greedyString(), command, requirement, contextRequirement,
+ redirect, modifier, forks, customSuggestions);
+ this.type = Preconditions.checkNotNull(type, "type");
+ }
+
+ @Override
+ public void parse(final StringReader reader, final CommandContextBuilder contextBuilder)
+ throws CommandSyntaxException {
+ // Same as super, except we use the rich ArgumentType
+ final int start = reader.getCursor();
+ final T result = this.type.parse(reader);
+ if (reader.canRead()) {
+ throw CommandSyntaxException.BUILT_IN_EXCEPTIONS.dispatcherParseException()
+ .createWithContext(reader, "Expected greedy ArgumentType to parse all input");
+ }
+
+ final ParsedArgument parsed = new ParsedArgument<>(start, reader.getCursor(), result);
+ contextBuilder.withArgument(getName(), parsed);
+ contextBuilder.withNode(this, parsed.getRange());
+ }
+
+ @Override
+ public CompletableFuture listSuggestions(
+ final CommandContext context, final SuggestionsBuilder builder)
+ throws CommandSyntaxException {
+ if (getCustomSuggestions() == null) {
+ return Suggestions.empty();
+ }
+ return getCustomSuggestions().getSuggestions(context, builder);
+ }
+
+ @Override
+ public RequiredArgumentBuilder createBuilder() {
+ throw new UnsupportedOperationException();
+ }
+
+ @Override
+ public boolean isValidInput(final String input) {
+ return true;
+ }
+
+ @Override
+ public void addChild(final CommandNode node) {
+ throw new UnsupportedOperationException("Cannot add children to a greedy node");
+ }
+
+ @Override
+ public boolean equals(final Object o) {
+ if (this == o) {
+ return true;
+ }
+ if (!(o instanceof VelocityArgumentCommandNode)) {
+ return false;
+ }
+ if (!super.equals(o)) {
+ return false;
+ }
+
+ final VelocityArgumentCommandNode, ?> that = (VelocityArgumentCommandNode, ?>) o;
+ return this.type.equals(that.type);
+ }
+
+ @Override
+ public int hashCode() {
+ int result = super.hashCode();
+ result = 31 * result + this.type.hashCode();
+ return result;
+ }
+
+ @Override
+ public Collection getExamples() {
+ return this.type.getExamples();
+ }
+
+ @Override
+ public String toString() {
+ return "";
+ }
+}
diff --git a/proxy/src/main/java/com/velocitypowered/proxy/command/AbstractCommandInvocation.java b/proxy/src/main/java/com/velocitypowered/proxy/command/invocation/AbstractCommandInvocation.java
similarity index 69%
rename from proxy/src/main/java/com/velocitypowered/proxy/command/AbstractCommandInvocation.java
rename to proxy/src/main/java/com/velocitypowered/proxy/command/invocation/AbstractCommandInvocation.java
index 87bd8e496..43fff5b07 100644
--- a/proxy/src/main/java/com/velocitypowered/proxy/command/AbstractCommandInvocation.java
+++ b/proxy/src/main/java/com/velocitypowered/proxy/command/invocation/AbstractCommandInvocation.java
@@ -15,7 +15,7 @@
* along with this program. If not, see .
*/
-package com.velocitypowered.proxy.command;
+package com.velocitypowered.proxy.command.invocation;
import com.google.common.base.Preconditions;
import com.velocitypowered.api.command.CommandInvocation;
@@ -38,11 +38,35 @@ abstract class AbstractCommandInvocation implements CommandInvocation {
@Override
public CommandSource source() {
- return source;
+ return this.source;
}
@Override
public T arguments() {
- return arguments;
+ return this.arguments;
+ }
+
+ @Override
+ public boolean equals(final Object o) {
+ if (this == o) {
+ return true;
+ }
+ if (o == null || getClass() != o.getClass()) {
+ return false;
+ }
+
+ final AbstractCommandInvocation> that = (AbstractCommandInvocation>) o;
+
+ if (!this.source.equals(that.source)) {
+ return false;
+ }
+ return this.arguments.equals(that.arguments);
+ }
+
+ @Override
+ public int hashCode() {
+ int result = this.source.hashCode();
+ result = 31 * result + this.arguments.hashCode();
+ return result;
}
}
diff --git a/proxy/src/main/java/com/velocitypowered/proxy/command/invocation/CommandInvocationFactory.java b/proxy/src/main/java/com/velocitypowered/proxy/command/invocation/CommandInvocationFactory.java
new file mode 100644
index 000000000..c462a01ce
--- /dev/null
+++ b/proxy/src/main/java/com/velocitypowered/proxy/command/invocation/CommandInvocationFactory.java
@@ -0,0 +1,78 @@
+/*
+ * Copyright (C) 2018 Velocity Contributors
+ *
+ * This program 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.
+ *
+ * This program 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 this program. If not, see .
+ */
+
+package com.velocitypowered.proxy.command.invocation;
+
+import com.mojang.brigadier.context.CommandContext;
+import com.mojang.brigadier.context.CommandContextBuilder;
+import com.mojang.brigadier.context.ParsedArgument;
+import com.mojang.brigadier.context.ParsedCommandNode;
+import com.velocitypowered.api.command.CommandInvocation;
+import com.velocitypowered.api.command.CommandSource;
+import java.util.List;
+import java.util.Map;
+
+/**
+ * Creates command invocation objects from a command context builder or
+ * a command context.
+ *
+ * Let {@code builder} be a command context builder, and {@code context}
+ * a context returned by calling {@link CommandContextBuilder#build(String)} on
+ * {@code builder}. The invocations returned by {@link #create(CommandContext)}
+ * when given {@code context}, and {@link #create(CommandContextBuilder)} when
+ * given {@code builder} are equal.
+ *
+ * @param the type of the built invocation
+ */
+public interface CommandInvocationFactory> {
+
+ /**
+ * Creates an invocation from the given command context.
+ *
+ * @param context the command context
+ * @return the built invocation context
+ */
+ default I create(final CommandContext context) {
+ return this.create(context.getSource(), context.getNodes(), context.getArguments());
+ }
+
+ /**
+ * Creates an invocation from the given command context builder.
+ *
+ * @param context the command context builder
+ * @return the built invocation context
+ */
+ default I create(final CommandContextBuilder context) {
+ return this.create(context.getSource(), context.getNodes(), context.getArguments());
+ }
+
+ /**
+ * Creates an invocation from the given parsed nodes and arguments.
+ *
+ * @param source the command source
+ * @param nodes the list of parsed nodes, as returned by {@link CommandContext#getNodes()} and
+ * {@link CommandContextBuilder#getNodes()}
+ * @param arguments the list of parsed arguments, as returned by
+ * {@link CommandContext#getArguments()} and {@link CommandContextBuilder#getArguments()}
+ * @return the built invocation context
+ */
+ // This provides an abstraction over methods common to CommandContext and CommandContextBuilder.
+ // Annoyingly, they mostly have the same getters but one is (correctly) not a subclass of
+ // the other. Subclasses may override the methods above to obtain class-specific data.
+ I create(final CommandSource source, final List extends ParsedCommandNode>> nodes,
+ final Map> arguments);
+}
diff --git a/proxy/src/main/java/com/velocitypowered/proxy/command/invocation/RawCommandInvocation.java b/proxy/src/main/java/com/velocitypowered/proxy/command/invocation/RawCommandInvocation.java
new file mode 100644
index 000000000..2579a2e65
--- /dev/null
+++ b/proxy/src/main/java/com/velocitypowered/proxy/command/invocation/RawCommandInvocation.java
@@ -0,0 +1,90 @@
+/*
+ * Copyright (C) 2018 Velocity Contributors
+ *
+ * This program 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.
+ *
+ * This program 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 this program. If not, see .
+ */
+
+package com.velocitypowered.proxy.command.invocation;
+
+import com.google.common.base.Preconditions;
+import com.mojang.brigadier.context.ParsedArgument;
+import com.mojang.brigadier.context.ParsedCommandNode;
+import com.velocitypowered.api.command.CommandSource;
+import com.velocitypowered.api.command.RawCommand;
+import com.velocitypowered.proxy.command.VelocityCommands;
+import java.util.List;
+import java.util.Map;
+
+public final class RawCommandInvocation extends AbstractCommandInvocation
+ implements RawCommand.Invocation {
+
+ public static final Factory FACTORY = new Factory();
+
+ private static class Factory implements CommandInvocationFactory {
+
+ @Override
+ public RawCommand.Invocation create(
+ final CommandSource source, final List extends ParsedCommandNode>> nodes,
+ final Map> arguments) {
+ final String alias = VelocityCommands.readAlias(nodes);
+ final String args = VelocityCommands.readArguments(arguments, String.class, "");
+ return new RawCommandInvocation(source, alias, args);
+ }
+ }
+
+ private final String alias;
+
+ private RawCommandInvocation(final CommandSource source,
+ final String alias, final String arguments) {
+ super(source, arguments);
+ this.alias = Preconditions.checkNotNull(alias, "alias");
+ }
+
+ @Override
+ public String alias() {
+ return this.alias;
+ }
+
+ @Override
+ public boolean equals(final Object o) {
+ if (this == o) {
+ return true;
+ }
+ if (o == null || getClass() != o.getClass()) {
+ return false;
+ }
+ if (!super.equals(o)) {
+ return false;
+ }
+
+ final RawCommandInvocation that = (RawCommandInvocation) o;
+ return this.alias.equals(that.alias);
+ }
+
+ @Override
+ public int hashCode() {
+ int result = super.hashCode();
+ result = 31 * result + this.alias.hashCode();
+ return result;
+ }
+
+ @Override
+ public String toString() {
+ return "RawCommandInvocation{"
+ + "source='" + this.source() + '\''
+ + ", alias='" + this.alias + '\''
+ + ", arguments='" + this.arguments() + '\''
+ + '}';
+ }
+}
diff --git a/proxy/src/main/java/com/velocitypowered/proxy/command/invocation/SimpleCommandInvocation.java b/proxy/src/main/java/com/velocitypowered/proxy/command/invocation/SimpleCommandInvocation.java
new file mode 100644
index 000000000..a6ec60611
--- /dev/null
+++ b/proxy/src/main/java/com/velocitypowered/proxy/command/invocation/SimpleCommandInvocation.java
@@ -0,0 +1,93 @@
+/*
+ * Copyright (C) 2018 Velocity Contributors
+ *
+ * This program 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.
+ *
+ * This program 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 this program. If not, see .
+ */
+
+package com.velocitypowered.proxy.command.invocation;
+
+import com.google.common.base.Preconditions;
+import com.mojang.brigadier.context.ParsedArgument;
+import com.mojang.brigadier.context.ParsedCommandNode;
+import com.velocitypowered.api.command.CommandSource;
+import com.velocitypowered.api.command.SimpleCommand;
+import com.velocitypowered.proxy.command.VelocityCommands;
+import com.velocitypowered.proxy.command.brigadier.StringArrayArgumentType;
+import java.util.Arrays;
+import java.util.List;
+import java.util.Map;
+
+public final class SimpleCommandInvocation extends AbstractCommandInvocation
+ implements SimpleCommand.Invocation {
+
+ public static final Factory FACTORY = new Factory();
+
+ private static class Factory implements CommandInvocationFactory {
+
+ @Override
+ public SimpleCommand.Invocation create(
+ final CommandSource source, final List extends ParsedCommandNode>> nodes,
+ final Map> arguments) {
+ final String alias = VelocityCommands.readAlias(nodes);
+ final String[] args = VelocityCommands.readArguments(
+ arguments, String[].class, StringArrayArgumentType.EMPTY);
+ return new SimpleCommandInvocation(source, alias, args);
+ }
+ }
+
+ private final String alias;
+
+ SimpleCommandInvocation(final CommandSource source, final String alias,
+ final String[] arguments) {
+ super(source, arguments);
+ this.alias = Preconditions.checkNotNull(alias, "alias");
+ }
+
+ @Override
+ public String alias() {
+ return this.alias;
+ }
+
+ @Override
+ public boolean equals(final Object o) {
+ if (this == o) {
+ return true;
+ }
+ if (o == null || getClass() != o.getClass()) {
+ return false;
+ }
+ if (!super.equals(o)) {
+ return false;
+ }
+
+ final SimpleCommandInvocation that = (SimpleCommandInvocation) o;
+ return this.alias.equals(that.alias);
+ }
+
+ @Override
+ public int hashCode() {
+ int result = super.hashCode();
+ result = 31 * result + this.alias.hashCode();
+ return result;
+ }
+
+ @Override
+ public String toString() {
+ return "SimpleCommandInvocation{"
+ + "source='" + this.source() + '\''
+ + ", alias='" + this.alias + '\''
+ + ", arguments='" + Arrays.toString(this.arguments()) + '\''
+ + '}';
+ }
+}
diff --git a/proxy/src/main/java/com/velocitypowered/proxy/command/registrar/AbstractCommandRegistrar.java b/proxy/src/main/java/com/velocitypowered/proxy/command/registrar/AbstractCommandRegistrar.java
new file mode 100644
index 000000000..c5fa6f46c
--- /dev/null
+++ b/proxy/src/main/java/com/velocitypowered/proxy/command/registrar/AbstractCommandRegistrar.java
@@ -0,0 +1,61 @@
+/*
+ * Copyright (C) 2018 Velocity Contributors
+ *
+ * This program 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.
+ *
+ * This program 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 this program. If not, see .
+ */
+
+package com.velocitypowered.proxy.command.registrar;
+
+import com.google.common.base.Preconditions;
+import com.mojang.brigadier.tree.LiteralCommandNode;
+import com.mojang.brigadier.tree.RootCommandNode;
+import com.velocitypowered.api.command.Command;
+import com.velocitypowered.api.command.CommandSource;
+import com.velocitypowered.proxy.command.VelocityCommands;
+import java.util.concurrent.locks.Lock;
+import org.checkerframework.checker.lock.qual.GuardedBy;
+
+/**
+ * Base class for {@link CommandRegistrar} implementations.
+ *
+ * @param the type of the command to register
+ */
+abstract class AbstractCommandRegistrar implements CommandRegistrar {
+
+ private final @GuardedBy("lock") RootCommandNode root;
+ private final Lock lock;
+
+ protected AbstractCommandRegistrar(final RootCommandNode root, final Lock lock) {
+ this.root = Preconditions.checkNotNull(root, "root");
+ this.lock = Preconditions.checkNotNull(lock, "lock");
+ }
+
+ protected void register(final LiteralCommandNode node) {
+ lock.lock();
+ try {
+ // Registration overrides previous aliased command
+ root.removeChildByName(node.getName());
+ root.addChild(node);
+ } finally {
+ lock.unlock();
+ }
+ }
+
+ protected void register(final LiteralCommandNode node,
+ final String secondaryAlias) {
+ final LiteralCommandNode copy =
+ VelocityCommands.shallowCopy(node, secondaryAlias);
+ this.register(copy);
+ }
+}
diff --git a/proxy/src/main/java/com/velocitypowered/proxy/command/registrar/BrigadierCommandRegistrar.java b/proxy/src/main/java/com/velocitypowered/proxy/command/registrar/BrigadierCommandRegistrar.java
new file mode 100644
index 000000000..432065f3f
--- /dev/null
+++ b/proxy/src/main/java/com/velocitypowered/proxy/command/registrar/BrigadierCommandRegistrar.java
@@ -0,0 +1,63 @@
+/*
+ * Copyright (C) 2018 Velocity Contributors
+ *
+ * This program 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.
+ *
+ * This program 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 this program. If not, see .
+ */
+
+package com.velocitypowered.proxy.command.registrar;
+
+import com.mojang.brigadier.tree.LiteralCommandNode;
+import com.mojang.brigadier.tree.RootCommandNode;
+import com.velocitypowered.api.command.BrigadierCommand;
+import com.velocitypowered.api.command.CommandMeta;
+import com.velocitypowered.api.command.CommandSource;
+import com.velocitypowered.proxy.command.VelocityCommands;
+import java.util.concurrent.locks.Lock;
+
+/**
+ * Registers {@link BrigadierCommand}s in a root node.
+ */
+public final class BrigadierCommandRegistrar extends AbstractCommandRegistrar {
+
+ public BrigadierCommandRegistrar(final RootCommandNode root, final Lock lock) {
+ super(root, lock);
+ }
+
+ @Override
+ public void register(final CommandMeta meta, final BrigadierCommand command) {
+ // The literal name might not match any aliases on the given meta.
+ // Register it (if valid), since it's probably what the user expects.
+ // If invalid, the metadata contains the same alias, but in lowercase.
+ final LiteralCommandNode literal = command.getNode();
+ final String primaryAlias = literal.getName();
+ if (VelocityCommands.isValidAlias(primaryAlias)) {
+ // Register directly without copying
+ this.register(literal);
+ }
+
+ for (final String alias : meta.getAliases()) {
+ if (primaryAlias.equals(alias)) {
+ continue;
+ }
+ this.register(literal, alias);
+ }
+
+ // Brigadier commands don't support hinting, ignore
+ }
+
+ @Override
+ public Class registrableSuperInterface() {
+ return BrigadierCommand.class;
+ }
+}
diff --git a/proxy/src/main/java/com/velocitypowered/proxy/command/registrar/CommandRegistrar.java b/proxy/src/main/java/com/velocitypowered/proxy/command/registrar/CommandRegistrar.java
new file mode 100644
index 000000000..5b53aee83
--- /dev/null
+++ b/proxy/src/main/java/com/velocitypowered/proxy/command/registrar/CommandRegistrar.java
@@ -0,0 +1,50 @@
+/*
+ * Copyright (C) 2018 Velocity Contributors
+ *
+ * This program 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.
+ *
+ * This program 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 this program. If not, see .
+ */
+
+package com.velocitypowered.proxy.command.registrar;
+
+import com.mojang.brigadier.tree.LiteralCommandNode;
+import com.mojang.brigadier.tree.RootCommandNode;
+import com.velocitypowered.api.command.Command;
+import com.velocitypowered.api.command.CommandMeta;
+
+/**
+ * Creates and registers the {@link LiteralCommandNode} representations of
+ * a given {@link Command} in a {@link RootCommandNode}.
+ *
+ * @param the type of the command to register
+ */
+public interface CommandRegistrar {
+
+ /**
+ * Registers the given command.
+ *
+ * @param meta the command metadata, including the case-insensitive aliases
+ * @param command the command to register
+ * @throws IllegalArgumentException if the given command cannot be registered
+ */
+ void register(final CommandMeta meta, final T command);
+
+ /**
+ * Returns the superclass or superinterface of all {@link Command} classes
+ * compatible with this registrar. Note that {@link #register(CommandMeta, Command)}
+ * may impose additional restrictions on individual {@link Command} instances.
+ *
+ * @return the superclass of all the classes compatible with this registrar
+ */
+ Class registrableSuperInterface();
+}
diff --git a/proxy/src/main/java/com/velocitypowered/proxy/command/registrar/InvocableCommandRegistrar.java b/proxy/src/main/java/com/velocitypowered/proxy/command/registrar/InvocableCommandRegistrar.java
new file mode 100644
index 000000000..e6ff7129c
--- /dev/null
+++ b/proxy/src/main/java/com/velocitypowered/proxy/command/registrar/InvocableCommandRegistrar.java
@@ -0,0 +1,122 @@
+/*
+ * Copyright (C) 2018 Velocity Contributors
+ *
+ * This program 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.
+ *
+ * This program 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 this program. If not, see .
+ */
+
+package com.velocitypowered.proxy.command.registrar;
+
+import com.google.common.base.Preconditions;
+import com.mojang.brigadier.Command;
+import com.mojang.brigadier.arguments.ArgumentType;
+import com.mojang.brigadier.builder.LiteralArgumentBuilder;
+import com.mojang.brigadier.context.CommandContextBuilder;
+import com.mojang.brigadier.tree.ArgumentCommandNode;
+import com.mojang.brigadier.tree.LiteralCommandNode;
+import com.mojang.brigadier.tree.RootCommandNode;
+import com.velocitypowered.api.command.CommandInvocation;
+import com.velocitypowered.api.command.CommandMeta;
+import com.velocitypowered.api.command.CommandSource;
+import com.velocitypowered.api.command.InvocableCommand;
+import com.velocitypowered.proxy.command.VelocityCommandMeta;
+import com.velocitypowered.proxy.command.VelocityCommands;
+import com.velocitypowered.proxy.command.brigadier.VelocityArgumentBuilder;
+import com.velocitypowered.proxy.command.invocation.CommandInvocationFactory;
+import java.util.Iterator;
+import java.util.concurrent.locks.Lock;
+import java.util.function.Predicate;
+
+/**
+ * Base class for {@link CommandRegistrar}s capable of registering a subinterface of
+ * {@link InvocableCommand} in a root node.
+ */
+abstract class InvocableCommandRegistrar,
+ I extends CommandInvocation, A> extends AbstractCommandRegistrar {
+
+ private final CommandInvocationFactory invocationFactory;
+ private final ArgumentType argumentsType;
+
+ protected InvocableCommandRegistrar(final RootCommandNode root, final Lock lock,
+ final CommandInvocationFactory invocationFactory,
+ final ArgumentType argumentsType) {
+ super(root, lock);
+ this.invocationFactory = Preconditions.checkNotNull(invocationFactory, "invocationFactory");
+ this.argumentsType = Preconditions.checkNotNull(argumentsType, "argumentsType");
+ }
+
+ @Override
+ public void register(final CommandMeta meta, final T command) {
+ final Iterator aliases = meta.getAliases().iterator();
+
+ final String primaryAlias = aliases.next();
+ final LiteralCommandNode literal =
+ this.createLiteral(command, meta, primaryAlias);
+ this.register(literal);
+
+ while (aliases.hasNext()) {
+ final String alias = aliases.next();
+ this.register(literal, alias);
+ }
+ }
+
+ private LiteralCommandNode createLiteral(final T command, final CommandMeta meta,
+ final String alias) {
+ final Predicate> requirement = context -> {
+ final I invocation = invocationFactory.create(context);
+ return command.hasPermission(invocation);
+ };
+ final Command callback = context -> {
+ final I invocation = invocationFactory.create(context);
+ command.execute(invocation);
+ return 1; // handled
+ };
+
+ final LiteralCommandNode literal = LiteralArgumentBuilder
+ .literal(alias)
+ .requiresWithContext((context, reader) -> {
+ if (reader.canRead()) {
+ // InvocableCommands do not follow a tree-like permissions checking structure.
+ // Thus, a CommandSource may be able to execute a command with arguments while
+ // not being able to execute the argument-less variant.
+ // Only check for permissions once parsing is complete.
+ return true;
+ }
+ return requirement.test(context);
+ })
+ .executes(callback)
+ .build();
+
+ final ArgumentCommandNode arguments = VelocityArgumentBuilder
+ .velocityArgument(VelocityCommands.ARGS_NODE_NAME, argumentsType)
+ .requiresWithContext((context, reader) -> requirement.test(context))
+ .executes(callback)
+ .suggests((context, builder) -> {
+ final I invocation = invocationFactory.create(context);
+ return command.suggestAsync(invocation).thenApply(suggestions -> {
+ for (String value : suggestions) {
+ Preconditions.checkNotNull(value, "suggestion");
+ builder.suggest(value);
+ }
+ return builder.build();
+ });
+ })
+ .build();
+ literal.addChild(arguments);
+
+ // Add hinting nodes
+ VelocityCommandMeta.copyHints(meta).forEach(literal::addChild);
+
+ return literal;
+ }
+}
diff --git a/proxy/src/main/java/com/velocitypowered/proxy/command/registrar/RawCommandRegistrar.java b/proxy/src/main/java/com/velocitypowered/proxy/command/registrar/RawCommandRegistrar.java
new file mode 100644
index 000000000..2c1e8f109
--- /dev/null
+++ b/proxy/src/main/java/com/velocitypowered/proxy/command/registrar/RawCommandRegistrar.java
@@ -0,0 +1,41 @@
+/*
+ * Copyright (C) 2018 Velocity Contributors
+ *
+ * This program 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.
+ *
+ * This program 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 this program. If not, see .
+ */
+
+package com.velocitypowered.proxy.command.registrar;
+
+import com.mojang.brigadier.arguments.StringArgumentType;
+import com.mojang.brigadier.tree.RootCommandNode;
+import com.velocitypowered.api.command.CommandSource;
+import com.velocitypowered.api.command.RawCommand;
+import com.velocitypowered.proxy.command.invocation.RawCommandInvocation;
+import java.util.concurrent.locks.Lock;
+
+/**
+ * Registers {@link RawCommand}s in a root node.
+ */
+public final class RawCommandRegistrar
+ extends InvocableCommandRegistrar {
+
+ public RawCommandRegistrar(final RootCommandNode root, final Lock lock) {
+ super(root, lock, RawCommandInvocation.FACTORY, StringArgumentType.greedyString());
+ }
+
+ @Override
+ public Class registrableSuperInterface() {
+ return RawCommand.class;
+ }
+}
diff --git a/proxy/src/main/java/com/velocitypowered/proxy/command/registrar/SimpleCommandRegistrar.java b/proxy/src/main/java/com/velocitypowered/proxy/command/registrar/SimpleCommandRegistrar.java
new file mode 100644
index 000000000..bfd4ef430
--- /dev/null
+++ b/proxy/src/main/java/com/velocitypowered/proxy/command/registrar/SimpleCommandRegistrar.java
@@ -0,0 +1,41 @@
+/*
+ * Copyright (C) 2018 Velocity Contributors
+ *
+ * This program 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.
+ *
+ * This program 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 this program. If not, see .
+ */
+
+package com.velocitypowered.proxy.command.registrar;
+
+import com.mojang.brigadier.tree.RootCommandNode;
+import com.velocitypowered.api.command.CommandSource;
+import com.velocitypowered.api.command.SimpleCommand;
+import com.velocitypowered.proxy.command.brigadier.StringArrayArgumentType;
+import com.velocitypowered.proxy.command.invocation.SimpleCommandInvocation;
+import java.util.concurrent.locks.Lock;
+
+/**
+ * Registers {@link SimpleCommand}s in a root node.
+ */
+public final class SimpleCommandRegistrar
+ extends InvocableCommandRegistrar {
+
+ public SimpleCommandRegistrar(final RootCommandNode root, final Lock lock) {
+ super(root, lock, SimpleCommandInvocation.FACTORY, StringArrayArgumentType.INSTANCE);
+ }
+
+ @Override
+ public Class registrableSuperInterface() {
+ return SimpleCommand.class;
+ }
+}
diff --git a/proxy/src/main/java/com/velocitypowered/proxy/connection/backend/BackendPlaySessionHandler.java b/proxy/src/main/java/com/velocitypowered/proxy/connection/backend/BackendPlaySessionHandler.java
index 557b78844..fa8dddafd 100644
--- a/proxy/src/main/java/com/velocitypowered/proxy/connection/backend/BackendPlaySessionHandler.java
+++ b/proxy/src/main/java/com/velocitypowered/proxy/connection/backend/BackendPlaySessionHandler.java
@@ -31,6 +31,7 @@ import com.velocitypowered.api.network.ProtocolVersion;
import com.velocitypowered.api.proxy.messages.ChannelIdentifier;
import com.velocitypowered.api.proxy.player.ResourcePackInfo;
import com.velocitypowered.proxy.VelocityServer;
+import com.velocitypowered.proxy.command.CommandGraphInjector;
import com.velocitypowered.proxy.connection.MinecraftConnection;
import com.velocitypowered.proxy.connection.MinecraftSessionHandler;
import com.velocitypowered.proxy.connection.client.ClientPlaySessionHandler;
@@ -219,18 +220,8 @@ public class BackendPlaySessionHandler implements MinecraftSessionHandler {
RootCommandNode rootNode = commands.getRootNode();
if (server.getConfiguration().isAnnounceProxyCommands()) {
// Inject commands from the proxy.
- RootCommandNode dispatcherRootNode =
- (RootCommandNode)
- filterNode(server.getCommandManager().getDispatcher().getRoot());
- assert dispatcherRootNode != null : "Filtering root node returned null.";
- Collection> proxyNodes = dispatcherRootNode.getChildren();
- for (CommandNode node : proxyNodes) {
- CommandNode existingServerChild = rootNode.getChild(node.getName());
- if (existingServerChild != null) {
- rootNode.getChildren().remove(existingServerChild);
- }
- rootNode.addChild(node);
- }
+ final CommandGraphInjector injector = server.getCommandManager().getInjector();
+ injector.inject(rootNode, serverConn.getPlayer());
}
server.getEventManager().fire(
@@ -243,50 +234,6 @@ public class BackendPlaySessionHandler implements MinecraftSessionHandler {
return true;
}
- /**
- * Creates a deep copy of the provided command node, but removes any node that are not accessible
- * by the player (respecting the requirement of the node).
- *
- * @param source source node
- * @return filtered node
- */
- private CommandNode filterNode(CommandNode source) {
- CommandNode dest;
- if (source instanceof RootCommandNode) {
- dest = new RootCommandNode<>();
- } else {
- if (source.getRequirement() != null) {
- try {
- if (!source.getRequirement().test(serverConn.getPlayer())) {
- return null;
- }
- } catch (Throwable e) {
- // swallow everything because plugins
- logger.error(
- "Requirement test for command node " + source + " encountered an exception", e);
- }
- }
-
- ArgumentBuilder destChildBuilder = source.createBuilder();
- destChildBuilder.requires((commandSource) -> true);
- if (destChildBuilder.getRedirect() != null) {
- destChildBuilder.redirect(filterNode(destChildBuilder.getRedirect()));
- }
-
- dest = destChildBuilder.build();
- }
-
- for (CommandNode sourceChild : source.getChildren()) {
- CommandNode destChild = filterNode(sourceChild);
- if (destChild == null) {
- continue;
- }
- dest.addChild(destChild);
- }
-
- return dest;
- }
-
@Override
public void handleGeneric(MinecraftPacket packet) {
if (packet instanceof PluginMessage) {
diff --git a/proxy/src/main/java/com/velocitypowered/proxy/util/BrigadierUtils.java b/proxy/src/main/java/com/velocitypowered/proxy/util/BrigadierUtils.java
deleted file mode 100644
index db6a5919d..000000000
--- a/proxy/src/main/java/com/velocitypowered/proxy/util/BrigadierUtils.java
+++ /dev/null
@@ -1,169 +0,0 @@
-/*
- * Copyright (C) 2018 Velocity Contributors
- *
- * This program 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.
- *
- * This program 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 this program. If not, see .
- */
-
-package com.velocitypowered.proxy.util;
-
-import com.google.common.base.Preconditions;
-import com.google.common.base.Splitter;
-import com.mojang.brigadier.Command;
-import com.mojang.brigadier.arguments.StringArgumentType;
-import com.mojang.brigadier.builder.ArgumentBuilder;
-import com.mojang.brigadier.builder.LiteralArgumentBuilder;
-import com.mojang.brigadier.builder.RequiredArgumentBuilder;
-import com.mojang.brigadier.context.CommandContext;
-import com.mojang.brigadier.suggestion.SuggestionProvider;
-import com.mojang.brigadier.tree.CommandNode;
-import com.mojang.brigadier.tree.LiteralCommandNode;
-import com.velocitypowered.api.command.CommandSource;
-import java.util.Locale;
-import org.checkerframework.checker.nullness.qual.Nullable;
-
-/**
- * Provides utilities for working with Brigadier commands.
- */
-public final class BrigadierUtils {
-
- private static final Splitter SPACE_SPLITTER = Splitter.on(' ');
-
- /**
- * Returns a literal node that redirects its execution to
- * the given destination node.
- *
- * @param alias the command alias
- * @param destination the destination node
- * @return the built node
- */
- public static LiteralCommandNode buildRedirect(
- final String alias, final LiteralCommandNode destination) {
- // Redirects only work for nodes with children, but break the top argument-less command.
- // Manually adding the root command after setting the redirect doesn't fix it.
- // See https://github.com/Mojang/brigadier/issues/46). Manually clone the node instead.
- LiteralArgumentBuilder builder = LiteralArgumentBuilder
- .literal(alias.toLowerCase(Locale.ENGLISH))
- .requires(destination.getRequirement())
- .forward(
- destination.getRedirect(), destination.getRedirectModifier(), destination.isFork())
- .executes(destination.getCommand());
- for (CommandNode