Mirror von
https://github.com/PaperMC/Velocity.git
synchronisiert 2024-11-17 05:20:14 +01:00
Merge pull request #520 from hugmanrique/feat/3-command-impl-cleanup
[3.0] Command system implementation refactor
Dieser Commit ist enthalten in:
Commit
83dff50bc7
@ -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 <https://www.gnu.org/licenses/>.
|
||||||
|
*/
|
||||||
|
|
||||||
|
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 <S> the type of the source to inject the nodes for
|
||||||
|
*/
|
||||||
|
public final class CommandGraphInjector<S> {
|
||||||
|
|
||||||
|
private static final StringRange ALIAS_RANGE = StringRange.at(0);
|
||||||
|
private static final StringReader ALIAS_READER = new StringReader("");
|
||||||
|
|
||||||
|
private final @GuardedBy("lock") CommandDispatcher<S> dispatcher;
|
||||||
|
private final Lock lock;
|
||||||
|
|
||||||
|
CommandGraphInjector(final CommandDispatcher<S> 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.
|
||||||
|
*
|
||||||
|
* <p>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<S> dest, final S source) {
|
||||||
|
lock.lock();
|
||||||
|
try {
|
||||||
|
final RootCommandNode<S> origin = this.dispatcher.getRoot();
|
||||||
|
final CommandContextBuilder<S> rootContext =
|
||||||
|
new CommandContextBuilder<>(this.dispatcher, source, origin, 0);
|
||||||
|
|
||||||
|
// Filter alias nodes
|
||||||
|
for (final CommandNode<S> node : origin.getChildren()) {
|
||||||
|
if (!node.canUse(source)) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
final CommandContextBuilder<S> context = rootContext.copy()
|
||||||
|
.withNode(node, ALIAS_RANGE);
|
||||||
|
if (!node.canUse(context, ALIAS_READER)) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
final LiteralCommandNode<S> asLiteral = (LiteralCommandNode<S>) node;
|
||||||
|
final LiteralCommandNode<S> copy = asLiteral.createBuilder().build();
|
||||||
|
final VelocityArgumentCommandNode<S, ?> 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<S> child : node.getChildren()) {
|
||||||
|
copy.addChild(child);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
this.addAlias(copy, dest);
|
||||||
|
}
|
||||||
|
} finally {
|
||||||
|
lock.unlock();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private @Nullable CommandNode<S> filterNode(final CommandNode<S> 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<S, ?> 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<S> target = this.filterNode(node.getRedirect(), source);
|
||||||
|
builder.forward(target, builder.getRedirectModifier(), builder.isFork());
|
||||||
|
}
|
||||||
|
final CommandNode<S> result = builder.build();
|
||||||
|
this.copyChildren(node, result, source);
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
private void copyChildren(final CommandNode<S> parent, final CommandNode<S> dest,
|
||||||
|
final S source) {
|
||||||
|
for (final CommandNode<S> child : parent.getChildren()) {
|
||||||
|
final CommandNode<S> filtered = this.filterNode(child, source);
|
||||||
|
if (filtered != null) {
|
||||||
|
dest.addChild(filtered);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void addAlias(final LiteralCommandNode<S> node, final RootCommandNode<S> dest) {
|
||||||
|
dest.removeChildByName(node.getName());
|
||||||
|
dest.addChild(node);
|
||||||
|
}
|
||||||
|
}
|
@ -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 <https://www.gnu.org/licenses/>.
|
|
||||||
*/
|
|
||||||
|
|
||||||
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 <I> the type of the built invocation
|
|
||||||
*/
|
|
||||||
@FunctionalInterface
|
|
||||||
public interface CommandInvocationFactory<I extends CommandInvocation<?>> {
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Returns an invocation context for the given Brigadier context.
|
|
||||||
*
|
|
||||||
* @param context the command context
|
|
||||||
* @return the built invocation context
|
|
||||||
*/
|
|
||||||
I create(final CommandContext<CommandSource> context);
|
|
||||||
}
|
|
@ -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 <https://www.gnu.org/licenses/>.
|
|
||||||
*/
|
|
||||||
|
|
||||||
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<T extends Command> {
|
|
||||||
|
|
||||||
InvocableCommandNodeFactory<SimpleCommand.Invocation> SIMPLE =
|
|
||||||
new InvocableCommandNodeFactory<SimpleCommand.Invocation>() {
|
|
||||||
@Override
|
|
||||||
protected SimpleCommand.Invocation createInvocation(
|
|
||||||
final CommandContext<CommandSource> context) {
|
|
||||||
return VelocitySimpleCommandInvocation.FACTORY.create(context);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
InvocableCommandNodeFactory<RawCommand.Invocation> RAW =
|
|
||||||
new InvocableCommandNodeFactory<RawCommand.Invocation>() {
|
|
||||||
@Override
|
|
||||||
protected RawCommand.Invocation createInvocation(
|
|
||||||
final CommandContext<CommandSource> 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<CommandSource> create(String alias, T command);
|
|
||||||
|
|
||||||
abstract class InvocableCommandNodeFactory<I extends CommandInvocation<?>>
|
|
||||||
implements CommandNodeFactory<InvocableCommand<I>> {
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public LiteralCommandNode<CommandSource> create(
|
|
||||||
final String alias, final InvocableCommand<I> 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<CommandSource> context);
|
|
||||||
}
|
|
||||||
}
|
|
@ -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 <https://www.gnu.org/licenses/>.
|
||||||
|
*/
|
||||||
|
|
||||||
|
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.
|
||||||
|
*
|
||||||
|
* <p>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 <S> the type of the command source
|
||||||
|
*/
|
||||||
|
final class SuggestionsProvider<S> {
|
||||||
|
|
||||||
|
private static final Logger LOGGER = LogManager.getLogger(SuggestionsProvider.class);
|
||||||
|
|
||||||
|
private static final StringRange ALIAS_SUGGESTION_RANGE = StringRange.at(0);
|
||||||
|
|
||||||
|
private final @GuardedBy("lock") CommandDispatcher<S> dispatcher;
|
||||||
|
private final Lock lock;
|
||||||
|
|
||||||
|
SuggestionsProvider(final CommandDispatcher<S> 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<Suggestions> provideSuggestions(final String input, final S source) {
|
||||||
|
final CommandContextBuilder<S> 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<Suggestions> provideSuggestions(
|
||||||
|
final StringReader reader, final CommandContextBuilder<S> context) {
|
||||||
|
lock.lock();
|
||||||
|
try {
|
||||||
|
final StringRange aliasRange = this.consumeAlias(reader);
|
||||||
|
final String alias = aliasRange.get(reader).toLowerCase(Locale.ENGLISH);
|
||||||
|
final LiteralCommandNode<S> literal =
|
||||||
|
(LiteralCommandNode<S>) 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<Suggestions> provideAliasSuggestions(
|
||||||
|
final StringReader reader, final CommandContextBuilder<S> 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<CommandNode<S>> aliases = contextSoFar.getRootNode().getChildren();
|
||||||
|
@SuppressWarnings("unchecked")
|
||||||
|
final CompletableFuture<Suggestions>[] futures = new CompletableFuture[aliases.size()];
|
||||||
|
int i = 0;
|
||||||
|
for (final CommandNode<S> node : aliases) {
|
||||||
|
CompletableFuture<Suggestions> future = Suggestions.empty();
|
||||||
|
final String alias = node.getName();
|
||||||
|
|
||||||
|
if (shouldConsider(alias, input) && node.canUse(source)) {
|
||||||
|
final CommandContextBuilder<S> 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.
|
||||||
|
*
|
||||||
|
* <p>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<Suggestions> provideArgumentsSuggestions(
|
||||||
|
final LiteralCommandNode<S> alias, final StringReader reader,
|
||||||
|
final CommandContextBuilder<S> contextSoFar) {
|
||||||
|
final S source = contextSoFar.getSource();
|
||||||
|
final String fullInput = reader.getString();
|
||||||
|
final VelocityArgumentCommandNode<S, ?> argsNode = VelocityCommands.getArgumentsNode(alias);
|
||||||
|
if (argsNode == null) {
|
||||||
|
// This is a BrigadierCommand, fallback to regular suggestions
|
||||||
|
reader.setCursor(0);
|
||||||
|
final ParseResults<S> 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<S> 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<Suggestions> 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<Suggestions> 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.
|
||||||
|
*
|
||||||
|
* <p>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<Suggestions> getArgumentsNodeSuggestions(
|
||||||
|
final VelocityArgumentCommandNode<S, ?> node, final StringReader reader,
|
||||||
|
final CommandContextBuilder<S> context) {
|
||||||
|
final int start = reader.getCursor();
|
||||||
|
final String fullInput = reader.getString();
|
||||||
|
final CommandContext<S> 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.
|
||||||
|
*
|
||||||
|
* <p>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<Suggestions> getHintSuggestions(
|
||||||
|
final LiteralCommandNode<S> alias, final StringReader reader,
|
||||||
|
final CommandContextBuilder<S> context) {
|
||||||
|
final ParseResults<S> 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.
|
||||||
|
*
|
||||||
|
* <p>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<S> parseHints(final CommandNode<S> node, final StringReader originalReader,
|
||||||
|
final CommandContextBuilder<S> 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<ParseResults<S>> potentials = null;
|
||||||
|
for (final CommandNode<S> child : node.getRelevantNodes(originalReader)) {
|
||||||
|
if (VelocityCommands.isArgumentsNode(child)) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
final CommandContextBuilder<S> 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<S> 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<Suggestions> merge(
|
||||||
|
final String fullInput, final CompletableFuture<Suggestions>... futures) {
|
||||||
|
// https://github.com/Mojang/brigadier/pull/81
|
||||||
|
return CompletableFuture.allOf(futures).handle((unused, throwable) -> {
|
||||||
|
final List<Suggestions> suggestions = new ArrayList<>(futures.length);
|
||||||
|
for (final CompletableFuture<Suggestions> 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);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
@ -18,40 +18,63 @@
|
|||||||
package com.velocitypowered.proxy.command;
|
package com.velocitypowered.proxy.command;
|
||||||
|
|
||||||
import com.google.common.base.Preconditions;
|
import com.google.common.base.Preconditions;
|
||||||
|
import com.google.common.collect.ImmutableList;
|
||||||
import com.google.common.collect.Lists;
|
import com.google.common.collect.Lists;
|
||||||
import com.mojang.brigadier.CommandDispatcher;
|
import com.mojang.brigadier.CommandDispatcher;
|
||||||
import com.mojang.brigadier.ParseResults;
|
import com.mojang.brigadier.ParseResults;
|
||||||
import com.mojang.brigadier.exceptions.CommandSyntaxException;
|
import com.mojang.brigadier.exceptions.CommandSyntaxException;
|
||||||
import com.mojang.brigadier.suggestion.Suggestion;
|
import com.mojang.brigadier.suggestion.Suggestion;
|
||||||
import com.mojang.brigadier.tree.CommandNode;
|
import com.mojang.brigadier.tree.RootCommandNode;
|
||||||
import com.mojang.brigadier.tree.LiteralCommandNode;
|
import com.spotify.futures.CompletableFutures;
|
||||||
import com.velocitypowered.api.command.BrigadierCommand;
|
import com.velocitypowered.api.command.BrigadierCommand;
|
||||||
import com.velocitypowered.api.command.Command;
|
import com.velocitypowered.api.command.Command;
|
||||||
import com.velocitypowered.api.command.CommandManager;
|
import com.velocitypowered.api.command.CommandManager;
|
||||||
import com.velocitypowered.api.command.CommandMeta;
|
import com.velocitypowered.api.command.CommandMeta;
|
||||||
import com.velocitypowered.api.command.CommandSource;
|
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;
|
||||||
import com.velocitypowered.api.event.command.CommandExecuteEvent.CommandResult;
|
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.event.VelocityEventManager;
|
||||||
import com.velocitypowered.proxy.util.BrigadierUtils;
|
|
||||||
import java.util.Iterator;
|
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
import java.util.Locale;
|
import java.util.Locale;
|
||||||
import java.util.concurrent.CompletableFuture;
|
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.identity.Identity;
|
||||||
import net.kyori.adventure.text.Component;
|
import net.kyori.adventure.text.Component;
|
||||||
import net.kyori.adventure.text.format.NamedTextColor;
|
import net.kyori.adventure.text.format.NamedTextColor;
|
||||||
|
import org.checkerframework.checker.lock.qual.GuardedBy;
|
||||||
|
import org.jetbrains.annotations.VisibleForTesting;
|
||||||
|
|
||||||
public class VelocityCommandManager implements CommandManager {
|
public class VelocityCommandManager implements CommandManager {
|
||||||
|
|
||||||
private final CommandDispatcher<CommandSource> dispatcher;
|
private final @GuardedBy("lock") CommandDispatcher<CommandSource> dispatcher;
|
||||||
private final VelocityEventManager eventManager;
|
private final ReadWriteLock lock;
|
||||||
|
|
||||||
|
private final VelocityEventManager eventManager;
|
||||||
|
private final List<CommandRegistrar<?>> registrars;
|
||||||
|
private final SuggestionsProvider<CommandSource> suggestionsProvider;
|
||||||
|
private final CommandGraphInjector<CommandSource> injector;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Constructs a command manager.
|
||||||
|
*
|
||||||
|
* @param eventManager the event manager
|
||||||
|
*/
|
||||||
public VelocityCommandManager(final VelocityEventManager eventManager) {
|
public VelocityCommandManager(final VelocityEventManager eventManager) {
|
||||||
this.eventManager = Preconditions.checkNotNull(eventManager);
|
this.lock = new ReentrantReadWriteLock();
|
||||||
this.dispatcher = new CommandDispatcher<>();
|
this.dispatcher = new CommandDispatcher<>();
|
||||||
|
this.eventManager = Preconditions.checkNotNull(eventManager);
|
||||||
|
final RootCommandNode<CommandSource> 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
|
@Override
|
||||||
@ -77,42 +100,44 @@ public class VelocityCommandManager implements CommandManager {
|
|||||||
Preconditions.checkNotNull(meta, "meta");
|
Preconditions.checkNotNull(meta, "meta");
|
||||||
Preconditions.checkNotNull(command, "command");
|
Preconditions.checkNotNull(command, "command");
|
||||||
|
|
||||||
Iterator<String> aliasIterator = meta.getAliases().iterator();
|
// TODO Warn if command implements multiple registrable interfaces?
|
||||||
String primaryAlias = aliasIterator.next();
|
for (final CommandRegistrar<?> registrar : this.registrars) {
|
||||||
|
if (this.tryRegister(registrar, command, meta)) {
|
||||||
LiteralCommandNode<CommandSource> node = null;
|
return; // success
|
||||||
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<CommandSource> hint : meta.getHints()) {
|
|
||||||
node.addChild(BrigadierUtils.wrapForHinting(hint, node.getCommand()));
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
throw new IllegalArgumentException(
|
||||||
|
command + " does not implement a registrable Command subinterface");
|
||||||
|
}
|
||||||
|
|
||||||
dispatcher.getRoot().addChild(node);
|
/**
|
||||||
while (aliasIterator.hasNext()) {
|
* Attempts to register the given command if it implements the
|
||||||
String currentAlias = aliasIterator.next();
|
* {@linkplain CommandRegistrar#registrableSuperInterface() registrable superinterface}
|
||||||
CommandNode<CommandSource> existingNode = dispatcher.getRoot()
|
* of the given registrar.
|
||||||
.getChild(currentAlias.toLowerCase(Locale.ENGLISH));
|
*
|
||||||
if (existingNode != null) {
|
* @param registrar the registrar to register the command
|
||||||
dispatcher.getRoot().getChildren().remove(existingNode);
|
* @param command the command to register
|
||||||
}
|
* @param meta the command metadata
|
||||||
dispatcher.getRoot().addChild(BrigadierUtils.buildRedirect(currentAlias, node));
|
* @param <T> 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 <T extends Command> boolean tryRegister(final CommandRegistrar<T> registrar,
|
||||||
|
final Command command, final CommandMeta meta) {
|
||||||
|
final Class<T> superInterface = registrar.registrableSuperInterface();
|
||||||
|
if (!superInterface.isInstance(command)) {
|
||||||
|
return false;
|
||||||
}
|
}
|
||||||
|
registrar.register(meta, superInterface.cast(command));
|
||||||
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void unregister(final String alias) {
|
public void unregister(final String alias) {
|
||||||
Preconditions.checkNotNull(alias, "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));
|
dispatcher.getRoot().removeChildByName(alias.toLowerCase(Locale.ENGLISH));
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -134,9 +159,11 @@ public class VelocityCommandManager implements CommandManager {
|
|||||||
Preconditions.checkNotNull(source, "source");
|
Preconditions.checkNotNull(source, "source");
|
||||||
Preconditions.checkNotNull(cmdLine, "cmdLine");
|
Preconditions.checkNotNull(cmdLine, "cmdLine");
|
||||||
|
|
||||||
ParseResults<CommandSource> results = parse(cmdLine, source, true);
|
final String normalizedInput = VelocityCommands.normalizeInput(cmdLine, true);
|
||||||
try {
|
try {
|
||||||
return dispatcher.execute(results) != BrigadierCommand.FORWARD;
|
// The parse can fail if the requirement predicates throw
|
||||||
|
final ParseResults<CommandSource> parse = this.parse(normalizedInput, source);
|
||||||
|
return dispatcher.execute(parse) != BrigadierCommand.FORWARD;
|
||||||
} catch (final CommandSyntaxException e) {
|
} catch (final CommandSyntaxException e) {
|
||||||
boolean isSyntaxError = !e.getType().equals(
|
boolean isSyntaxError = !e.getType().equals(
|
||||||
CommandSyntaxException.BUILT_IN_EXCEPTIONS.dispatcherUnknownCommand());
|
CommandSyntaxException.BUILT_IN_EXCEPTIONS.dispatcherUnknownCommand());
|
||||||
@ -190,22 +217,39 @@ public class VelocityCommandManager implements CommandManager {
|
|||||||
Preconditions.checkNotNull(source, "source");
|
Preconditions.checkNotNull(source, "source");
|
||||||
Preconditions.checkNotNull(cmdLine, "cmdLine");
|
Preconditions.checkNotNull(cmdLine, "cmdLine");
|
||||||
|
|
||||||
ParseResults<CommandSource> parse = parse(cmdLine, source, false);
|
final String normalizedInput = VelocityCommands.normalizeInput(cmdLine, false);
|
||||||
return dispatcher.getCompletionSuggestions(parse)
|
try {
|
||||||
.thenApply(suggestions -> Lists.transform(suggestions.getList(), Suggestion::getText));
|
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<CommandSource> parse(final String cmdLine, final CommandSource source,
|
/**
|
||||||
final boolean trim) {
|
* Parses the given command input.
|
||||||
String normalized = BrigadierUtils.normalizeInput(cmdLine, trim);
|
*
|
||||||
return dispatcher.parse(normalized, source);
|
* @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<CommandSource> 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.
|
* Returns whether the given alias is registered on this manager.
|
||||||
*
|
*
|
||||||
* @param alias the command alias to check
|
* @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
|
@Override
|
||||||
public boolean hasCommand(final String alias) {
|
public boolean hasCommand(final String alias) {
|
||||||
@ -213,7 +257,12 @@ public class VelocityCommandManager implements CommandManager {
|
|||||||
return dispatcher.getRoot().getChild(alias.toLowerCase(Locale.ENGLISH)) != null;
|
return dispatcher.getRoot().getChild(alias.toLowerCase(Locale.ENGLISH)) != null;
|
||||||
}
|
}
|
||||||
|
|
||||||
public CommandDispatcher<CommandSource> getDispatcher() {
|
@VisibleForTesting // this constitutes unsafe publication
|
||||||
return dispatcher;
|
RootCommandNode<CommandSource> getRoot() {
|
||||||
|
return dispatcher.getRoot();
|
||||||
|
}
|
||||||
|
|
||||||
|
public CommandGraphInjector<CommandSource> getInjector() {
|
||||||
|
return injector;
|
||||||
}
|
}
|
||||||
}
|
}
|
@ -20,15 +20,18 @@ package com.velocitypowered.proxy.command;
|
|||||||
import com.google.common.base.Preconditions;
|
import com.google.common.base.Preconditions;
|
||||||
import com.google.common.collect.ImmutableList;
|
import com.google.common.collect.ImmutableList;
|
||||||
import com.google.common.collect.ImmutableSet;
|
import com.google.common.collect.ImmutableSet;
|
||||||
|
import com.mojang.brigadier.builder.ArgumentBuilder;
|
||||||
import com.mojang.brigadier.tree.CommandNode;
|
import com.mojang.brigadier.tree.CommandNode;
|
||||||
|
import com.velocitypowered.api.command.Command;
|
||||||
import com.velocitypowered.api.command.CommandMeta;
|
import com.velocitypowered.api.command.CommandMeta;
|
||||||
import com.velocitypowered.api.command.CommandSource;
|
import com.velocitypowered.api.command.CommandSource;
|
||||||
import java.util.Collection;
|
import java.util.Collection;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
import java.util.Locale;
|
import java.util.Locale;
|
||||||
import java.util.Set;
|
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 {
|
static final class Builder implements CommandMeta.Builder {
|
||||||
|
|
||||||
@ -46,9 +49,9 @@ final class VelocityCommandMeta implements CommandMeta {
|
|||||||
public CommandMeta.Builder aliases(final String... aliases) {
|
public CommandMeta.Builder aliases(final String... aliases) {
|
||||||
Preconditions.checkNotNull(aliases, "aliases");
|
Preconditions.checkNotNull(aliases, "aliases");
|
||||||
for (int i = 0, length = aliases.length; i < length; i++) {
|
for (int i = 0, length = aliases.length; i < length; i++) {
|
||||||
final String alias1 = aliases[i];
|
final String alias = aliases[i];
|
||||||
Preconditions.checkNotNull(alias1, "alias at index %s", i);
|
Preconditions.checkNotNull(alias, "alias at index %s", i);
|
||||||
this.aliases.add(alias1.toLowerCase(Locale.ENGLISH));
|
this.aliases.add(alias.toLowerCase(Locale.ENGLISH));
|
||||||
}
|
}
|
||||||
return this;
|
return this;
|
||||||
}
|
}
|
||||||
@ -56,16 +59,56 @@ final class VelocityCommandMeta implements CommandMeta {
|
|||||||
@Override
|
@Override
|
||||||
public CommandMeta.Builder hint(final CommandNode<CommandSource> node) {
|
public CommandMeta.Builder hint(final CommandNode<CommandSource> node) {
|
||||||
Preconditions.checkNotNull(node, "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;
|
return this;
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public CommandMeta build() {
|
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.
|
||||||
|
*
|
||||||
|
* <p>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<CommandSource> copyForHinting(final CommandNode<CommandSource> 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<CommandSource, ?> builder = hint.createBuilder()
|
||||||
|
// Requirement checking is performed by SuggestionProvider
|
||||||
|
.requires(source -> false);
|
||||||
|
for (final CommandNode<CommandSource> 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<CommandNode<CommandSource>> copyHints(final CommandMeta meta) {
|
||||||
|
return meta.getHints().stream().map(VelocityCommandMeta::copyForHinting);
|
||||||
|
}
|
||||||
|
|
||||||
private final Set<String> aliases;
|
private final Set<String> aliases;
|
||||||
private final List<CommandNode<CommandSource>> hints;
|
private final List<CommandNode<CommandSource>> hints;
|
||||||
|
|
||||||
@ -77,11 +120,35 @@ final class VelocityCommandMeta implements CommandMeta {
|
|||||||
|
|
||||||
@Override
|
@Override
|
||||||
public Collection<String> getAliases() {
|
public Collection<String> getAliases() {
|
||||||
return aliases;
|
return this.aliases;
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public Collection<CommandNode<CommandSource>> getHints() {
|
public Collection<CommandNode<CommandSource>> 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;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -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 <https://www.gnu.org/licenses/>.
|
||||||
|
*/
|
||||||
|
|
||||||
|
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 <V> the type of the arguments
|
||||||
|
* @return the command arguments
|
||||||
|
*/
|
||||||
|
public static <V> V readArguments(final Map<String, ? extends ParsedArgument<?, ?>> arguments,
|
||||||
|
final Class<V> 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}.
|
||||||
|
*
|
||||||
|
* <p>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<CommandSource> shallowCopy(
|
||||||
|
final LiteralCommandNode<CommandSource> original, final String newName) {
|
||||||
|
// Brigadier resolves the redirect of a node if further input can be parsed.
|
||||||
|
// Let <bar> be a literal node having a redirect to a <foo> literal. Then,
|
||||||
|
// the context returned by CommandDispatcher#parseNodes when given the input
|
||||||
|
// string "<bar> " does not contain a child context with <foo> as its root node.
|
||||||
|
// Thus, the vanilla client asks the children of <bar> for suggestions, instead
|
||||||
|
// of those of <foo> (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<CommandSource> builder = LiteralArgumentBuilder
|
||||||
|
.<CommandSource>literal(newName)
|
||||||
|
.requires(original.getRequirement())
|
||||||
|
.requiresWithContext(original.getContextRequirement())
|
||||||
|
.forward(original.getRedirect(), original.getRedirectModifier(), original.isFork())
|
||||||
|
.executes(original.getCommand());
|
||||||
|
for (final CommandNode<CommandSource> 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 <S> the type of the command source
|
||||||
|
* @return the arguments node, or null if not present
|
||||||
|
*/
|
||||||
|
static <S> @Nullable VelocityArgumentCommandNode<S, ?> getArgumentsNode(
|
||||||
|
final LiteralCommandNode<S> alias) {
|
||||||
|
final CommandNode<S> node = alias.getChild(ARGS_NODE_NAME);
|
||||||
|
if (node instanceof VelocityArgumentCommandNode) {
|
||||||
|
return (VelocityArgumentCommandNode<S, ?>) 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();
|
||||||
|
}
|
||||||
|
}
|
@ -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 <https://www.gnu.org/licenses/>.
|
|
||||||
*/
|
|
||||||
|
|
||||||
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<String>
|
|
||||||
implements RawCommand.Invocation {
|
|
||||||
|
|
||||||
static final Factory FACTORY = new Factory();
|
|
||||||
|
|
||||||
static class Factory implements CommandInvocationFactory<RawCommand.Invocation> {
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public RawCommand.Invocation create(final CommandContext<CommandSource> 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;
|
|
||||||
}
|
|
||||||
}
|
|
@ -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 <https://www.gnu.org/licenses/>.
|
|
||||||
*/
|
|
||||||
|
|
||||||
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<String[]>
|
|
||||||
implements SimpleCommand.Invocation {
|
|
||||||
|
|
||||||
static final Factory FACTORY = new Factory();
|
|
||||||
|
|
||||||
static class Factory implements CommandInvocationFactory<SimpleCommand.Invocation> {
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public SimpleCommand.Invocation create(final CommandContext<CommandSource> 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;
|
|
||||||
}
|
|
||||||
}
|
|
@ -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 <https://www.gnu.org/licenses/>.
|
||||||
|
*/
|
||||||
|
|
||||||
|
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<String[]> {
|
||||||
|
|
||||||
|
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<String> 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<String> getExamples() {
|
||||||
|
return EXAMPLES;
|
||||||
|
}
|
||||||
|
}
|
@ -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 <https://www.gnu.org/licenses/>.
|
||||||
|
*/
|
||||||
|
|
||||||
|
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 <S> the type of the command source
|
||||||
|
* @param <T> the type of the argument to parse
|
||||||
|
*/
|
||||||
|
public final class VelocityArgumentBuilder<S, T>
|
||||||
|
extends ArgumentBuilder<S, VelocityArgumentBuilder<S, T>> {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 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 <S> the type of the command source
|
||||||
|
* @param <T> the type of the argument to parse
|
||||||
|
* @return a builder
|
||||||
|
*/
|
||||||
|
public static <S, T> VelocityArgumentBuilder<S, T> velocityArgument(final String name,
|
||||||
|
final ArgumentType<T> type) {
|
||||||
|
Preconditions.checkNotNull(name, "name");
|
||||||
|
Preconditions.checkNotNull(type, "type");
|
||||||
|
return new VelocityArgumentBuilder<>(name, type);
|
||||||
|
}
|
||||||
|
|
||||||
|
private final String name;
|
||||||
|
private final ArgumentType<T> type;
|
||||||
|
private SuggestionProvider<S> suggestionsProvider = null;
|
||||||
|
|
||||||
|
private VelocityArgumentBuilder(final String name, final ArgumentType<T> type) {
|
||||||
|
this.name = name;
|
||||||
|
this.type = type;
|
||||||
|
}
|
||||||
|
|
||||||
|
public VelocityArgumentBuilder<S, T> suggests(final @Nullable SuggestionProvider<S> provider) {
|
||||||
|
this.suggestionsProvider = provider;
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public VelocityArgumentBuilder<S, T> then(final ArgumentBuilder<S, ?> argument) {
|
||||||
|
throw new UnsupportedOperationException("Cannot add children to a greedy node");
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public VelocityArgumentBuilder<S, T> then(final CommandNode<S> argument) {
|
||||||
|
throw new UnsupportedOperationException("Cannot add children to a greedy node");
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
protected VelocityArgumentBuilder<S, T> getThis() {
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public VelocityArgumentCommandNode<S, T> build() {
|
||||||
|
return new VelocityArgumentCommandNode<>(this.name, this.type, getCommand(), getRequirement(),
|
||||||
|
getContextRequirement(), getRedirect(), getRedirectModifier(), isFork(),
|
||||||
|
this.suggestionsProvider);
|
||||||
|
}
|
||||||
|
}
|
@ -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 <https://www.gnu.org/licenses/>.
|
||||||
|
*/
|
||||||
|
|
||||||
|
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 <S> the type of the command source
|
||||||
|
* @param <T> the type of the argument to parse
|
||||||
|
*/
|
||||||
|
public class VelocityArgumentCommandNode<S, T> extends ArgumentCommandNode<S, String> {
|
||||||
|
|
||||||
|
private final ArgumentType<T> type;
|
||||||
|
|
||||||
|
VelocityArgumentCommandNode(
|
||||||
|
final String name, final ArgumentType<T> type, final Command<S> command,
|
||||||
|
final Predicate<S> requirement,
|
||||||
|
final BiPredicate<CommandContextBuilder<S>, ImmutableStringReader> contextRequirement,
|
||||||
|
final CommandNode<S> redirect, final RedirectModifier<S> modifier, final boolean forks,
|
||||||
|
final SuggestionProvider<S> 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<S> 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<S, T> parsed = new ParsedArgument<>(start, reader.getCursor(), result);
|
||||||
|
contextBuilder.withArgument(getName(), parsed);
|
||||||
|
contextBuilder.withNode(this, parsed.getRange());
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public CompletableFuture<Suggestions> listSuggestions(
|
||||||
|
final CommandContext<S> context, final SuggestionsBuilder builder)
|
||||||
|
throws CommandSyntaxException {
|
||||||
|
if (getCustomSuggestions() == null) {
|
||||||
|
return Suggestions.empty();
|
||||||
|
}
|
||||||
|
return getCustomSuggestions().getSuggestions(context, builder);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public RequiredArgumentBuilder<S, String> createBuilder() {
|
||||||
|
throw new UnsupportedOperationException();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public boolean isValidInput(final String input) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void addChild(final CommandNode<S> 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<String> getExamples() {
|
||||||
|
return this.type.getExamples();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public String toString() {
|
||||||
|
return "<argument " + this.getName() + ":" + this.type + ">";
|
||||||
|
}
|
||||||
|
}
|
@ -15,7 +15,7 @@
|
|||||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
package com.velocitypowered.proxy.command;
|
package com.velocitypowered.proxy.command.invocation;
|
||||||
|
|
||||||
import com.google.common.base.Preconditions;
|
import com.google.common.base.Preconditions;
|
||||||
import com.velocitypowered.api.command.CommandInvocation;
|
import com.velocitypowered.api.command.CommandInvocation;
|
||||||
@ -38,11 +38,35 @@ abstract class AbstractCommandInvocation<T> implements CommandInvocation<T> {
|
|||||||
|
|
||||||
@Override
|
@Override
|
||||||
public CommandSource source() {
|
public CommandSource source() {
|
||||||
return source;
|
return this.source;
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public T arguments() {
|
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;
|
||||||
}
|
}
|
||||||
}
|
}
|
@ -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 <https://www.gnu.org/licenses/>.
|
||||||
|
*/
|
||||||
|
|
||||||
|
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.
|
||||||
|
*
|
||||||
|
* <p>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 <I> the type of the built invocation
|
||||||
|
*/
|
||||||
|
public interface CommandInvocationFactory<I extends CommandInvocation<?>> {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Creates an invocation from the given command context.
|
||||||
|
*
|
||||||
|
* @param context the command context
|
||||||
|
* @return the built invocation context
|
||||||
|
*/
|
||||||
|
default I create(final CommandContext<CommandSource> 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<CommandSource> 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<String, ? extends ParsedArgument<?, ?>> arguments);
|
||||||
|
}
|
@ -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 <https://www.gnu.org/licenses/>.
|
||||||
|
*/
|
||||||
|
|
||||||
|
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<String>
|
||||||
|
implements RawCommand.Invocation {
|
||||||
|
|
||||||
|
public static final Factory FACTORY = new Factory();
|
||||||
|
|
||||||
|
private static class Factory implements CommandInvocationFactory<RawCommand.Invocation> {
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public RawCommand.Invocation create(
|
||||||
|
final CommandSource source, final List<? extends ParsedCommandNode<?>> nodes,
|
||||||
|
final Map<String, ? extends ParsedArgument<?, ?>> 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() + '\''
|
||||||
|
+ '}';
|
||||||
|
}
|
||||||
|
}
|
@ -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 <https://www.gnu.org/licenses/>.
|
||||||
|
*/
|
||||||
|
|
||||||
|
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<String[]>
|
||||||
|
implements SimpleCommand.Invocation {
|
||||||
|
|
||||||
|
public static final Factory FACTORY = new Factory();
|
||||||
|
|
||||||
|
private static class Factory implements CommandInvocationFactory<SimpleCommand.Invocation> {
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public SimpleCommand.Invocation create(
|
||||||
|
final CommandSource source, final List<? extends ParsedCommandNode<?>> nodes,
|
||||||
|
final Map<String, ? extends ParsedArgument<?, ?>> 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()) + '\''
|
||||||
|
+ '}';
|
||||||
|
}
|
||||||
|
}
|
@ -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 <https://www.gnu.org/licenses/>.
|
||||||
|
*/
|
||||||
|
|
||||||
|
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 <T> the type of the command to register
|
||||||
|
*/
|
||||||
|
abstract class AbstractCommandRegistrar<T extends Command> implements CommandRegistrar<T> {
|
||||||
|
|
||||||
|
private final @GuardedBy("lock") RootCommandNode<CommandSource> root;
|
||||||
|
private final Lock lock;
|
||||||
|
|
||||||
|
protected AbstractCommandRegistrar(final RootCommandNode<CommandSource> root, final Lock lock) {
|
||||||
|
this.root = Preconditions.checkNotNull(root, "root");
|
||||||
|
this.lock = Preconditions.checkNotNull(lock, "lock");
|
||||||
|
}
|
||||||
|
|
||||||
|
protected void register(final LiteralCommandNode<CommandSource> node) {
|
||||||
|
lock.lock();
|
||||||
|
try {
|
||||||
|
// Registration overrides previous aliased command
|
||||||
|
root.removeChildByName(node.getName());
|
||||||
|
root.addChild(node);
|
||||||
|
} finally {
|
||||||
|
lock.unlock();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
protected void register(final LiteralCommandNode<CommandSource> node,
|
||||||
|
final String secondaryAlias) {
|
||||||
|
final LiteralCommandNode<CommandSource> copy =
|
||||||
|
VelocityCommands.shallowCopy(node, secondaryAlias);
|
||||||
|
this.register(copy);
|
||||||
|
}
|
||||||
|
}
|
@ -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 <https://www.gnu.org/licenses/>.
|
||||||
|
*/
|
||||||
|
|
||||||
|
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<BrigadierCommand> {
|
||||||
|
|
||||||
|
public BrigadierCommandRegistrar(final RootCommandNode<CommandSource> 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<CommandSource> 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<BrigadierCommand> registrableSuperInterface() {
|
||||||
|
return BrigadierCommand.class;
|
||||||
|
}
|
||||||
|
}
|
@ -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 <https://www.gnu.org/licenses/>.
|
||||||
|
*/
|
||||||
|
|
||||||
|
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 <T> the type of the command to register
|
||||||
|
*/
|
||||||
|
public interface CommandRegistrar<T extends Command> {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 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<T> registrableSuperInterface();
|
||||||
|
}
|
@ -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 <https://www.gnu.org/licenses/>.
|
||||||
|
*/
|
||||||
|
|
||||||
|
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<T extends InvocableCommand<I>,
|
||||||
|
I extends CommandInvocation<A>, A> extends AbstractCommandRegistrar<T> {
|
||||||
|
|
||||||
|
private final CommandInvocationFactory<I> invocationFactory;
|
||||||
|
private final ArgumentType<A> argumentsType;
|
||||||
|
|
||||||
|
protected InvocableCommandRegistrar(final RootCommandNode<CommandSource> root, final Lock lock,
|
||||||
|
final CommandInvocationFactory<I> invocationFactory,
|
||||||
|
final ArgumentType<A> 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<String> aliases = meta.getAliases().iterator();
|
||||||
|
|
||||||
|
final String primaryAlias = aliases.next();
|
||||||
|
final LiteralCommandNode<CommandSource> literal =
|
||||||
|
this.createLiteral(command, meta, primaryAlias);
|
||||||
|
this.register(literal);
|
||||||
|
|
||||||
|
while (aliases.hasNext()) {
|
||||||
|
final String alias = aliases.next();
|
||||||
|
this.register(literal, alias);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private LiteralCommandNode<CommandSource> createLiteral(final T command, final CommandMeta meta,
|
||||||
|
final String alias) {
|
||||||
|
final Predicate<CommandContextBuilder<CommandSource>> requirement = context -> {
|
||||||
|
final I invocation = invocationFactory.create(context);
|
||||||
|
return command.hasPermission(invocation);
|
||||||
|
};
|
||||||
|
final Command<CommandSource> callback = context -> {
|
||||||
|
final I invocation = invocationFactory.create(context);
|
||||||
|
command.execute(invocation);
|
||||||
|
return 1; // handled
|
||||||
|
};
|
||||||
|
|
||||||
|
final LiteralCommandNode<CommandSource> literal = LiteralArgumentBuilder
|
||||||
|
.<CommandSource>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<CommandSource, String> arguments = VelocityArgumentBuilder
|
||||||
|
.<CommandSource, A>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;
|
||||||
|
}
|
||||||
|
}
|
@ -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 <https://www.gnu.org/licenses/>.
|
||||||
|
*/
|
||||||
|
|
||||||
|
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<RawCommand, RawCommand.Invocation, String> {
|
||||||
|
|
||||||
|
public RawCommandRegistrar(final RootCommandNode<CommandSource> root, final Lock lock) {
|
||||||
|
super(root, lock, RawCommandInvocation.FACTORY, StringArgumentType.greedyString());
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public Class<RawCommand> registrableSuperInterface() {
|
||||||
|
return RawCommand.class;
|
||||||
|
}
|
||||||
|
}
|
@ -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 <https://www.gnu.org/licenses/>.
|
||||||
|
*/
|
||||||
|
|
||||||
|
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<SimpleCommand, SimpleCommand.Invocation, String[]> {
|
||||||
|
|
||||||
|
public SimpleCommandRegistrar(final RootCommandNode<CommandSource> root, final Lock lock) {
|
||||||
|
super(root, lock, SimpleCommandInvocation.FACTORY, StringArrayArgumentType.INSTANCE);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public Class<SimpleCommand> registrableSuperInterface() {
|
||||||
|
return SimpleCommand.class;
|
||||||
|
}
|
||||||
|
}
|
@ -31,6 +31,7 @@ import com.velocitypowered.api.network.ProtocolVersion;
|
|||||||
import com.velocitypowered.api.proxy.messages.ChannelIdentifier;
|
import com.velocitypowered.api.proxy.messages.ChannelIdentifier;
|
||||||
import com.velocitypowered.api.proxy.player.ResourcePackInfo;
|
import com.velocitypowered.api.proxy.player.ResourcePackInfo;
|
||||||
import com.velocitypowered.proxy.VelocityServer;
|
import com.velocitypowered.proxy.VelocityServer;
|
||||||
|
import com.velocitypowered.proxy.command.CommandGraphInjector;
|
||||||
import com.velocitypowered.proxy.connection.MinecraftConnection;
|
import com.velocitypowered.proxy.connection.MinecraftConnection;
|
||||||
import com.velocitypowered.proxy.connection.MinecraftSessionHandler;
|
import com.velocitypowered.proxy.connection.MinecraftSessionHandler;
|
||||||
import com.velocitypowered.proxy.connection.client.ClientPlaySessionHandler;
|
import com.velocitypowered.proxy.connection.client.ClientPlaySessionHandler;
|
||||||
@ -219,18 +220,8 @@ public class BackendPlaySessionHandler implements MinecraftSessionHandler {
|
|||||||
RootCommandNode<CommandSource> rootNode = commands.getRootNode();
|
RootCommandNode<CommandSource> rootNode = commands.getRootNode();
|
||||||
if (server.getConfiguration().isAnnounceProxyCommands()) {
|
if (server.getConfiguration().isAnnounceProxyCommands()) {
|
||||||
// Inject commands from the proxy.
|
// Inject commands from the proxy.
|
||||||
RootCommandNode<CommandSource> dispatcherRootNode =
|
final CommandGraphInjector<CommandSource> injector = server.getCommandManager().getInjector();
|
||||||
(RootCommandNode<CommandSource>)
|
injector.inject(rootNode, serverConn.getPlayer());
|
||||||
filterNode(server.getCommandManager().getDispatcher().getRoot());
|
|
||||||
assert dispatcherRootNode != null : "Filtering root node returned null.";
|
|
||||||
Collection<CommandNode<CommandSource>> proxyNodes = dispatcherRootNode.getChildren();
|
|
||||||
for (CommandNode<CommandSource> node : proxyNodes) {
|
|
||||||
CommandNode<CommandSource> existingServerChild = rootNode.getChild(node.getName());
|
|
||||||
if (existingServerChild != null) {
|
|
||||||
rootNode.getChildren().remove(existingServerChild);
|
|
||||||
}
|
|
||||||
rootNode.addChild(node);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
server.getEventManager().fire(
|
server.getEventManager().fire(
|
||||||
@ -243,50 +234,6 @@ public class BackendPlaySessionHandler implements MinecraftSessionHandler {
|
|||||||
return true;
|
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<CommandSource> filterNode(CommandNode<CommandSource> source) {
|
|
||||||
CommandNode<CommandSource> 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<CommandSource, ?> destChildBuilder = source.createBuilder();
|
|
||||||
destChildBuilder.requires((commandSource) -> true);
|
|
||||||
if (destChildBuilder.getRedirect() != null) {
|
|
||||||
destChildBuilder.redirect(filterNode(destChildBuilder.getRedirect()));
|
|
||||||
}
|
|
||||||
|
|
||||||
dest = destChildBuilder.build();
|
|
||||||
}
|
|
||||||
|
|
||||||
for (CommandNode<CommandSource> sourceChild : source.getChildren()) {
|
|
||||||
CommandNode<CommandSource> destChild = filterNode(sourceChild);
|
|
||||||
if (destChild == null) {
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
dest.addChild(destChild);
|
|
||||||
}
|
|
||||||
|
|
||||||
return dest;
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void handleGeneric(MinecraftPacket packet) {
|
public void handleGeneric(MinecraftPacket packet) {
|
||||||
if (packet instanceof PluginMessage) {
|
if (packet instanceof PluginMessage) {
|
||||||
|
@ -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 <https://www.gnu.org/licenses/>.
|
|
||||||
*/
|
|
||||||
|
|
||||||
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<CommandSource> buildRedirect(
|
|
||||||
final String alias, final LiteralCommandNode<CommandSource> 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<CommandSource> builder = LiteralArgumentBuilder
|
|
||||||
.<CommandSource>literal(alias.toLowerCase(Locale.ENGLISH))
|
|
||||||
.requires(destination.getRequirement())
|
|
||||||
.forward(
|
|
||||||
destination.getRedirect(), destination.getRedirectModifier(), destination.isFork())
|
|
||||||
.executes(destination.getCommand());
|
|
||||||
for (CommandNode<CommandSource> child : destination.getChildren()) {
|
|
||||||
builder.then(child);
|
|
||||||
}
|
|
||||||
return builder.build();
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Returns a literal node that optionally accepts arguments
|
|
||||||
* as a raw {@link String}.
|
|
||||||
*
|
|
||||||
* @param alias the literal alias
|
|
||||||
* @param brigadierCommand the command to execute
|
|
||||||
* @param suggestionProvider the suggestion provider
|
|
||||||
* @return the built node
|
|
||||||
*/
|
|
||||||
public static LiteralCommandNode<CommandSource> buildRawArgumentsLiteral(
|
|
||||||
final String alias, final Command<CommandSource> brigadierCommand,
|
|
||||||
SuggestionProvider<CommandSource> suggestionProvider) {
|
|
||||||
return LiteralArgumentBuilder
|
|
||||||
.<CommandSource>literal(alias.toLowerCase(Locale.ENGLISH))
|
|
||||||
.then(RequiredArgumentBuilder
|
|
||||||
.<CommandSource, String>argument("arguments", StringArgumentType.greedyString())
|
|
||||||
.suggests(suggestionProvider)
|
|
||||||
.executes(brigadierCommand))
|
|
||||||
.executes(brigadierCommand)
|
|
||||||
.build();
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Returns the used command alias.
|
|
||||||
*
|
|
||||||
* @param context the command context
|
|
||||||
* @return the parsed command alias
|
|
||||||
*/
|
|
||||||
public static String getAlias(final CommandContext<CommandSource> context) {
|
|
||||||
return context.getNodes().get(0).getNode().getName();
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Returns the raw {@link String} arguments of a command execution.
|
|
||||||
*
|
|
||||||
* @param context the command context
|
|
||||||
* @return the parsed arguments
|
|
||||||
*/
|
|
||||||
public static String getRawArguments(final CommandContext<CommandSource> context) {
|
|
||||||
String cmdLine = context.getInput();
|
|
||||||
int firstSpace = cmdLine.indexOf(' ');
|
|
||||||
if (firstSpace == -1) {
|
|
||||||
return "";
|
|
||||||
}
|
|
||||||
return cmdLine.substring(firstSpace + 1);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Returns the splitted arguments of a command node built with
|
|
||||||
* {@link #buildRawArgumentsLiteral(String, Command, SuggestionProvider)}.
|
|
||||||
*
|
|
||||||
* @param context the command context
|
|
||||||
* @return the parsed arguments
|
|
||||||
*/
|
|
||||||
public static String[] getSplitArguments(final CommandContext<CommandSource> context) {
|
|
||||||
String line = getRawArguments(context);
|
|
||||||
if (line.isEmpty()) {
|
|
||||||
return new String[0];
|
|
||||||
}
|
|
||||||
return SPACE_SPLITTER.splitToList(line).toArray(new String[0]);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Returns the normalized representation of the given command input.
|
|
||||||
*
|
|
||||||
* @param cmdLine the command input
|
|
||||||
* @param trim whether to trim argument-less inputs
|
|
||||||
* @return the normalized command
|
|
||||||
*/
|
|
||||||
public static String normalizeInput(final String cmdLine, final boolean trim) {
|
|
||||||
// Command aliases are case insensitive, but Brigadier isn't
|
|
||||||
String command = trim ? cmdLine.trim() : cmdLine;
|
|
||||||
int firstSpace = command.indexOf(' ');
|
|
||||||
if (firstSpace != -1) {
|
|
||||||
return command.substring(0, firstSpace).toLowerCase(Locale.ENGLISH)
|
|
||||||
+ command.substring(firstSpace);
|
|
||||||
}
|
|
||||||
return command.toLowerCase(Locale.ENGLISH);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Prepares the given command node prior for hinting metadata to
|
|
||||||
* a {@link com.velocitypowered.api.command.Command}.
|
|
||||||
*
|
|
||||||
* @param node the command node to be wrapped
|
|
||||||
* @param command the command to execute
|
|
||||||
* @return the wrapped command node
|
|
||||||
*/
|
|
||||||
public static CommandNode<CommandSource> wrapForHinting(
|
|
||||||
final CommandNode<CommandSource> node, final @Nullable Command<CommandSource> command) {
|
|
||||||
Preconditions.checkNotNull(node, "node");
|
|
||||||
ArgumentBuilder<CommandSource, ?> builder = node.createBuilder();
|
|
||||||
builder.executes(command);
|
|
||||||
for (CommandNode<CommandSource> child : node.getChildren()) {
|
|
||||||
builder.then(wrapForHinting(child, command));
|
|
||||||
}
|
|
||||||
return builder.build();
|
|
||||||
}
|
|
||||||
|
|
||||||
private BrigadierUtils() {
|
|
||||||
throw new AssertionError();
|
|
||||||
}
|
|
||||||
}
|
|
@ -0,0 +1,349 @@
|
|||||||
|
/*
|
||||||
|
* 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 <https://www.gnu.org/licenses/>.
|
||||||
|
*/
|
||||||
|
|
||||||
|
package com.velocitypowered.proxy.command;
|
||||||
|
|
||||||
|
import static com.mojang.brigadier.arguments.IntegerArgumentType.getInteger;
|
||||||
|
import static com.mojang.brigadier.arguments.IntegerArgumentType.integer;
|
||||||
|
import static com.mojang.brigadier.arguments.StringArgumentType.word;
|
||||||
|
import static org.junit.jupiter.api.Assertions.assertEquals;
|
||||||
|
import static org.junit.jupiter.api.Assertions.assertSame;
|
||||||
|
import static org.junit.jupiter.api.Assertions.assertThrows;
|
||||||
|
import static org.junit.jupiter.api.Assertions.fail;
|
||||||
|
|
||||||
|
import com.mojang.brigadier.builder.LiteralArgumentBuilder;
|
||||||
|
import com.mojang.brigadier.builder.RequiredArgumentBuilder;
|
||||||
|
import com.velocitypowered.api.command.BrigadierCommand;
|
||||||
|
import com.velocitypowered.api.command.CommandSource;
|
||||||
|
import java.util.concurrent.CompletableFuture;
|
||||||
|
import java.util.concurrent.CompletionException;
|
||||||
|
import java.util.concurrent.atomic.AtomicInteger;
|
||||||
|
import org.junit.jupiter.api.Test;
|
||||||
|
|
||||||
|
public class BrigadierCommandTests extends CommandTestSuite {
|
||||||
|
|
||||||
|
// Execution
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void testExecutesAlias() {
|
||||||
|
final var callCount = new AtomicInteger();
|
||||||
|
|
||||||
|
final var node = LiteralArgumentBuilder
|
||||||
|
.<CommandSource>literal("hello")
|
||||||
|
.executes(context -> {
|
||||||
|
assertEquals(source, context.getSource());
|
||||||
|
assertEquals("hello", context.getInput());
|
||||||
|
assertEquals(1, context.getNodes().size());
|
||||||
|
callCount.incrementAndGet();
|
||||||
|
return 1;
|
||||||
|
})
|
||||||
|
.build();
|
||||||
|
manager.register(new BrigadierCommand(node));
|
||||||
|
|
||||||
|
assertHandled("hello");
|
||||||
|
assertEquals(1, callCount.get());
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void testExecuteIgnoresAliasCase() {
|
||||||
|
final var callCount = new AtomicInteger();
|
||||||
|
|
||||||
|
final var node = LiteralArgumentBuilder
|
||||||
|
.<CommandSource>literal("hello")
|
||||||
|
.executes(context -> {
|
||||||
|
assertEquals("hello", context.getInput());
|
||||||
|
callCount.incrementAndGet();
|
||||||
|
return 1;
|
||||||
|
})
|
||||||
|
.build();
|
||||||
|
manager.register(new BrigadierCommand(node));
|
||||||
|
|
||||||
|
assertHandled("Hello");
|
||||||
|
assertEquals(1, callCount.get());
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void testExecuteInputIsTrimmed() {
|
||||||
|
final var callCount = new AtomicInteger();
|
||||||
|
|
||||||
|
final var node = LiteralArgumentBuilder
|
||||||
|
.<CommandSource>literal("hello")
|
||||||
|
.executes(context -> {
|
||||||
|
assertEquals("hello", context.getInput());
|
||||||
|
callCount.incrementAndGet();
|
||||||
|
return 1;
|
||||||
|
})
|
||||||
|
.build();
|
||||||
|
manager.register(new BrigadierCommand(node));
|
||||||
|
|
||||||
|
assertHandled(" hello");
|
||||||
|
assertHandled(" hello");
|
||||||
|
assertHandled("hello ");
|
||||||
|
assertHandled("hello ");
|
||||||
|
assertEquals(4, callCount.get());
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void testExecuteAfterUnregisterForwards() {
|
||||||
|
final var node = LiteralArgumentBuilder
|
||||||
|
.<CommandSource>literal("hello")
|
||||||
|
.executes(context -> fail())
|
||||||
|
.build();
|
||||||
|
manager.register(new BrigadierCommand(node));
|
||||||
|
manager.unregister("hello");
|
||||||
|
|
||||||
|
assertForwarded("hello");
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void testForwardsAndDoesNotExecuteImpermissibleAlias() {
|
||||||
|
final var callCount = new AtomicInteger();
|
||||||
|
|
||||||
|
final var node = LiteralArgumentBuilder
|
||||||
|
.<CommandSource>literal("hello")
|
||||||
|
.executes(context -> fail())
|
||||||
|
.requires(actualSource -> {
|
||||||
|
assertEquals(source, actualSource);
|
||||||
|
callCount.incrementAndGet();
|
||||||
|
return false;
|
||||||
|
})
|
||||||
|
.build();
|
||||||
|
manager.register(new BrigadierCommand(node));
|
||||||
|
|
||||||
|
assertForwarded("hello");
|
||||||
|
assertEquals(1, callCount.get());
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void testForwardsAndDoesNotExecuteContextImpermissibleAlias() {
|
||||||
|
final var callCount = new AtomicInteger();
|
||||||
|
|
||||||
|
final var node = LiteralArgumentBuilder
|
||||||
|
.<CommandSource>literal("hello")
|
||||||
|
.executes(context -> fail())
|
||||||
|
.requiresWithContext((context, reader) -> {
|
||||||
|
assertEquals(source, context.getSource());
|
||||||
|
assertEquals("hello", reader.getRead());
|
||||||
|
assertEquals(1, context.getNodes().size());
|
||||||
|
callCount.incrementAndGet();
|
||||||
|
return false;
|
||||||
|
})
|
||||||
|
.build();
|
||||||
|
manager.register(new BrigadierCommand(node));
|
||||||
|
|
||||||
|
assertForwarded("hello");
|
||||||
|
assertEquals(1, callCount.get());
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void testExecutesNonAliasLevelNode() {
|
||||||
|
final var callCount = new AtomicInteger();
|
||||||
|
|
||||||
|
final var node = LiteralArgumentBuilder
|
||||||
|
.<CommandSource>literal("buy")
|
||||||
|
.executes(context -> fail())
|
||||||
|
.then(RequiredArgumentBuilder
|
||||||
|
.<CommandSource, Integer>argument("quantity", integer())
|
||||||
|
.executes(context -> {
|
||||||
|
assertEquals("buy 12", context.getInput());
|
||||||
|
assertEquals(12, getInteger(context, "quantity"));
|
||||||
|
assertEquals(2, context.getNodes().size());
|
||||||
|
callCount.incrementAndGet();
|
||||||
|
return 1;
|
||||||
|
}))
|
||||||
|
.build();
|
||||||
|
manager.register(new BrigadierCommand(node));
|
||||||
|
|
||||||
|
assertHandled("buy 12");
|
||||||
|
assertEquals(1, callCount.get());
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void testHandlesAndDoesNotExecuteWithImpermissibleNonAliasLevelNode() {
|
||||||
|
final var callCount = new AtomicInteger();
|
||||||
|
|
||||||
|
final var node = LiteralArgumentBuilder
|
||||||
|
.<CommandSource>literal("hello")
|
||||||
|
.executes(context -> fail())
|
||||||
|
.then(LiteralArgumentBuilder
|
||||||
|
.<CommandSource>literal("world")
|
||||||
|
.executes(context -> fail())
|
||||||
|
.requires(source -> {
|
||||||
|
callCount.incrementAndGet();
|
||||||
|
return false;
|
||||||
|
}))
|
||||||
|
.build();
|
||||||
|
manager.register(new BrigadierCommand(node));
|
||||||
|
|
||||||
|
assertHandled("hello world");
|
||||||
|
assertEquals(1, callCount.get());
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void testExecuteAsyncCompletesExceptionallyOnCallbackException() {
|
||||||
|
final var expected = new RuntimeException();
|
||||||
|
final var node = LiteralArgumentBuilder
|
||||||
|
.<CommandSource>literal("hello")
|
||||||
|
.executes(context -> {
|
||||||
|
throw expected;
|
||||||
|
})
|
||||||
|
.build();
|
||||||
|
manager.register(new BrigadierCommand(node));
|
||||||
|
|
||||||
|
final Exception wrapper = assertThrows(CompletionException.class, () ->
|
||||||
|
manager.executeAsync(source, "hello").join());
|
||||||
|
|
||||||
|
assertSame(expected, wrapper.getCause().getCause());
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void testExecuteAsyncCompletesExceptionallyOnRequirementException() {
|
||||||
|
final var expected = new RuntimeException();
|
||||||
|
final var node = LiteralArgumentBuilder
|
||||||
|
.<CommandSource>literal("hello")
|
||||||
|
.requires(source1 -> {
|
||||||
|
throw expected;
|
||||||
|
})
|
||||||
|
.executes(context -> fail()) // needed for dispatcher to consider the node
|
||||||
|
.build();
|
||||||
|
manager.register(new BrigadierCommand(node));
|
||||||
|
|
||||||
|
final Exception wrapper = assertThrows(CompletionException.class, () ->
|
||||||
|
manager.executeAsync(source, "hello").join());
|
||||||
|
|
||||||
|
assertSame(expected, wrapper.getCause().getCause());
|
||||||
|
}
|
||||||
|
|
||||||
|
// Suggestions
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void testDoesNotSuggestAliasAfterUnregister() {
|
||||||
|
final var node = LiteralArgumentBuilder
|
||||||
|
.<CommandSource>literal("hello")
|
||||||
|
.build();
|
||||||
|
manager.register(new BrigadierCommand(node));
|
||||||
|
manager.unregister("hello");
|
||||||
|
|
||||||
|
assertSuggestions("");
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void testArgumentSuggestions() {
|
||||||
|
final var node = LiteralArgumentBuilder
|
||||||
|
.<CommandSource>literal("hello")
|
||||||
|
.then(RequiredArgumentBuilder
|
||||||
|
.<CommandSource, String>argument("argument", word())
|
||||||
|
.suggests((context, builder) -> builder
|
||||||
|
.suggest("foo")
|
||||||
|
.suggest("bar")
|
||||||
|
.suggest("baz")
|
||||||
|
.buildFuture()))
|
||||||
|
.build();
|
||||||
|
manager.register(new BrigadierCommand(node));
|
||||||
|
|
||||||
|
assertSuggestions("hello ", "bar", "baz", "foo");
|
||||||
|
assertSuggestions("hello ba", "bar", "baz", "foo");
|
||||||
|
assertSuggestions("hello bar", "baz", "foo");
|
||||||
|
}
|
||||||
|
|
||||||
|
// The following 2 tests ensure we strictly follow Brigadier's behavior, even
|
||||||
|
// if it doesn't make much sense.
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void testSuggestsEvenIfImpermissible() {
|
||||||
|
final var node = LiteralArgumentBuilder
|
||||||
|
.<CommandSource>literal("parent")
|
||||||
|
.then(LiteralArgumentBuilder
|
||||||
|
.<CommandSource>literal("child")
|
||||||
|
.requiresWithContext((context, reader) -> fail()))
|
||||||
|
.build();
|
||||||
|
manager.register(new BrigadierCommand(node));
|
||||||
|
|
||||||
|
assertSuggestions("parent ", "child");
|
||||||
|
assertSuggestions("parent chi", "child");
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void testDoesNotSuggestIfImpermissibleDuringParse() {
|
||||||
|
final var callCount = new AtomicInteger();
|
||||||
|
|
||||||
|
final var node = LiteralArgumentBuilder
|
||||||
|
.<CommandSource>literal("parent")
|
||||||
|
.then(LiteralArgumentBuilder
|
||||||
|
.<CommandSource>literal("child")
|
||||||
|
.requiresWithContext((context, reader) -> {
|
||||||
|
// CommandDispatcher#parseNodes checks whether the child node can be added
|
||||||
|
// to the context object. CommandDispatcher#getCompletionSuggestions then
|
||||||
|
// considers a suggestion context with "parent" as the parent, and considers
|
||||||
|
// the suggestions of relevant children, which includes "child".
|
||||||
|
assertEquals(2, context.getNodes().size());
|
||||||
|
callCount.incrementAndGet();
|
||||||
|
return false;
|
||||||
|
}))
|
||||||
|
.build();
|
||||||
|
manager.register(new BrigadierCommand(node));
|
||||||
|
|
||||||
|
assertSuggestions("parent child");
|
||||||
|
assertEquals(1, callCount.get());
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void testDoesNotSuggestIfCustomSuggestionProviderFutureCompletesExceptionally() {
|
||||||
|
final var node = LiteralArgumentBuilder
|
||||||
|
.<CommandSource>literal("parent")
|
||||||
|
.then(RequiredArgumentBuilder
|
||||||
|
.<CommandSource, String>argument("child", word())
|
||||||
|
.suggests((context, builder) ->
|
||||||
|
CompletableFuture.failedFuture(new RuntimeException())))
|
||||||
|
.build();
|
||||||
|
manager.register(new BrigadierCommand(node));
|
||||||
|
|
||||||
|
assertSuggestions("parent ");
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void testDoesNotSuggestIfCustomSuggestionProviderThrows() {
|
||||||
|
final var node = LiteralArgumentBuilder
|
||||||
|
.<CommandSource>literal("parent")
|
||||||
|
.then(RequiredArgumentBuilder
|
||||||
|
.<CommandSource, String>argument("child", word())
|
||||||
|
.suggests((context, builder) -> {
|
||||||
|
throw new RuntimeException();
|
||||||
|
}))
|
||||||
|
.build();
|
||||||
|
manager.register(new BrigadierCommand(node));
|
||||||
|
|
||||||
|
assertSuggestions("parent ");
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void testSuggestCompletesExceptionallyIfRequirementPredicateThrows() {
|
||||||
|
final var node = LiteralArgumentBuilder
|
||||||
|
.<CommandSource>literal("parent")
|
||||||
|
.requires(source1 -> {
|
||||||
|
throw new RuntimeException();
|
||||||
|
})
|
||||||
|
.then(RequiredArgumentBuilder
|
||||||
|
.<CommandSource, String>argument("child", word())
|
||||||
|
.suggests((context, builder) -> fail()))
|
||||||
|
.build();
|
||||||
|
manager.register(new BrigadierCommand(node));
|
||||||
|
|
||||||
|
assertThrows(CompletionException.class, () ->
|
||||||
|
manager.offerSuggestions(source, "parent ").join());
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,232 @@
|
|||||||
|
/*
|
||||||
|
* 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 <https://www.gnu.org/licenses/>.
|
||||||
|
*/
|
||||||
|
|
||||||
|
package com.velocitypowered.proxy.command;
|
||||||
|
|
||||||
|
import static com.mojang.brigadier.arguments.IntegerArgumentType.integer;
|
||||||
|
import static com.mojang.brigadier.builder.LiteralArgumentBuilder.literal;
|
||||||
|
import static com.mojang.brigadier.builder.RequiredArgumentBuilder.argument;
|
||||||
|
import static org.junit.jupiter.api.Assertions.assertArrayEquals;
|
||||||
|
import static org.junit.jupiter.api.Assertions.assertEquals;
|
||||||
|
import static org.junit.jupiter.api.Assertions.assertTrue;
|
||||||
|
import static org.junit.jupiter.api.Assertions.fail;
|
||||||
|
|
||||||
|
import com.mojang.brigadier.builder.LiteralArgumentBuilder;
|
||||||
|
import com.mojang.brigadier.tree.LiteralCommandNode;
|
||||||
|
import com.mojang.brigadier.tree.RootCommandNode;
|
||||||
|
import com.velocitypowered.api.command.BrigadierCommand;
|
||||||
|
import com.velocitypowered.api.command.CommandSource;
|
||||||
|
import com.velocitypowered.api.command.RawCommand;
|
||||||
|
import com.velocitypowered.api.command.SimpleCommand;
|
||||||
|
import java.util.concurrent.atomic.AtomicInteger;
|
||||||
|
import org.junit.jupiter.api.BeforeEach;
|
||||||
|
import org.junit.jupiter.api.Test;
|
||||||
|
|
||||||
|
public class CommandGraphInjectorTests extends CommandTestSuite {
|
||||||
|
|
||||||
|
private RootCommandNode<CommandSource> dest;
|
||||||
|
|
||||||
|
@BeforeEach
|
||||||
|
void setUp() {
|
||||||
|
super.setUp();
|
||||||
|
this.dest = new RootCommandNode<>();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void testInjectInvocableCommand() {
|
||||||
|
final var meta = manager.metaBuilder("hello").build();
|
||||||
|
manager.register(meta, (SimpleCommand) invocation -> fail());
|
||||||
|
manager.getInjector().inject(dest, source);
|
||||||
|
|
||||||
|
// Preserves alias and arguments node
|
||||||
|
final var expected = manager.getRoot();
|
||||||
|
assertEquals(expected, dest);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void testFiltersImpermissibleAlias() {
|
||||||
|
final var callCount = new AtomicInteger();
|
||||||
|
|
||||||
|
final var meta = manager.metaBuilder("hello").build();
|
||||||
|
manager.register(meta, new SimpleCommand() {
|
||||||
|
@Override
|
||||||
|
public void execute(final Invocation invocation) {
|
||||||
|
fail();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public boolean hasPermission(final Invocation invocation) {
|
||||||
|
assertEquals(source, invocation.source());
|
||||||
|
assertEquals("hello", invocation.alias());
|
||||||
|
assertArrayEquals(new String[0], invocation.arguments());
|
||||||
|
callCount.incrementAndGet();
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
manager.getInjector().inject(dest, source);
|
||||||
|
|
||||||
|
assertTrue(dest.getChildren().isEmpty());
|
||||||
|
assertEquals(1, callCount.get());
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void testInjectsHintsOfInvocableCommand() {
|
||||||
|
final var hint = LiteralArgumentBuilder
|
||||||
|
.<CommandSource>literal("hint")
|
||||||
|
.build();
|
||||||
|
final var meta = manager.metaBuilder("hello")
|
||||||
|
.hint(hint)
|
||||||
|
.build();
|
||||||
|
manager.register(meta, (SimpleCommand) invocation -> fail());
|
||||||
|
manager.getInjector().inject(dest, source);
|
||||||
|
|
||||||
|
// Preserves hint node
|
||||||
|
final var expected = manager.getRoot();
|
||||||
|
assertEquals(expected, dest);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void testFiltersHintsOfImpermissibleAlias() {
|
||||||
|
final var callCount = new AtomicInteger();
|
||||||
|
|
||||||
|
final var hint = LiteralArgumentBuilder
|
||||||
|
.<CommandSource>literal("hint")
|
||||||
|
.build();
|
||||||
|
final var meta = manager.metaBuilder("hello")
|
||||||
|
.hint(hint)
|
||||||
|
.build();
|
||||||
|
manager.register(meta, new RawCommand() {
|
||||||
|
@Override
|
||||||
|
public void execute(final Invocation invocation) {
|
||||||
|
fail();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public boolean hasPermission(final Invocation invocation) {
|
||||||
|
callCount.incrementAndGet();
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
manager.getInjector().inject(dest, source);
|
||||||
|
|
||||||
|
assertTrue(dest.getChildren().isEmpty());
|
||||||
|
assertEquals(1, callCount.get()); // does not call hasPermission for hints
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void testInjectsBrigadierCommand() {
|
||||||
|
final LiteralCommandNode<CommandSource> node = LiteralArgumentBuilder
|
||||||
|
.<CommandSource>literal("hello")
|
||||||
|
.then(literal("world"))
|
||||||
|
.then(argument("count", integer()))
|
||||||
|
.build();
|
||||||
|
manager.register(new BrigadierCommand(node));
|
||||||
|
manager.getInjector().inject(dest, source);
|
||||||
|
|
||||||
|
assertEquals(node, dest.getChild("hello"));
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void testFiltersImpermissibleBrigadierCommandChildren() {
|
||||||
|
final var callCount = new AtomicInteger();
|
||||||
|
|
||||||
|
final var registered = LiteralArgumentBuilder
|
||||||
|
.<CommandSource>literal("greet")
|
||||||
|
.then(LiteralArgumentBuilder
|
||||||
|
.<CommandSource>literal("somebody")
|
||||||
|
.requires(source -> {
|
||||||
|
callCount.incrementAndGet();
|
||||||
|
return false;
|
||||||
|
}))
|
||||||
|
.build();
|
||||||
|
manager.register(new BrigadierCommand(registered));
|
||||||
|
manager.getInjector().inject(dest, source);
|
||||||
|
|
||||||
|
final var expected = LiteralArgumentBuilder
|
||||||
|
.literal("greet")
|
||||||
|
.build();
|
||||||
|
assertEquals(expected, dest.getChild("greet"));
|
||||||
|
assertEquals(1, callCount.get());
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void testInjectPreservesBrigadierCommandAliasRedirect() {
|
||||||
|
final var registered = LiteralArgumentBuilder
|
||||||
|
.<CommandSource>literal("origin")
|
||||||
|
.redirect(LiteralArgumentBuilder
|
||||||
|
.<CommandSource>literal("target")
|
||||||
|
.build())
|
||||||
|
.build();
|
||||||
|
manager.register(new BrigadierCommand(registered));
|
||||||
|
manager.getInjector().inject(dest, source);
|
||||||
|
|
||||||
|
final var expected = LiteralArgumentBuilder
|
||||||
|
.<CommandSource>literal("origin")
|
||||||
|
.redirect(LiteralArgumentBuilder
|
||||||
|
.<CommandSource>literal("target")
|
||||||
|
.build())
|
||||||
|
.build();
|
||||||
|
assertEquals(expected, dest.getChild("origin"));
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void testFiltersImpermissibleBrigadierCommandRedirects() {
|
||||||
|
final var callCount = new AtomicInteger();
|
||||||
|
|
||||||
|
final var registered = LiteralArgumentBuilder
|
||||||
|
.<CommandSource>literal("hello")
|
||||||
|
.then(LiteralArgumentBuilder
|
||||||
|
.<CommandSource>literal("origin")
|
||||||
|
.redirect(LiteralArgumentBuilder
|
||||||
|
.<CommandSource>literal("target")
|
||||||
|
.requires(source -> {
|
||||||
|
callCount.incrementAndGet();
|
||||||
|
return false;
|
||||||
|
})
|
||||||
|
.build()
|
||||||
|
)
|
||||||
|
)
|
||||||
|
.build();
|
||||||
|
manager.register(new BrigadierCommand(registered));
|
||||||
|
manager.getInjector().inject(dest, source);
|
||||||
|
|
||||||
|
final var expected = LiteralArgumentBuilder
|
||||||
|
.<CommandSource>literal("hello")
|
||||||
|
.then(literal("origin"))
|
||||||
|
.build();
|
||||||
|
assertEquals(expected, dest.getChild("hello"));
|
||||||
|
assertEquals(1, callCount.get());
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void testInjectOverridesAliasInDestination() {
|
||||||
|
final var registered = LiteralArgumentBuilder
|
||||||
|
.<CommandSource>literal("foo")
|
||||||
|
.then(literal("bar"))
|
||||||
|
.build();
|
||||||
|
manager.register(new BrigadierCommand(registered));
|
||||||
|
|
||||||
|
final var original = LiteralArgumentBuilder
|
||||||
|
.<CommandSource>literal("foo")
|
||||||
|
.then(literal("baz"))
|
||||||
|
.build();
|
||||||
|
dest.addChild(original);
|
||||||
|
manager.getInjector().inject(dest, source);
|
||||||
|
|
||||||
|
assertEquals(registered, dest.getChild("foo"));
|
||||||
|
}
|
||||||
|
}
|
@ -17,490 +17,170 @@
|
|||||||
|
|
||||||
package com.velocitypowered.proxy.command;
|
package com.velocitypowered.proxy.command;
|
||||||
|
|
||||||
import static org.junit.jupiter.api.Assertions.assertArrayEquals;
|
|
||||||
import static org.junit.jupiter.api.Assertions.assertEquals;
|
|
||||||
import static org.junit.jupiter.api.Assertions.assertFalse;
|
import static org.junit.jupiter.api.Assertions.assertFalse;
|
||||||
|
import static org.junit.jupiter.api.Assertions.assertThrows;
|
||||||
import static org.junit.jupiter.api.Assertions.assertTrue;
|
import static org.junit.jupiter.api.Assertions.assertTrue;
|
||||||
import static org.junit.jupiter.api.Assertions.fail;
|
import static org.junit.jupiter.api.Assertions.fail;
|
||||||
|
|
||||||
import com.google.common.collect.ImmutableList;
|
|
||||||
import com.google.common.collect.ImmutableSet;
|
|
||||||
import com.mojang.brigadier.arguments.IntegerArgumentType;
|
|
||||||
import com.mojang.brigadier.arguments.StringArgumentType;
|
|
||||||
import com.mojang.brigadier.builder.LiteralArgumentBuilder;
|
import com.mojang.brigadier.builder.LiteralArgumentBuilder;
|
||||||
import com.mojang.brigadier.builder.RequiredArgumentBuilder;
|
|
||||||
import com.mojang.brigadier.tree.ArgumentCommandNode;
|
|
||||||
import com.mojang.brigadier.tree.CommandNode;
|
|
||||||
import com.mojang.brigadier.tree.LiteralCommandNode;
|
|
||||||
import com.velocitypowered.api.command.BrigadierCommand;
|
import com.velocitypowered.api.command.BrigadierCommand;
|
||||||
import com.velocitypowered.api.command.CommandMeta;
|
|
||||||
import com.velocitypowered.api.command.CommandSource;
|
import com.velocitypowered.api.command.CommandSource;
|
||||||
import com.velocitypowered.api.command.RawCommand;
|
import com.velocitypowered.api.command.RawCommand;
|
||||||
import com.velocitypowered.api.command.SimpleCommand;
|
import com.velocitypowered.api.command.SimpleCommand;
|
||||||
import com.velocitypowered.proxy.event.VelocityEventManager;
|
|
||||||
import com.velocitypowered.proxy.plugin.MockEventManager;
|
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
import java.util.concurrent.ExecutionException;
|
|
||||||
import java.util.concurrent.atomic.AtomicBoolean;
|
import java.util.concurrent.atomic.AtomicBoolean;
|
||||||
import java.util.concurrent.atomic.AtomicReference;
|
|
||||||
import org.junit.jupiter.api.Disabled;
|
|
||||||
import org.junit.jupiter.api.Test;
|
import org.junit.jupiter.api.Test;
|
||||||
|
|
||||||
public class CommandManagerTests {
|
public class CommandManagerTests extends CommandTestSuite {
|
||||||
|
|
||||||
private static final VelocityEventManager EVENT_MANAGER = new MockEventManager();
|
// Registration
|
||||||
|
|
||||||
static {
|
@Test
|
||||||
Runtime.getRuntime().addShutdownHook(new Thread(() -> {
|
void testRegisterWithMeta() {
|
||||||
try {
|
final var meta = manager.metaBuilder("hello").build();
|
||||||
EVENT_MANAGER.shutdown();
|
manager.register(meta, DummyCommand.INSTANCE);
|
||||||
} catch (InterruptedException e) {
|
|
||||||
Thread.currentThread().interrupt();
|
|
||||||
}
|
|
||||||
}));
|
|
||||||
}
|
|
||||||
|
|
||||||
static VelocityCommandManager createManager() {
|
assertTrue(manager.hasCommand("hello"));
|
||||||
return new VelocityCommandManager(EVENT_MANAGER);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
void testConstruction() {
|
void testRegisterWithMetaContainingMultipleAliases() {
|
||||||
VelocityCommandManager manager = createManager();
|
final var meta = manager.metaBuilder("foo")
|
||||||
assertFalse(manager.hasCommand("foo"));
|
.aliases("bar")
|
||||||
assertTrue(manager.getDispatcher().getRoot().getChildren().isEmpty());
|
.aliases("baz", "qux")
|
||||||
assertFalse(manager.executeAsync(MockCommandSource.INSTANCE, "foo").join());
|
.build();
|
||||||
assertFalse(manager.executeImmediatelyAsync(MockCommandSource.INSTANCE, "bar").join());
|
manager.register(meta, DummyCommand.INSTANCE);
|
||||||
assertTrue(manager.offerSuggestions(MockCommandSource.INSTANCE, "").join().isEmpty());
|
|
||||||
}
|
|
||||||
|
|
||||||
@Test
|
assertTrue(manager.hasCommand("foo"));
|
||||||
void testBrigadierRegister() {
|
|
||||||
VelocityCommandManager manager = createManager();
|
|
||||||
LiteralCommandNode<CommandSource> node = LiteralArgumentBuilder
|
|
||||||
.<CommandSource>literal("foo")
|
|
||||||
.build();
|
|
||||||
BrigadierCommand command = new BrigadierCommand(node);
|
|
||||||
manager.register(command);
|
|
||||||
|
|
||||||
assertEquals(node, command.getNode());
|
|
||||||
assertTrue(manager.hasCommand("fOo"));
|
|
||||||
|
|
||||||
LiteralCommandNode<CommandSource> barNode = LiteralArgumentBuilder
|
|
||||||
.<CommandSource>literal("bar")
|
|
||||||
.build();
|
|
||||||
BrigadierCommand aliasesCommand = new BrigadierCommand(barNode);
|
|
||||||
CommandMeta meta = manager.metaBuilder(aliasesCommand)
|
|
||||||
.aliases("baZ")
|
|
||||||
.build();
|
|
||||||
|
|
||||||
assertEquals(ImmutableSet.of("bar", "baz"), meta.getAliases());
|
|
||||||
assertTrue(meta.getHints().isEmpty());
|
|
||||||
manager.register(meta, aliasesCommand);
|
|
||||||
assertTrue(manager.hasCommand("bAr"));
|
|
||||||
assertTrue(manager.hasCommand("Baz"));
|
|
||||||
}
|
|
||||||
|
|
||||||
@Test
|
|
||||||
void testSimpleRegister() {
|
|
||||||
VelocityCommandManager manager = createManager();
|
|
||||||
SimpleCommand command = new NoopSimpleCommand();
|
|
||||||
|
|
||||||
manager.register("Foo", command);
|
|
||||||
assertTrue(manager.hasCommand("foO"));
|
|
||||||
manager.unregister("fOo");
|
|
||||||
assertFalse(manager.hasCommand("foo"));
|
|
||||||
assertFalse(manager.executeAsync(MockCommandSource.INSTANCE, "foo").join());
|
|
||||||
|
|
||||||
manager.register("foo", command, "bAr", "BAZ");
|
|
||||||
assertTrue(manager.hasCommand("bar"));
|
assertTrue(manager.hasCommand("bar"));
|
||||||
assertTrue(manager.hasCommand("bAz"));
|
assertTrue(manager.hasCommand("baz"));
|
||||||
|
assertTrue(manager.hasCommand("qux"));
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
void testRawRegister() {
|
void testRegisterAliasesAreCaseInsensitive() {
|
||||||
VelocityCommandManager manager = createManager();
|
final var meta = manager.metaBuilder("Foo")
|
||||||
RawCommand command = new NoopRawCommand();
|
.aliases("Bar")
|
||||||
|
.build();
|
||||||
|
manager.register(meta, DummyCommand.INSTANCE);
|
||||||
|
|
||||||
manager.register("foO", command, "BAR");
|
assertTrue(manager.hasCommand("foo"));
|
||||||
assertTrue(manager.hasCommand("fOo"));
|
|
||||||
assertTrue(manager.hasCommand("bar"));
|
assertTrue(manager.hasCommand("bar"));
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
void testBrigadierExecute() {
|
void testRegisterBrigadierCommand() {
|
||||||
VelocityCommandManager manager = createManager();
|
final var node = LiteralArgumentBuilder
|
||||||
AtomicBoolean executed = new AtomicBoolean(false);
|
.<CommandSource>literal("hello")
|
||||||
AtomicBoolean checkedRequires = new AtomicBoolean(false);
|
.build();
|
||||||
LiteralCommandNode<CommandSource> node = LiteralArgumentBuilder
|
|
||||||
.<CommandSource>literal("buy")
|
|
||||||
.executes(context -> {
|
|
||||||
assertEquals(MockCommandSource.INSTANCE, context.getSource());
|
|
||||||
assertEquals("buy", context.getInput());
|
|
||||||
executed.set(true);
|
|
||||||
return 1;
|
|
||||||
})
|
|
||||||
.build();
|
|
||||||
CommandNode<CommandSource> quantityNode = RequiredArgumentBuilder
|
|
||||||
.<CommandSource, Integer>argument("quantity", IntegerArgumentType.integer(12, 16))
|
|
||||||
.requires(source -> {
|
|
||||||
assertEquals(MockCommandSource.INSTANCE, source);
|
|
||||||
checkedRequires.set(true);
|
|
||||||
return true;
|
|
||||||
})
|
|
||||||
.executes(context -> {
|
|
||||||
int argument = IntegerArgumentType.getInteger(context, "quantity");
|
|
||||||
assertEquals(14, argument);
|
|
||||||
executed.set(true);
|
|
||||||
return 1;
|
|
||||||
})
|
|
||||||
.build();
|
|
||||||
CommandNode<CommandSource> productNode = RequiredArgumentBuilder
|
|
||||||
.<CommandSource, String>argument("product", StringArgumentType.string())
|
|
||||||
.requires(source -> {
|
|
||||||
checkedRequires.set(true);
|
|
||||||
return false;
|
|
||||||
})
|
|
||||||
.executes(context -> fail("was executed"))
|
|
||||||
.build();
|
|
||||||
quantityNode.addChild(productNode);
|
|
||||||
node.addChild(quantityNode);
|
|
||||||
manager.register(new BrigadierCommand(node));
|
manager.register(new BrigadierCommand(node));
|
||||||
|
|
||||||
assertTrue(manager.executeAsync(MockCommandSource.INSTANCE, "buy ").join());
|
assertTrue(manager.hasCommand("hello"));
|
||||||
assertTrue(executed.compareAndSet(true, false), "was executed");
|
|
||||||
assertTrue(manager.executeImmediatelyAsync(MockCommandSource.INSTANCE, "buy 14").join());
|
|
||||||
assertTrue(checkedRequires.compareAndSet(true, false));
|
|
||||||
assertTrue(executed.get());
|
|
||||||
assertTrue(manager.executeAsync(MockCommandSource.INSTANCE, "buy 9").join(),
|
|
||||||
"Invalid arg returns false");
|
|
||||||
assertTrue(manager.executeImmediatelyAsync(MockCommandSource.INSTANCE, "buy 12 bananas")
|
|
||||||
.join());
|
|
||||||
assertTrue(checkedRequires.get());
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
void testSimpleExecute() {
|
void testRegisterOverridesPreviousCommand() {
|
||||||
VelocityCommandManager manager = createManager();
|
final var called = new AtomicBoolean();
|
||||||
AtomicBoolean executed = new AtomicBoolean(false);
|
|
||||||
SimpleCommand command = invocation -> {
|
|
||||||
assertEquals(MockCommandSource.INSTANCE, invocation.source());
|
|
||||||
assertArrayEquals(new String[] {"bar", "254"}, invocation.arguments());
|
|
||||||
executed.set(true);
|
|
||||||
};
|
|
||||||
manager.register("foo", command);
|
|
||||||
|
|
||||||
assertTrue(manager.executeAsync(MockCommandSource.INSTANCE, "foo bar 254").join());
|
final var oldMeta = manager.metaBuilder("foo").build();
|
||||||
assertTrue(executed.get());
|
manager.register(oldMeta, DummyCommand.INSTANCE); // fails on execution
|
||||||
|
final var newMeta = manager.metaBuilder("foo").build();
|
||||||
|
manager.register(newMeta, (RawCommand) invocation -> called.set(true));
|
||||||
|
manager.executeAsync(MockCommandSource.INSTANCE, "foo").join();
|
||||||
|
|
||||||
SimpleCommand noPermsCommand = new SimpleCommand() {
|
assertTrue(called.get());
|
||||||
@Override
|
|
||||||
public void execute(final Invocation invocation) {
|
|
||||||
fail("was executed");
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public boolean hasPermission(final Invocation invocation) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
manager.register("dangerous", noPermsCommand, "veryDangerous");
|
|
||||||
assertFalse(manager.executeAsync(MockCommandSource.INSTANCE, "dangerous").join());
|
|
||||||
assertFalse(manager.executeImmediatelyAsync(MockCommandSource.INSTANCE, "verydangerous 123")
|
|
||||||
.join());
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
void testRawExecute() {
|
void testAddingExecutableHintToMetaThrows() {
|
||||||
VelocityCommandManager manager = createManager();
|
final var hintNode = LiteralArgumentBuilder
|
||||||
AtomicBoolean executed = new AtomicBoolean(false);
|
.<CommandSource>literal("hint")
|
||||||
RawCommand command = new RawCommand() {
|
.executes(context -> fail())
|
||||||
@Override
|
.build();
|
||||||
public void execute(final Invocation invocation) {
|
|
||||||
assertEquals(MockCommandSource.INSTANCE, invocation.source());
|
|
||||||
assertEquals("lobby 23", invocation.arguments());
|
|
||||||
executed.set(true);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
manager.register("sendMe", command);
|
|
||||||
|
|
||||||
assertTrue(manager.executeImmediatelyAsync(MockCommandSource.INSTANCE, "sendMe lobby 23")
|
assertThrows(IllegalArgumentException.class, () -> {
|
||||||
.join());
|
manager.metaBuilder("hello").hint(hintNode);
|
||||||
assertTrue(executed.compareAndSet(true, false));
|
});
|
||||||
|
|
||||||
RawCommand noArgsCommand = new RawCommand() {
|
|
||||||
@Override
|
|
||||||
public void execute(final Invocation invocation) {
|
|
||||||
assertEquals("", invocation.arguments());
|
|
||||||
executed.set(true);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
manager.register("noargs", noArgsCommand);
|
|
||||||
|
|
||||||
assertTrue(manager.executeImmediatelyAsync(MockCommandSource.INSTANCE, "noargs").join());
|
|
||||||
assertTrue(executed.get());
|
|
||||||
assertTrue(manager.executeImmediatelyAsync(MockCommandSource.INSTANCE, "noargs ").join());
|
|
||||||
|
|
||||||
RawCommand noPermsCommand = new RawCommand() {
|
|
||||||
@Override
|
|
||||||
public void execute(final Invocation invocation) {
|
|
||||||
fail("was executed");
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public boolean hasPermission(final Invocation invocation) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
manager.register("sendThem", noPermsCommand);
|
|
||||||
assertFalse(manager.executeImmediatelyAsync(MockCommandSource.INSTANCE, "sendThem foo")
|
|
||||||
.join());
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
void testSuggestions() {
|
void testAddingHintWithRedirectToMetaThrows() {
|
||||||
VelocityCommandManager manager = createManager();
|
final var targetNode = LiteralArgumentBuilder
|
||||||
|
.<CommandSource>literal("target")
|
||||||
|
.build();
|
||||||
|
final var hintNode = LiteralArgumentBuilder
|
||||||
|
.<CommandSource>literal("origin")
|
||||||
|
.redirect(targetNode)
|
||||||
|
.build();
|
||||||
|
|
||||||
LiteralCommandNode<CommandSource> brigadierNode = LiteralArgumentBuilder
|
assertThrows(IllegalArgumentException.class, () -> {
|
||||||
.<CommandSource>literal("brigadier")
|
manager.metaBuilder("hello").hint(hintNode);
|
||||||
.build();
|
});
|
||||||
CommandNode<CommandSource> nameNode = RequiredArgumentBuilder
|
}
|
||||||
.<CommandSource, String>argument("name", StringArgumentType.string())
|
|
||||||
.build();
|
|
||||||
CommandNode<CommandSource> numberNode = RequiredArgumentBuilder
|
|
||||||
.<CommandSource, Integer>argument("quantity", IntegerArgumentType.integer())
|
|
||||||
.suggests((context, builder) -> builder.suggest(2).suggest(3).buildFuture())
|
|
||||||
.build();
|
|
||||||
nameNode.addChild(numberNode);
|
|
||||||
brigadierNode.addChild(nameNode);
|
|
||||||
manager.register(new BrigadierCommand(brigadierNode));
|
|
||||||
|
|
||||||
SimpleCommand simpleCommand = new SimpleCommand() {
|
// Un-registration
|
||||||
@Override
|
|
||||||
public void execute(final Invocation invocation) {
|
|
||||||
fail();
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
@Test
|
||||||
public List<String> suggest(final Invocation invocation) {
|
void testUnregisterUnregisteredAliasIsIgnored() {
|
||||||
switch (invocation.arguments().length) {
|
manager.unregister("hello");
|
||||||
case 0:
|
|
||||||
return ImmutableList.of("foo", "bar");
|
|
||||||
case 1:
|
|
||||||
return ImmutableList.of("123");
|
|
||||||
default:
|
|
||||||
return ImmutableList.of();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
};
|
|
||||||
manager.register("simple", simpleCommand);
|
|
||||||
|
|
||||||
RawCommand rawCommand = new RawCommand() {
|
assertFalse(manager.hasCommand("hello"));
|
||||||
@Override
|
|
||||||
public void execute(final Invocation invocation) {
|
|
||||||
fail();
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public List<String> suggest(final Invocation invocation) {
|
|
||||||
switch (invocation.arguments()) {
|
|
||||||
case "":
|
|
||||||
return ImmutableList.of("foo", "baz");
|
|
||||||
case "foo ":
|
|
||||||
return ImmutableList.of("2", "3", "5", "7");
|
|
||||||
case "bar ":
|
|
||||||
return ImmutableList.of("11", "13", "17");
|
|
||||||
default:
|
|
||||||
return ImmutableList.of();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
};
|
|
||||||
manager.register("raw", rawCommand);
|
|
||||||
|
|
||||||
assertEquals(
|
|
||||||
ImmutableList.of("brigadier", "raw", "simple"),
|
|
||||||
manager.offerSuggestions(MockCommandSource.INSTANCE, "").join(),
|
|
||||||
"literals are in alphabetical order");
|
|
||||||
assertEquals(
|
|
||||||
ImmutableList.of("brigadier"),
|
|
||||||
manager.offerSuggestions(MockCommandSource.INSTANCE, "briga").join());
|
|
||||||
assertTrue(manager.offerSuggestions(MockCommandSource.INSTANCE, "brigadier")
|
|
||||||
.join().isEmpty());
|
|
||||||
assertTrue(manager.offerSuggestions(MockCommandSource.INSTANCE, "brigadier ")
|
|
||||||
.join().isEmpty());
|
|
||||||
assertTrue(manager.offerSuggestions(MockCommandSource.INSTANCE, "brigadier foo")
|
|
||||||
.join().isEmpty());
|
|
||||||
assertEquals(
|
|
||||||
ImmutableList.of("2", "3"),
|
|
||||||
manager.offerSuggestions(MockCommandSource.INSTANCE, "brigadier foo ").join());
|
|
||||||
assertEquals(
|
|
||||||
ImmutableList.of("bar", "foo"),
|
|
||||||
manager.offerSuggestions(MockCommandSource.INSTANCE, "simple ").join());
|
|
||||||
assertTrue(manager.offerSuggestions(MockCommandSource.INSTANCE, "simple")
|
|
||||||
.join().isEmpty());
|
|
||||||
assertEquals(
|
|
||||||
ImmutableList.of("123"),
|
|
||||||
manager.offerSuggestions(MockCommandSource.INSTANCE, "simPle foo").join());
|
|
||||||
assertEquals(
|
|
||||||
ImmutableList.of("baz", "foo"),
|
|
||||||
manager.offerSuggestions(MockCommandSource.INSTANCE, "raw ").join());
|
|
||||||
assertEquals(
|
|
||||||
ImmutableList.of("2", "3", "5", "7"),
|
|
||||||
manager.offerSuggestions(MockCommandSource.INSTANCE, "raw foo ").join());
|
|
||||||
assertTrue(manager.offerSuggestions(MockCommandSource.INSTANCE, "raw foo")
|
|
||||||
.join().isEmpty());
|
|
||||||
assertEquals(
|
|
||||||
ImmutableList.of("11", "13", "17"),
|
|
||||||
manager.offerSuggestions(MockCommandSource.INSTANCE, "rAW bar ").join());
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
void testBrigadierSuggestionPermissions() {
|
void testUnregisterRegisteredAlias() {
|
||||||
VelocityCommandManager manager = createManager();
|
manager.register("hello", DummyCommand.INSTANCE);
|
||||||
LiteralCommandNode<CommandSource> manageNode = LiteralArgumentBuilder
|
manager.unregister("hello");
|
||||||
.<CommandSource>literal("manage")
|
|
||||||
.requires(source -> false)
|
|
||||||
.build();
|
|
||||||
CommandNode<CommandSource> idNode = RequiredArgumentBuilder
|
|
||||||
.<CommandSource, Integer>argument("id", IntegerArgumentType.integer(0))
|
|
||||||
.suggests((context, builder) -> fail("called suggestion builder"))
|
|
||||||
.build();
|
|
||||||
manageNode.addChild(idNode);
|
|
||||||
manager.register(new BrigadierCommand(manageNode));
|
|
||||||
|
|
||||||
// Brigadier doesn't call the children predicate when requesting suggestions.
|
assertFalse(manager.hasCommand("hello"));
|
||||||
// However, it won't query children if the source doesn't pass the parent
|
|
||||||
// #requires predicate.
|
|
||||||
assertTrue(manager.offerSuggestions(MockCommandSource.INSTANCE, "manage ")
|
|
||||||
.join().isEmpty());
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
@Disabled
|
void testUnregisterSecondaryAlias() {
|
||||||
void testHinting() {
|
final var meta = manager.metaBuilder("foo")
|
||||||
VelocityCommandManager manager = createManager();
|
.aliases("bar")
|
||||||
AtomicBoolean executed = new AtomicBoolean(false);
|
.build();
|
||||||
AtomicBoolean calledSuggestionProvider = new AtomicBoolean(false);
|
manager.register(meta, DummyCommand.INSTANCE);
|
||||||
AtomicReference<String> expectedArgs = new AtomicReference<>();
|
manager.unregister("bar");
|
||||||
RawCommand command = new RawCommand() {
|
|
||||||
@Override
|
|
||||||
public void execute(final Invocation invocation) {
|
|
||||||
assertEquals(expectedArgs.get(), invocation.arguments());
|
|
||||||
executed.set(true);
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
assertFalse(manager.hasCommand("bar"));
|
||||||
public List<String> suggest(final Invocation invocation) {
|
assertTrue(manager.hasCommand("foo"));
|
||||||
return ImmutableList.of("raw");
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
CommandNode<CommandSource> barHint = LiteralArgumentBuilder
|
|
||||||
.<CommandSource>literal("bar")
|
|
||||||
.executes(context -> fail("hints don't get executed"))
|
|
||||||
.build();
|
|
||||||
ArgumentCommandNode<CommandSource, Integer> numberArg = RequiredArgumentBuilder
|
|
||||||
.<CommandSource, Integer>argument("number", IntegerArgumentType.integer())
|
|
||||||
.suggests((context, builder) -> {
|
|
||||||
calledSuggestionProvider.set(true);
|
|
||||||
return builder.suggest("456").buildFuture();
|
|
||||||
})
|
|
||||||
.build();
|
|
||||||
barHint.addChild(numberArg);
|
|
||||||
CommandNode<CommandSource> bazHint = LiteralArgumentBuilder
|
|
||||||
.<CommandSource>literal("baz")
|
|
||||||
.build();
|
|
||||||
CommandMeta meta = manager.metaBuilder("foo")
|
|
||||||
.aliases("foo2")
|
|
||||||
.hint(barHint)
|
|
||||||
.hint(bazHint)
|
|
||||||
.build();
|
|
||||||
manager.register(meta, command);
|
|
||||||
|
|
||||||
expectedArgs.set("notBarOrBaz");
|
|
||||||
assertTrue(manager.executeAsync(MockCommandSource.INSTANCE, "foo notBarOrBaz").join());
|
|
||||||
assertTrue(executed.compareAndSet(true, false));
|
|
||||||
expectedArgs.set("anotherArg 123");
|
|
||||||
assertTrue(manager.executeAsync(MockCommandSource.INSTANCE, "Foo2 anotherArg 123").join());
|
|
||||||
assertTrue(executed.compareAndSet(true, false));
|
|
||||||
expectedArgs.set("bar");
|
|
||||||
assertTrue(manager.executeAsync(MockCommandSource.INSTANCE, "foo bar").join());
|
|
||||||
assertTrue(executed.compareAndSet(true, false));
|
|
||||||
expectedArgs.set("bar 123");
|
|
||||||
assertTrue(manager.executeAsync(MockCommandSource.INSTANCE, "foo2 bar 123").join());
|
|
||||||
assertTrue(executed.compareAndSet(true, false));
|
|
||||||
|
|
||||||
assertEquals(ImmutableList.of("bar", "baz", "raw"),
|
|
||||||
manager.offerSuggestions(MockCommandSource.INSTANCE, "foo ").join());
|
|
||||||
assertFalse(calledSuggestionProvider.get());
|
|
||||||
assertEquals(ImmutableList.of("456"),
|
|
||||||
manager.offerSuggestions(MockCommandSource.INSTANCE, "foo bar ").join());
|
|
||||||
assertTrue(calledSuggestionProvider.compareAndSet(true, false));
|
|
||||||
assertEquals(ImmutableList.of(),
|
|
||||||
manager.offerSuggestions(MockCommandSource.INSTANCE, "foo2 baz ").join());
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Execution
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
void testSuggestionPermissions() throws ExecutionException, InterruptedException {
|
void testExecuteUnknownAliasIsForwarded() {
|
||||||
VelocityCommandManager manager = createManager();
|
assertForwarded("");
|
||||||
RawCommand rawCommand = new RawCommand() {
|
assertForwarded("hello");
|
||||||
@Override
|
|
||||||
public void execute(final Invocation invocation) {
|
|
||||||
fail("The Command should not be executed while testing suggestions");
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public boolean hasPermission(Invocation invocation) {
|
|
||||||
return invocation.arguments().length() > 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public List<String> suggest(final Invocation invocation) {
|
|
||||||
return ImmutableList.of("suggestion");
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
manager.register("foo", rawCommand);
|
|
||||||
|
|
||||||
assertTrue(manager.offerSuggestions(MockCommandSource.INSTANCE, "foo").get().isEmpty());
|
|
||||||
assertFalse(manager.offerSuggestions(MockCommandSource.INSTANCE, "foo bar").get().isEmpty());
|
|
||||||
|
|
||||||
SimpleCommand oldCommand = new SimpleCommand() {
|
|
||||||
@Override
|
|
||||||
public void execute(Invocation invocation) {
|
|
||||||
fail("The Command should not be executed while testing suggestions");
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public boolean hasPermission(Invocation invocation) {
|
|
||||||
return invocation.arguments().length > 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public List<String> suggest(Invocation invocation) {
|
|
||||||
return ImmutableList.of("suggestion");
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
manager.register("bar", oldCommand);
|
|
||||||
|
|
||||||
assertTrue(manager.offerSuggestions(MockCommandSource.INSTANCE, "bar").get().isEmpty());
|
|
||||||
assertFalse(manager.offerSuggestions(MockCommandSource.INSTANCE, "bar foo").get().isEmpty());
|
|
||||||
}
|
}
|
||||||
|
|
||||||
static class NoopSimpleCommand implements SimpleCommand {
|
// Suggestions
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void testEmptyManagerSuggestNoAliases() {
|
||||||
|
assertSuggestions("");
|
||||||
|
}
|
||||||
|
|
||||||
|
static final class DummyCommand implements SimpleCommand {
|
||||||
|
|
||||||
|
static final DummyCommand INSTANCE = new DummyCommand();
|
||||||
|
|
||||||
|
private DummyCommand() {}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void execute(final Invocation invocation) {
|
public void execute(final Invocation invocation) {
|
||||||
|
fail();
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
static class NoopRawCommand implements RawCommand {
|
|
||||||
@Override
|
@Override
|
||||||
public void execute(final Invocation invocation) {
|
public List<String> suggest(final Invocation invocation) {
|
||||||
|
return fail();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public boolean hasPermission(final Invocation invocation) {
|
||||||
|
return fail();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
@ -0,0 +1,71 @@
|
|||||||
|
/*
|
||||||
|
* 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 <https://www.gnu.org/licenses/>.
|
||||||
|
*/
|
||||||
|
|
||||||
|
package com.velocitypowered.proxy.command;
|
||||||
|
|
||||||
|
import static org.junit.jupiter.api.Assertions.assertEquals;
|
||||||
|
import static org.junit.jupiter.api.Assertions.assertFalse;
|
||||||
|
import static org.junit.jupiter.api.Assertions.assertTrue;
|
||||||
|
|
||||||
|
import com.velocitypowered.api.command.CommandSource;
|
||||||
|
import com.velocitypowered.proxy.event.MockEventManager;
|
||||||
|
import com.velocitypowered.proxy.event.VelocityEventManager;
|
||||||
|
import java.util.Arrays;
|
||||||
|
import org.junit.jupiter.api.AfterAll;
|
||||||
|
import org.junit.jupiter.api.BeforeAll;
|
||||||
|
import org.junit.jupiter.api.BeforeEach;
|
||||||
|
|
||||||
|
abstract class CommandTestSuite {
|
||||||
|
|
||||||
|
private static VelocityEventManager eventManager;
|
||||||
|
|
||||||
|
protected VelocityCommandManager manager;
|
||||||
|
protected final CommandSource source = MockCommandSource.INSTANCE;
|
||||||
|
|
||||||
|
@BeforeAll
|
||||||
|
static void beforeAll() {
|
||||||
|
eventManager = new MockEventManager();
|
||||||
|
}
|
||||||
|
|
||||||
|
@AfterAll
|
||||||
|
static void afterAll() {
|
||||||
|
try {
|
||||||
|
eventManager.shutdown();
|
||||||
|
eventManager = null;
|
||||||
|
} catch (final InterruptedException e) {
|
||||||
|
Thread.currentThread().interrupt();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@BeforeEach
|
||||||
|
void setUp() {
|
||||||
|
this.manager = new VelocityCommandManager(eventManager);
|
||||||
|
}
|
||||||
|
|
||||||
|
final void assertHandled(final String input) {
|
||||||
|
assertTrue(manager.executeAsync(source, input).join());
|
||||||
|
}
|
||||||
|
|
||||||
|
final void assertForwarded(final String input) {
|
||||||
|
assertFalse(manager.executeAsync(source, input).join());
|
||||||
|
}
|
||||||
|
|
||||||
|
final void assertSuggestions(final String input, final String... expectedSuggestions) {
|
||||||
|
final var actual = manager.offerSuggestions(source, input).join();
|
||||||
|
assertEquals(Arrays.asList(expectedSuggestions), actual);
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,482 @@
|
|||||||
|
/*
|
||||||
|
* 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 <https://www.gnu.org/licenses/>.
|
||||||
|
*/
|
||||||
|
|
||||||
|
package com.velocitypowered.proxy.command;
|
||||||
|
|
||||||
|
import static com.mojang.brigadier.arguments.StringArgumentType.word;
|
||||||
|
import static org.junit.jupiter.api.Assertions.assertEquals;
|
||||||
|
import static org.junit.jupiter.api.Assertions.assertThrows;
|
||||||
|
import static org.junit.jupiter.api.Assertions.fail;
|
||||||
|
|
||||||
|
import com.google.common.collect.ImmutableList;
|
||||||
|
import com.mojang.brigadier.builder.LiteralArgumentBuilder;
|
||||||
|
import com.mojang.brigadier.builder.RequiredArgumentBuilder;
|
||||||
|
import com.velocitypowered.api.command.CommandSource;
|
||||||
|
import com.velocitypowered.api.command.RawCommand;
|
||||||
|
import java.util.Collections;
|
||||||
|
import java.util.List;
|
||||||
|
import java.util.concurrent.CompletableFuture;
|
||||||
|
import java.util.concurrent.CompletionException;
|
||||||
|
import java.util.concurrent.atomic.AtomicInteger;
|
||||||
|
import org.junit.jupiter.api.Test;
|
||||||
|
|
||||||
|
public class RawCommandTests extends CommandTestSuite {
|
||||||
|
|
||||||
|
// Execution
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void testExecuteAlias() {
|
||||||
|
final var callCount = new AtomicInteger();
|
||||||
|
|
||||||
|
final var meta = manager.metaBuilder("hello").build();
|
||||||
|
manager.register(meta, (RawCommand) invocation -> {
|
||||||
|
assertEquals(source, invocation.source());
|
||||||
|
assertEquals("hello", invocation.alias());
|
||||||
|
assertEquals("", invocation.arguments());
|
||||||
|
callCount.incrementAndGet();
|
||||||
|
});
|
||||||
|
|
||||||
|
assertHandled("hello");
|
||||||
|
assertEquals(1, callCount.get());
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void testExecuteIgnoresAliasCase() {
|
||||||
|
final var callCount = new AtomicInteger();
|
||||||
|
|
||||||
|
final var meta = manager.metaBuilder("hello").build();
|
||||||
|
manager.register(meta, (RawCommand) invocation -> {
|
||||||
|
assertEquals("hello", invocation.alias());
|
||||||
|
callCount.incrementAndGet();
|
||||||
|
});
|
||||||
|
|
||||||
|
assertHandled("Hello");
|
||||||
|
assertEquals(1, callCount.get());
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void testExecuteInputIsTrimmed() {
|
||||||
|
final var callCount = new AtomicInteger();
|
||||||
|
|
||||||
|
final var meta = manager.metaBuilder("hello").build();
|
||||||
|
manager.register(meta, (RawCommand) invocation -> {
|
||||||
|
assertEquals("hello", invocation.alias());
|
||||||
|
assertEquals("", invocation.arguments());
|
||||||
|
callCount.incrementAndGet();
|
||||||
|
});
|
||||||
|
|
||||||
|
assertHandled(" hello");
|
||||||
|
assertHandled(" hello");
|
||||||
|
assertHandled("hello ");
|
||||||
|
assertHandled("hello ");
|
||||||
|
assertEquals(4, callCount.get());
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void testExecuteAfterUnregisterForwards() {
|
||||||
|
final var meta = manager.metaBuilder("hello").build();
|
||||||
|
manager.register(meta, (RawCommand) invocation -> fail());
|
||||||
|
manager.unregister("hello");
|
||||||
|
|
||||||
|
assertForwarded("hello");
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void testForwardsAndDoesNotExecuteImpermissibleAlias() {
|
||||||
|
final var callCount = new AtomicInteger();
|
||||||
|
|
||||||
|
final var meta = manager.metaBuilder("hello").build();
|
||||||
|
manager.register(meta, new RawCommand() {
|
||||||
|
@Override
|
||||||
|
public void execute(final Invocation invocation) {
|
||||||
|
fail();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public boolean hasPermission(final Invocation invocation) {
|
||||||
|
assertEquals(source, invocation.source());
|
||||||
|
assertEquals("hello", invocation.alias());
|
||||||
|
assertEquals("", invocation.arguments());
|
||||||
|
callCount.incrementAndGet();
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
assertForwarded("hello");
|
||||||
|
assertEquals(1, callCount.get());
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void testExecutesWithArguments() {
|
||||||
|
final var callCount = new AtomicInteger();
|
||||||
|
|
||||||
|
final var meta = manager.metaBuilder("hello").build();
|
||||||
|
manager.register(meta, (RawCommand) invocation -> {
|
||||||
|
assertEquals("hello", invocation.alias());
|
||||||
|
assertEquals("dear world", invocation.arguments());
|
||||||
|
callCount.incrementAndGet();
|
||||||
|
});
|
||||||
|
|
||||||
|
assertHandled("hello dear world");
|
||||||
|
assertEquals(1, callCount.get());
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void testHandlesAndDoesNotExecuteWithImpermissibleArgs() {
|
||||||
|
final var callCount = new AtomicInteger();
|
||||||
|
|
||||||
|
final var meta = manager.metaBuilder("color").build();
|
||||||
|
manager.register(meta, new RawCommand() {
|
||||||
|
@Override
|
||||||
|
public void execute(final Invocation invocation) {
|
||||||
|
fail();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public boolean hasPermission(final Invocation invocation) {
|
||||||
|
assertEquals("color", invocation.alias());
|
||||||
|
assertEquals("red", invocation.arguments());
|
||||||
|
callCount.incrementAndGet();
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
assertHandled("color red");
|
||||||
|
assertEquals(1, callCount.get());
|
||||||
|
}
|
||||||
|
|
||||||
|
// Suggestions
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void testDoesNotSuggestAliasIfImpermissible() {
|
||||||
|
final var meta = manager.metaBuilder("hello").build();
|
||||||
|
manager.register(meta, new RawCommand() {
|
||||||
|
@Override
|
||||||
|
public void execute(final Invocation invocation) {
|
||||||
|
fail();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public boolean hasPermission(final Invocation invocation) {
|
||||||
|
assertEquals("hello", invocation.alias());
|
||||||
|
assertEquals("", invocation.arguments());
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public List<String> suggest(final Invocation invocation) {
|
||||||
|
return fail();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void testDoesNotSuggestAliasAfterUnregister() {
|
||||||
|
final var meta = manager.metaBuilder("hello").build();
|
||||||
|
manager.register(meta, new RawCommand() {
|
||||||
|
@Override
|
||||||
|
public void execute(final Invocation invocation) {
|
||||||
|
fail();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public List<String> suggest(final Invocation invocation) {
|
||||||
|
return fail();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
manager.unregister("hello");
|
||||||
|
|
||||||
|
assertSuggestions("");
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void testSuggestsArgumentsAfterAlias() {
|
||||||
|
final var meta = manager.metaBuilder("hello").build();
|
||||||
|
manager.register(meta, new RawCommand() {
|
||||||
|
@Override
|
||||||
|
public void execute(final Invocation invocation) {
|
||||||
|
fail();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public List<String> suggest(final Invocation invocation) {
|
||||||
|
assertEquals("hello", invocation.alias());
|
||||||
|
assertEquals("", invocation.arguments());
|
||||||
|
return ImmutableList.of("world", "people"); // ensures we don't mutate the user's list
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
assertSuggestions("hello ", "people", "world"); // in alphabetical order
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void testSuggestsArgumentsAfterAliasIgnoresAliasCase() {
|
||||||
|
final var meta = manager.metaBuilder("hello").build();
|
||||||
|
manager.register(meta, new RawCommand() {
|
||||||
|
@Override
|
||||||
|
public void execute(final Invocation invocation) {
|
||||||
|
fail();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public List<String> suggest(final Invocation invocation) {
|
||||||
|
assertEquals("hello", invocation.alias());
|
||||||
|
return ImmutableList.of("world");
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
assertSuggestions("Hello ", "world");
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void testSuggestsArgumentsAfterPartialArguments() {
|
||||||
|
final var meta = manager.metaBuilder("numbers").build();
|
||||||
|
manager.register(meta, new RawCommand() {
|
||||||
|
@Override
|
||||||
|
public void execute(final Invocation invocation) {
|
||||||
|
fail();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public List<String> suggest(final Invocation invocation) {
|
||||||
|
assertEquals("12345678", invocation.arguments());
|
||||||
|
return Collections.singletonList("9");
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
assertSuggestions("numbers 12345678", "9");
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void testDoesNotSuggestFirstArgumentIfImpermissibleAlias() {
|
||||||
|
final var callCount = new AtomicInteger();
|
||||||
|
|
||||||
|
final var meta = manager.metaBuilder("hello").build();
|
||||||
|
manager.register(meta, new RawCommand() {
|
||||||
|
@Override
|
||||||
|
public void execute(final Invocation invocation) {
|
||||||
|
fail();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public boolean hasPermission(final Invocation invocation) {
|
||||||
|
assertEquals("hello", invocation.alias());
|
||||||
|
assertEquals("", invocation.arguments());
|
||||||
|
callCount.incrementAndGet();
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public List<String> suggest(final Invocation invocation) {
|
||||||
|
return fail();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
assertSuggestions("hello ");
|
||||||
|
assertEquals(1, callCount.get());
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void testDoesNotSuggestArgumentsAfterPartialImpermissibleArguments() {
|
||||||
|
final var callCount = new AtomicInteger();
|
||||||
|
|
||||||
|
final var meta = manager.metaBuilder("foo").build();
|
||||||
|
manager.register(meta, new RawCommand() {
|
||||||
|
@Override
|
||||||
|
public void execute(final Invocation invocation) {
|
||||||
|
fail();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public boolean hasPermission(final Invocation invocation) {
|
||||||
|
assertEquals("foo", invocation.alias());
|
||||||
|
assertEquals("bar baz ", invocation.arguments());
|
||||||
|
callCount.incrementAndGet();
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public List<String> suggest(final Invocation invocation) {
|
||||||
|
return fail();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
assertSuggestions("foo bar baz ");
|
||||||
|
assertEquals(1, callCount.get());
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void testDoesNotSuggestIfFutureCompletesExceptionally() {
|
||||||
|
final var meta = manager.metaBuilder("hello").build();
|
||||||
|
manager.register(meta, new RawCommand() {
|
||||||
|
@Override
|
||||||
|
public void execute(final Invocation invocation) {
|
||||||
|
fail();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public CompletableFuture<List<String>> suggestAsync(final Invocation invocation) {
|
||||||
|
return CompletableFuture.failedFuture(new RuntimeException());
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
assertSuggestions("hello ");
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void testDoesNotSuggestIfSuggestAsyncThrows() {
|
||||||
|
final var meta = manager.metaBuilder("hello").build();
|
||||||
|
manager.register(meta, new RawCommand() {
|
||||||
|
@Override
|
||||||
|
public void execute(final Invocation invocation) {
|
||||||
|
fail();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public CompletableFuture<List<String>> suggestAsync(final Invocation invocation) {
|
||||||
|
throw new RuntimeException();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// Also logs an error to the console, but testing this is quite involved
|
||||||
|
assertSuggestions("hello ");
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void testSuggestCompletesExceptionallyIfHasPermissionThrows() {
|
||||||
|
final var meta = manager.metaBuilder("hello").build();
|
||||||
|
manager.register(meta, new RawCommand() {
|
||||||
|
@Override
|
||||||
|
public void execute(final Invocation invocation) {
|
||||||
|
fail();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public boolean hasPermission(final Invocation invocation) {
|
||||||
|
throw new RuntimeException();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public CompletableFuture<List<String>> suggestAsync(final Invocation invocation) {
|
||||||
|
return fail();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
assertThrows(CompletionException.class, () ->
|
||||||
|
manager.offerSuggestions(source, "hello ").join());
|
||||||
|
}
|
||||||
|
|
||||||
|
// Hinting
|
||||||
|
|
||||||
|
// Even if the following 2 cases look really similar, they test
|
||||||
|
// different parts of SuggestionsProvider.
|
||||||
|
@Test
|
||||||
|
void testDoesNotSuggestHintIfImpermissibleAlias() {
|
||||||
|
final var hint = LiteralArgumentBuilder
|
||||||
|
.<CommandSource>literal("hint")
|
||||||
|
.build();
|
||||||
|
final var meta = manager.metaBuilder("hello")
|
||||||
|
.hint(hint)
|
||||||
|
.build();
|
||||||
|
manager.register(meta, new RawCommand() {
|
||||||
|
@Override
|
||||||
|
public void execute(final Invocation invocation) {
|
||||||
|
fail();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public boolean hasPermission(final Invocation invocation) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
assertSuggestions("hello ");
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void testDoesNotSuggestHintIfImpermissibleArguments() {
|
||||||
|
final var hint = LiteralArgumentBuilder
|
||||||
|
.<CommandSource>literal("hint")
|
||||||
|
.build();
|
||||||
|
final var meta = manager.metaBuilder("hello")
|
||||||
|
.hint(hint)
|
||||||
|
.build();
|
||||||
|
manager.register(meta, new RawCommand() {
|
||||||
|
@Override
|
||||||
|
public void execute(final Invocation invocation) {
|
||||||
|
fail();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public boolean hasPermission(final Invocation invocation) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
assertSuggestions("hello hin");
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void testSuggestsMergesIgnoringHintsWhoseCustomSuggestionProviderFutureCompletesExceptionally() {
|
||||||
|
final var hint = RequiredArgumentBuilder
|
||||||
|
.<CommandSource, String>argument("hint", word())
|
||||||
|
.suggests((context, builder) -> CompletableFuture.failedFuture(new RuntimeException()))
|
||||||
|
.build();
|
||||||
|
final var meta = manager.metaBuilder("hello")
|
||||||
|
.hint(hint)
|
||||||
|
.build();
|
||||||
|
manager.register(meta, new RawCommand() {
|
||||||
|
@Override
|
||||||
|
public void execute(final Invocation invocation) {
|
||||||
|
fail();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public List<String> suggest(final Invocation invocation) {
|
||||||
|
return ImmutableList.of("world");
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
assertSuggestions("hello ", "world");
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void testSuggestsMergesIgnoringHintsWhoseCustomSuggestionProviderThrows() {
|
||||||
|
final var hint = RequiredArgumentBuilder
|
||||||
|
.<CommandSource, String>argument("hint", word())
|
||||||
|
.suggests((context, builder) -> {
|
||||||
|
throw new RuntimeException();
|
||||||
|
})
|
||||||
|
.build();
|
||||||
|
final var meta = manager.metaBuilder("hello")
|
||||||
|
.hint(hint)
|
||||||
|
.build();
|
||||||
|
manager.register(meta, new RawCommand() {
|
||||||
|
@Override
|
||||||
|
public void execute(final Invocation invocation) {
|
||||||
|
fail();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public List<String> suggest(final Invocation invocation) {
|
||||||
|
return ImmutableList.of("world");
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
assertSuggestions("hello ", "world");
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,483 @@
|
|||||||
|
/*
|
||||||
|
* 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 <https://www.gnu.org/licenses/>.
|
||||||
|
*/
|
||||||
|
|
||||||
|
package com.velocitypowered.proxy.command;
|
||||||
|
|
||||||
|
import static com.mojang.brigadier.arguments.StringArgumentType.word;
|
||||||
|
import static org.junit.jupiter.api.Assertions.assertArrayEquals;
|
||||||
|
import static org.junit.jupiter.api.Assertions.assertEquals;
|
||||||
|
import static org.junit.jupiter.api.Assertions.assertThrows;
|
||||||
|
import static org.junit.jupiter.api.Assertions.fail;
|
||||||
|
|
||||||
|
import com.google.common.collect.ImmutableList;
|
||||||
|
import com.mojang.brigadier.builder.LiteralArgumentBuilder;
|
||||||
|
import com.mojang.brigadier.builder.RequiredArgumentBuilder;
|
||||||
|
import com.velocitypowered.api.command.CommandSource;
|
||||||
|
import com.velocitypowered.api.command.SimpleCommand;
|
||||||
|
import java.util.Collections;
|
||||||
|
import java.util.List;
|
||||||
|
import java.util.concurrent.CompletableFuture;
|
||||||
|
import java.util.concurrent.CompletionException;
|
||||||
|
import java.util.concurrent.atomic.AtomicInteger;
|
||||||
|
import org.junit.jupiter.api.Test;
|
||||||
|
|
||||||
|
public class SimpleCommandTests extends CommandTestSuite {
|
||||||
|
|
||||||
|
// Execution
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void testExecutesAlias() {
|
||||||
|
final var callCount = new AtomicInteger();
|
||||||
|
|
||||||
|
final var meta = manager.metaBuilder("hello").build();
|
||||||
|
manager.register(meta, (SimpleCommand) invocation -> {
|
||||||
|
assertEquals(source, invocation.source());
|
||||||
|
assertEquals("hello", invocation.alias());
|
||||||
|
assertArrayEquals(new String[0], invocation.arguments());
|
||||||
|
callCount.incrementAndGet();
|
||||||
|
});
|
||||||
|
|
||||||
|
assertHandled("hello");
|
||||||
|
assertEquals(1, callCount.get());
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void testExecuteIgnoresAliasCase() {
|
||||||
|
final var callCount = new AtomicInteger();
|
||||||
|
|
||||||
|
final var meta = manager.metaBuilder("hello").build();
|
||||||
|
manager.register(meta, (SimpleCommand) invocation -> {
|
||||||
|
assertEquals("hello", invocation.alias());
|
||||||
|
callCount.incrementAndGet();
|
||||||
|
});
|
||||||
|
|
||||||
|
assertHandled("Hello");
|
||||||
|
assertEquals(1, callCount.get());
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void testExecuteInputIsTrimmed() {
|
||||||
|
final var callCount = new AtomicInteger();
|
||||||
|
|
||||||
|
final var meta = manager.metaBuilder("hello").build();
|
||||||
|
manager.register(meta, (SimpleCommand) invocation -> {
|
||||||
|
assertEquals("hello", invocation.alias());
|
||||||
|
assertArrayEquals(new String[0], invocation.arguments());
|
||||||
|
callCount.incrementAndGet();
|
||||||
|
});
|
||||||
|
|
||||||
|
assertHandled(" hello");
|
||||||
|
assertHandled(" hello");
|
||||||
|
assertHandled("hello ");
|
||||||
|
assertHandled("hello ");
|
||||||
|
assertEquals(4, callCount.get());
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void testExecuteAfterUnregisterForwards() {
|
||||||
|
final var meta = manager.metaBuilder("hello").build();
|
||||||
|
manager.register(meta, (SimpleCommand) invocation -> fail());
|
||||||
|
manager.unregister("hello");
|
||||||
|
|
||||||
|
assertForwarded("hello");
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void testForwardsAndDoesNotExecuteImpermissibleAlias() {
|
||||||
|
final var callCount = new AtomicInteger();
|
||||||
|
|
||||||
|
final var meta = manager.metaBuilder("hello").build();
|
||||||
|
manager.register(meta, new SimpleCommand() {
|
||||||
|
@Override
|
||||||
|
public void execute(final Invocation invocation) {
|
||||||
|
fail();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public boolean hasPermission(final Invocation invocation) {
|
||||||
|
assertEquals(source, invocation.source());
|
||||||
|
assertEquals("hello", invocation.alias());
|
||||||
|
assertArrayEquals(new String[0], invocation.arguments());
|
||||||
|
callCount.incrementAndGet();
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
assertForwarded("hello");
|
||||||
|
assertEquals(1, callCount.get());
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void testExecutesWithArguments() {
|
||||||
|
final var callCount = new AtomicInteger();
|
||||||
|
|
||||||
|
final var meta = manager.metaBuilder("hello").build();
|
||||||
|
manager.register(meta, (SimpleCommand) invocation -> {
|
||||||
|
assertEquals("hello", invocation.alias());
|
||||||
|
assertArrayEquals(new String[] { "dear", "world" }, invocation.arguments());
|
||||||
|
callCount.incrementAndGet();
|
||||||
|
});
|
||||||
|
|
||||||
|
assertHandled("hello dear world");
|
||||||
|
assertEquals(1, callCount.get());
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void testHandlesAndDoesNotExecuteWithImpermissibleArgs() {
|
||||||
|
final var callCount = new AtomicInteger();
|
||||||
|
|
||||||
|
final var meta = manager.metaBuilder("color").build();
|
||||||
|
manager.register(meta, new SimpleCommand() {
|
||||||
|
@Override
|
||||||
|
public void execute(final Invocation invocation) {
|
||||||
|
fail();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public boolean hasPermission(final Invocation invocation) {
|
||||||
|
assertEquals("color", invocation.alias());
|
||||||
|
assertArrayEquals(new String[] { "red" }, invocation.arguments());
|
||||||
|
callCount.incrementAndGet();
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
assertHandled("color red");
|
||||||
|
assertEquals(1, callCount.get());
|
||||||
|
}
|
||||||
|
|
||||||
|
// Suggestions
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void testDoesNotSuggestAliasIfImpermissible() {
|
||||||
|
final var meta = manager.metaBuilder("hello").build();
|
||||||
|
manager.register(meta, new SimpleCommand() {
|
||||||
|
@Override
|
||||||
|
public void execute(final Invocation invocation) {
|
||||||
|
fail();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public boolean hasPermission(final Invocation invocation) {
|
||||||
|
assertEquals("hello", invocation.alias());
|
||||||
|
assertArrayEquals(new String[0], invocation.arguments());
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public List<String> suggest(final Invocation invocation) {
|
||||||
|
return fail();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void testDoesNotSuggestAliasAfterUnregister() {
|
||||||
|
final var meta = manager.metaBuilder("hello").build();
|
||||||
|
manager.register(meta, new SimpleCommand() {
|
||||||
|
@Override
|
||||||
|
public void execute(final Invocation invocation) {
|
||||||
|
fail();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public List<String> suggest(final Invocation invocation) {
|
||||||
|
return fail();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
manager.unregister("hello");
|
||||||
|
|
||||||
|
assertSuggestions("");
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void testSuggestsArgumentsAfterAlias() {
|
||||||
|
final var meta = manager.metaBuilder("hello").build();
|
||||||
|
manager.register(meta, new SimpleCommand() {
|
||||||
|
@Override
|
||||||
|
public void execute(final Invocation invocation) {
|
||||||
|
fail();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public List<String> suggest(final Invocation invocation) {
|
||||||
|
assertEquals("hello", invocation.alias());
|
||||||
|
assertArrayEquals(new String[0], invocation.arguments());
|
||||||
|
return ImmutableList.of("world", "people"); // ensures we don't mutate the user's list
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
assertSuggestions("hello ", "people", "world"); // in alphabetical order
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void testSuggestsArgumentsAfterAliasIgnoresAliasCase() {
|
||||||
|
final var meta = manager.metaBuilder("hello").build();
|
||||||
|
manager.register(meta, new SimpleCommand() {
|
||||||
|
@Override
|
||||||
|
public void execute(final Invocation invocation) {
|
||||||
|
fail();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public List<String> suggest(final Invocation invocation) {
|
||||||
|
assertEquals("hello", invocation.alias());
|
||||||
|
return ImmutableList.of("world");
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
assertSuggestions("Hello ", "world");
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void testSuggestsArgumentsAfterPartialArguments() {
|
||||||
|
final var meta = manager.metaBuilder("numbers").build();
|
||||||
|
manager.register(meta, new SimpleCommand() {
|
||||||
|
@Override
|
||||||
|
public void execute(final Invocation invocation) {
|
||||||
|
fail();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public List<String> suggest(final Invocation invocation) {
|
||||||
|
assertArrayEquals(new String[] { "12345678" }, invocation.arguments());
|
||||||
|
return Collections.singletonList("9");
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
assertSuggestions("numbers 12345678", "9");
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void testDoesNotSuggestFirstArgumentIfImpermissibleAlias() {
|
||||||
|
final var callCount = new AtomicInteger();
|
||||||
|
|
||||||
|
final var meta = manager.metaBuilder("hello").build();
|
||||||
|
manager.register(meta, new SimpleCommand() {
|
||||||
|
@Override
|
||||||
|
public void execute(final Invocation invocation) {
|
||||||
|
fail();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public boolean hasPermission(final Invocation invocation) {
|
||||||
|
assertEquals("hello", invocation.alias());
|
||||||
|
assertArrayEquals(new String[0], invocation.arguments());
|
||||||
|
callCount.incrementAndGet();
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public List<String> suggest(final Invocation invocation) {
|
||||||
|
return fail();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
assertSuggestions("hello ");
|
||||||
|
assertEquals(1, callCount.get());
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void testDoesNotSuggestArgumentsAfterPartialImpermissibleArguments() {
|
||||||
|
final var callCount = new AtomicInteger();
|
||||||
|
|
||||||
|
final var meta = manager.metaBuilder("foo").build();
|
||||||
|
manager.register(meta, new SimpleCommand() {
|
||||||
|
@Override
|
||||||
|
public void execute(final Invocation invocation) {
|
||||||
|
fail();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public boolean hasPermission(final Invocation invocation) {
|
||||||
|
assertEquals("foo", invocation.alias());
|
||||||
|
assertArrayEquals(new String[] { "bar", "baz", "" }, invocation.arguments());
|
||||||
|
callCount.incrementAndGet();
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public List<String> suggest(final Invocation invocation) {
|
||||||
|
return fail();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
assertSuggestions("foo bar baz ");
|
||||||
|
assertEquals(1, callCount.get());
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void testDoesNotSuggestIfFutureCompletesExceptionally() {
|
||||||
|
final var meta = manager.metaBuilder("hello").build();
|
||||||
|
manager.register(meta, new SimpleCommand() {
|
||||||
|
@Override
|
||||||
|
public void execute(final Invocation invocation) {
|
||||||
|
fail();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public CompletableFuture<List<String>> suggestAsync(final Invocation invocation) {
|
||||||
|
return CompletableFuture.failedFuture(new RuntimeException());
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
assertSuggestions("hello ");
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void testDoesNotSuggestIfSuggestAsyncThrows() {
|
||||||
|
final var meta = manager.metaBuilder("hello").build();
|
||||||
|
manager.register(meta, new SimpleCommand() {
|
||||||
|
@Override
|
||||||
|
public void execute(final Invocation invocation) {
|
||||||
|
fail();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public CompletableFuture<List<String>> suggestAsync(final Invocation invocation) {
|
||||||
|
throw new RuntimeException();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// Also logs an error to the console, but testing this is quite involved
|
||||||
|
assertSuggestions("hello ");
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void testSuggestCompletesExceptionallyIfHasPermissionThrows() {
|
||||||
|
final var meta = manager.metaBuilder("hello").build();
|
||||||
|
manager.register(meta, new SimpleCommand() {
|
||||||
|
@Override
|
||||||
|
public void execute(final Invocation invocation) {
|
||||||
|
fail();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public boolean hasPermission(final Invocation invocation) {
|
||||||
|
throw new RuntimeException();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public CompletableFuture<List<String>> suggestAsync(final Invocation invocation) {
|
||||||
|
return fail();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
assertThrows(CompletionException.class, () ->
|
||||||
|
manager.offerSuggestions(source, "hello ").join());
|
||||||
|
}
|
||||||
|
|
||||||
|
// Hinting
|
||||||
|
|
||||||
|
// Even if the following 2 cases look really similar, they test
|
||||||
|
// different parts of SuggestionsProvider.
|
||||||
|
@Test
|
||||||
|
void testDoesNotSuggestHintIfImpermissibleAlias() {
|
||||||
|
final var hint = LiteralArgumentBuilder
|
||||||
|
.<CommandSource>literal("hint")
|
||||||
|
.build();
|
||||||
|
final var meta = manager.metaBuilder("hello")
|
||||||
|
.hint(hint)
|
||||||
|
.build();
|
||||||
|
manager.register(meta, new SimpleCommand() {
|
||||||
|
@Override
|
||||||
|
public void execute(final Invocation invocation) {
|
||||||
|
fail();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public boolean hasPermission(final Invocation invocation) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
assertSuggestions("hello ");
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void testDoesNotSuggestHintIfImpermissibleArguments() {
|
||||||
|
final var hint = LiteralArgumentBuilder
|
||||||
|
.<CommandSource>literal("hint")
|
||||||
|
.build();
|
||||||
|
final var meta = manager.metaBuilder("hello")
|
||||||
|
.hint(hint)
|
||||||
|
.build();
|
||||||
|
manager.register(meta, new SimpleCommand() {
|
||||||
|
@Override
|
||||||
|
public void execute(final Invocation invocation) {
|
||||||
|
fail();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public boolean hasPermission(final Invocation invocation) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
assertSuggestions("hello hin");
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void testSuggestsMergesIgnoringHintsWhoseCustomSuggestionProviderFutureCompletesExceptionally() {
|
||||||
|
final var hint = RequiredArgumentBuilder
|
||||||
|
.<CommandSource, String>argument("hint", word())
|
||||||
|
.suggests((context, builder) -> CompletableFuture.failedFuture(new RuntimeException()))
|
||||||
|
.build();
|
||||||
|
final var meta = manager.metaBuilder("hello")
|
||||||
|
.hint(hint)
|
||||||
|
.build();
|
||||||
|
manager.register(meta, new SimpleCommand() {
|
||||||
|
@Override
|
||||||
|
public void execute(final Invocation invocation) {
|
||||||
|
fail();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public List<String> suggest(final Invocation invocation) {
|
||||||
|
return ImmutableList.of("world");
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
assertSuggestions("hello ", "world");
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void testSuggestsMergesIgnoringHintsWhoseCustomSuggestionProviderThrows() {
|
||||||
|
final var hint = RequiredArgumentBuilder
|
||||||
|
.<CommandSource, String>argument("hint", word())
|
||||||
|
.suggests((context, builder) -> {
|
||||||
|
throw new RuntimeException();
|
||||||
|
})
|
||||||
|
.build();
|
||||||
|
final var meta = manager.metaBuilder("hello")
|
||||||
|
.hint(hint)
|
||||||
|
.build();
|
||||||
|
manager.register(meta, new SimpleCommand() {
|
||||||
|
@Override
|
||||||
|
public void execute(final Invocation invocation) {
|
||||||
|
fail();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public List<String> suggest(final Invocation invocation) {
|
||||||
|
return ImmutableList.of("world");
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
assertSuggestions("hello ", "world");
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,259 @@
|
|||||||
|
/*
|
||||||
|
* 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 <https://www.gnu.org/licenses/>.
|
||||||
|
*/
|
||||||
|
|
||||||
|
package com.velocitypowered.proxy.command;
|
||||||
|
|
||||||
|
import static com.mojang.brigadier.arguments.StringArgumentType.word;
|
||||||
|
import static org.junit.jupiter.api.Assertions.fail;
|
||||||
|
|
||||||
|
import com.google.common.collect.ImmutableList;
|
||||||
|
import com.mojang.brigadier.builder.LiteralArgumentBuilder;
|
||||||
|
import com.mojang.brigadier.builder.RequiredArgumentBuilder;
|
||||||
|
import com.velocitypowered.api.command.Command;
|
||||||
|
import com.velocitypowered.api.command.CommandSource;
|
||||||
|
import com.velocitypowered.api.command.RawCommand;
|
||||||
|
import java.util.List;
|
||||||
|
import java.util.concurrent.CompletableFuture;
|
||||||
|
import org.junit.jupiter.api.Test;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Tests {@link Command} implementation-independent suggestion methods of
|
||||||
|
* {@link SuggestionsProvider}.
|
||||||
|
*/
|
||||||
|
public class SuggestionsProviderTests extends CommandTestSuite {
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void testSuggestsAliasesForEmptyInput() {
|
||||||
|
manager.register(manager.metaBuilder("foo").build(), NoSuggestionsCommand.INSTANCE);
|
||||||
|
manager.register(manager.metaBuilder("bar").build(), NoSuggestionsCommand.INSTANCE);
|
||||||
|
manager.register(manager.metaBuilder("baz").build(), NoSuggestionsCommand.INSTANCE);
|
||||||
|
|
||||||
|
assertSuggestions("", "bar", "baz", "foo"); // in alphabetical order
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void testDoesNotSuggestForLeadingWhitespace() {
|
||||||
|
final var meta = manager.metaBuilder("hello").build();
|
||||||
|
manager.register(meta, NoSuggestionsCommand.INSTANCE);
|
||||||
|
|
||||||
|
assertSuggestions(" ");
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void testSuggestsAliasesForPartialAlias() {
|
||||||
|
manager.register(manager.metaBuilder("hello").build(), NoSuggestionsCommand.INSTANCE);
|
||||||
|
manager.register(manager.metaBuilder("hey").build(), NoSuggestionsCommand.INSTANCE);
|
||||||
|
|
||||||
|
assertSuggestions("hell", "hello");
|
||||||
|
assertSuggestions("He", "hello", "hey");
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void testDoesNotSuggestForFullAlias() {
|
||||||
|
final var meta = manager.metaBuilder("hello").build();
|
||||||
|
manager.register(meta, NoSuggestionsCommand.INSTANCE);
|
||||||
|
|
||||||
|
assertSuggestions("hello");
|
||||||
|
assertSuggestions("Hello");
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void testDoesNotSuggestForPartialIncorrectAlias() {
|
||||||
|
final var meta = manager.metaBuilder("hello").build();
|
||||||
|
manager.register(meta, NoSuggestionsCommand.INSTANCE);
|
||||||
|
|
||||||
|
assertSuggestions("yo");
|
||||||
|
assertSuggestions("welcome");
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void testDoesNotSuggestArgumentsForIncorrectAlias() {
|
||||||
|
final var meta = manager.metaBuilder("hello").build();
|
||||||
|
manager.register(meta, new RawCommand() {
|
||||||
|
@Override
|
||||||
|
public void execute(final Invocation invocation) {
|
||||||
|
fail();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public List<String> suggest(final Invocation invocation) {
|
||||||
|
return fail();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
assertSuggestions("hell ");
|
||||||
|
}
|
||||||
|
|
||||||
|
// Secondary aliases
|
||||||
|
// The following tests check for inconsistencies between the primary alias node and
|
||||||
|
// a secondary alias literal.
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void testSuggestsAllAliases() {
|
||||||
|
final var meta = manager.metaBuilder("foo")
|
||||||
|
.aliases("bar", "baz")
|
||||||
|
.build();
|
||||||
|
manager.register(meta, NoSuggestionsCommand.INSTANCE);
|
||||||
|
|
||||||
|
assertSuggestions("", "bar", "baz", "foo");
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void testSuggestsArgumentsViaAlias() {
|
||||||
|
final var meta = manager.metaBuilder("hello")
|
||||||
|
.aliases("hi")
|
||||||
|
.build();
|
||||||
|
manager.register(meta, new RawCommand() {
|
||||||
|
@Override
|
||||||
|
public void execute(final Invocation invocation) {
|
||||||
|
fail();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public List<String> suggest(final Invocation invocation) {
|
||||||
|
return ImmutableList.of("world");
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
assertSuggestions("hi ", "world");
|
||||||
|
}
|
||||||
|
|
||||||
|
// Hinting
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void testSuggestsHintLiteral() {
|
||||||
|
final var hint = LiteralArgumentBuilder
|
||||||
|
.<CommandSource>literal("hint")
|
||||||
|
.build();
|
||||||
|
final var meta = manager.metaBuilder("hello")
|
||||||
|
.hint(hint)
|
||||||
|
.build();
|
||||||
|
manager.register(meta, NoSuggestionsCommand.INSTANCE);
|
||||||
|
|
||||||
|
assertSuggestions("hello ", "hint");
|
||||||
|
assertSuggestions("hello hin", "hint");
|
||||||
|
assertSuggestions("hello hint");
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void testSuggestsHintCustomSuggestions() {
|
||||||
|
final var hint = RequiredArgumentBuilder
|
||||||
|
.<CommandSource, String>argument("hint", word())
|
||||||
|
.suggests((context, builder) -> builder
|
||||||
|
.suggest("one")
|
||||||
|
.suggest("two")
|
||||||
|
.suggest("three")
|
||||||
|
.buildFuture())
|
||||||
|
.build();
|
||||||
|
final var meta = manager.metaBuilder("hello")
|
||||||
|
.hint(hint)
|
||||||
|
.build();
|
||||||
|
manager.register(meta, NoSuggestionsCommand.INSTANCE);
|
||||||
|
|
||||||
|
assertSuggestions("hello ", "one", "three", "two");
|
||||||
|
assertSuggestions("hello two", "one", "three");
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void testSuggestsMergesArgumentsSuggestionsWithHintSuggestions() {
|
||||||
|
final var hint = LiteralArgumentBuilder
|
||||||
|
.<CommandSource>literal("bar")
|
||||||
|
.build();
|
||||||
|
final var meta = manager.metaBuilder("foo")
|
||||||
|
.hint(hint)
|
||||||
|
.build();
|
||||||
|
manager.register(meta, new RawCommand() {
|
||||||
|
@Override
|
||||||
|
public void execute(final Invocation invocation) {
|
||||||
|
fail();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public List<String> suggest(final Invocation invocation) {
|
||||||
|
return ImmutableList.of("baz", "qux");
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
assertSuggestions("foo ", "bar", "baz", "qux");
|
||||||
|
assertSuggestions("foo bar", "baz", "qux");
|
||||||
|
assertSuggestions("foo baz", "qux");
|
||||||
|
}
|
||||||
|
|
||||||
|
// Hints are suggested iff the source can use the given arguments; see
|
||||||
|
// VelocityCommandMeta#copyHints.
|
||||||
|
@Test
|
||||||
|
void testSuggestIgnoresHintRequirementPredicateResults() {
|
||||||
|
final var hint = RequiredArgumentBuilder
|
||||||
|
.<CommandSource, String>argument("hint", word())
|
||||||
|
.requires(source1 -> fail())
|
||||||
|
.suggests((context, builder) -> builder.suggest("suggestion").buildFuture())
|
||||||
|
.build();
|
||||||
|
final var meta = manager.metaBuilder("hello")
|
||||||
|
.hint(hint)
|
||||||
|
.build();
|
||||||
|
manager.register(meta, NoSuggestionsCommand.INSTANCE);
|
||||||
|
|
||||||
|
assertSuggestions("hello ", "suggestion");
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void testDoesNotSuggestHintIfHintSuggestionProviderFutureCompletesExceptionally() {
|
||||||
|
final var hint = RequiredArgumentBuilder
|
||||||
|
.<CommandSource, String>argument("hint", word())
|
||||||
|
.suggests((context, builder) -> CompletableFuture.failedFuture(new RuntimeException()))
|
||||||
|
.build();
|
||||||
|
final var meta = manager.metaBuilder("hello")
|
||||||
|
.hint(hint)
|
||||||
|
.build();
|
||||||
|
manager.register(meta, NoSuggestionsCommand.INSTANCE);
|
||||||
|
|
||||||
|
assertSuggestions("hello ");
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void testDoesNotSuggestHintIfCustomSuggestionProviderThrows() {
|
||||||
|
final var hint = RequiredArgumentBuilder
|
||||||
|
.<CommandSource, String>argument("hint", word())
|
||||||
|
.suggests((context, builder) -> {
|
||||||
|
throw new RuntimeException();
|
||||||
|
})
|
||||||
|
.build();
|
||||||
|
final var meta = manager.metaBuilder("hello")
|
||||||
|
.hint(hint)
|
||||||
|
.build();
|
||||||
|
manager.register(meta, NoSuggestionsCommand.INSTANCE);
|
||||||
|
|
||||||
|
assertSuggestions("hello ");
|
||||||
|
}
|
||||||
|
|
||||||
|
static final class NoSuggestionsCommand implements RawCommand {
|
||||||
|
|
||||||
|
static final NoSuggestionsCommand INSTANCE = new NoSuggestionsCommand();
|
||||||
|
|
||||||
|
private NoSuggestionsCommand() {}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void execute(final Invocation invocation) {
|
||||||
|
fail();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public List<String> suggest(final Invocation invocation) {
|
||||||
|
return ImmutableList.of();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,111 @@
|
|||||||
|
/*
|
||||||
|
* 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 <https://www.gnu.org/licenses/>.
|
||||||
|
*/
|
||||||
|
|
||||||
|
package com.velocitypowered.proxy.command.brigadier;
|
||||||
|
|
||||||
|
import static org.junit.jupiter.api.Assertions.assertArrayEquals;
|
||||||
|
import static org.junit.jupiter.api.Assertions.assertFalse;
|
||||||
|
|
||||||
|
import com.mojang.brigadier.StringReader;
|
||||||
|
import com.mojang.brigadier.exceptions.CommandSyntaxException;
|
||||||
|
import org.junit.jupiter.api.Test;
|
||||||
|
|
||||||
|
public class StringArrayArgumentTypeTests {
|
||||||
|
|
||||||
|
private static final StringArrayArgumentType TYPE = StringArrayArgumentType.INSTANCE;
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void testEmptyString() throws CommandSyntaxException {
|
||||||
|
final StringReader reader = new StringReader("");
|
||||||
|
assertArrayEquals(new String[0], TYPE.parse(reader));
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void testParseWord() throws CommandSyntaxException {
|
||||||
|
final StringReader reader = new StringReader("Hello");
|
||||||
|
assertArrayEquals(new String[] { "Hello" }, TYPE.parse(reader));
|
||||||
|
assertFalse(reader.canRead());
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void testParseString() throws CommandSyntaxException {
|
||||||
|
final StringReader reader = new StringReader("Hello world!");
|
||||||
|
assertArrayEquals(new String[] { "Hello", "world!" }, TYPE.parse(reader));
|
||||||
|
assertFalse(reader.canRead());
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void testNoEscaping() throws CommandSyntaxException {
|
||||||
|
final StringReader reader = new StringReader("\"My house\" is blue");
|
||||||
|
assertArrayEquals(new String[] { "\"My", "house\"", "is", "blue" }, TYPE.parse(reader));
|
||||||
|
assertFalse(reader.canRead());
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void testUnbalancedEscapingIsIgnored() throws CommandSyntaxException {
|
||||||
|
final StringReader reader = new StringReader("This is a \"sentence");
|
||||||
|
assertArrayEquals(new String[] { "This", "is", "a", "\"sentence" }, TYPE.parse(reader));
|
||||||
|
assertFalse(reader.canRead());
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void testLeadingWhitespace() throws CommandSyntaxException {
|
||||||
|
final StringReader reader = new StringReader(" ¡Hola!");
|
||||||
|
assertArrayEquals(new String[] { "", "¡Hola!" }, TYPE.parse(reader));
|
||||||
|
assertFalse(reader.canRead());
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void testMultipleLeadingWhitespace() throws CommandSyntaxException {
|
||||||
|
final StringReader reader = new StringReader(" Anguish Languish");
|
||||||
|
assertArrayEquals(new String[] { "", "", "", "Anguish", "Languish" }, TYPE.parse(reader));
|
||||||
|
assertFalse(reader.canRead());
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void testTrailingWhitespace() throws CommandSyntaxException {
|
||||||
|
final StringReader reader = new StringReader("This is a test. ");
|
||||||
|
assertArrayEquals(new String[] { "This", "is", "a", "test.", "" }, TYPE.parse(reader));
|
||||||
|
assertFalse(reader.canRead());
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void testMultipleTrailingWhitespace() throws CommandSyntaxException {
|
||||||
|
final StringReader reader = new StringReader("Lorem ipsum ");
|
||||||
|
assertArrayEquals(new String[] { "Lorem", "ipsum", "", "" }, TYPE.parse(reader));
|
||||||
|
assertFalse(reader.canRead());
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void testMultipleWhitespaceCharsArePreserved() throws CommandSyntaxException {
|
||||||
|
final StringReader reader = new StringReader(
|
||||||
|
" This is a message that shouldn't be normalized ");
|
||||||
|
assertArrayEquals(new String[] {
|
||||||
|
"", "This", "", "is", "a", "", "", "message", "", "that", "shouldn't", "", "", "", "be",
|
||||||
|
"normalized", "", ""}, TYPE.parse(reader));
|
||||||
|
assertFalse(reader.canRead());
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void testRespectsCursor() throws CommandSyntaxException {
|
||||||
|
final StringReader reader = new StringReader("Hello beautiful world");
|
||||||
|
reader.setCursor(6);
|
||||||
|
|
||||||
|
assertArrayEquals(new String[] { "beautiful", "world"}, TYPE.parse(reader));
|
||||||
|
assertFalse(reader.canRead());
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,102 @@
|
|||||||
|
/*
|
||||||
|
* 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 <https://www.gnu.org/licenses/>.
|
||||||
|
*/
|
||||||
|
|
||||||
|
package com.velocitypowered.proxy.command.brigadier;
|
||||||
|
|
||||||
|
import static com.velocitypowered.proxy.command.brigadier.VelocityArgumentBuilder.velocityArgument;
|
||||||
|
import static org.junit.jupiter.api.Assertions.assertArrayEquals;
|
||||||
|
import static org.junit.jupiter.api.Assertions.assertEquals;
|
||||||
|
import static org.junit.jupiter.api.Assertions.assertFalse;
|
||||||
|
import static org.junit.jupiter.api.Assertions.assertSame;
|
||||||
|
import static org.junit.jupiter.api.Assertions.assertTrue;
|
||||||
|
|
||||||
|
import com.mojang.brigadier.CommandDispatcher;
|
||||||
|
import com.mojang.brigadier.StringReader;
|
||||||
|
import com.mojang.brigadier.context.CommandContextBuilder;
|
||||||
|
import com.mojang.brigadier.context.ParsedArgument;
|
||||||
|
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 org.junit.jupiter.api.BeforeEach;
|
||||||
|
import org.junit.jupiter.api.Test;
|
||||||
|
|
||||||
|
@SuppressWarnings("unchecked")
|
||||||
|
public class VelocityArgumentCommandNodeTests {
|
||||||
|
|
||||||
|
private static final StringArrayArgumentType STRING_ARRAY = StringArrayArgumentType.INSTANCE;
|
||||||
|
|
||||||
|
private CommandContextBuilder<Object> contextBuilder;
|
||||||
|
|
||||||
|
@BeforeEach
|
||||||
|
void setUp() {
|
||||||
|
final CommandDispatcher<Object> dispatcher = new CommandDispatcher<>();
|
||||||
|
this.contextBuilder = new CommandContextBuilder<>(dispatcher, new Object(),
|
||||||
|
dispatcher.getRoot(), 0);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void testParse() throws CommandSyntaxException {
|
||||||
|
final VelocityArgumentCommandNode<Object, String[]> node =
|
||||||
|
velocityArgument("foo", STRING_ARRAY).build();
|
||||||
|
final StringReader reader = new StringReader("hello world");
|
||||||
|
node.parse(reader, this.contextBuilder);
|
||||||
|
|
||||||
|
final StringRange expectedRange = StringRange.between(0, reader.getTotalLength());
|
||||||
|
|
||||||
|
assertFalse(reader.canRead());
|
||||||
|
|
||||||
|
assertFalse(this.contextBuilder.getNodes().isEmpty());
|
||||||
|
assertSame(node, this.contextBuilder.getNodes().get(0).getNode());
|
||||||
|
assertEquals(expectedRange, this.contextBuilder.getNodes().get(0).getRange());
|
||||||
|
|
||||||
|
assertTrue(this.contextBuilder.getArguments().containsKey("foo"));
|
||||||
|
final ParsedArgument<Object, String[]> parsed =
|
||||||
|
(ParsedArgument<Object, String[]>) this.contextBuilder.getArguments().get("foo");
|
||||||
|
assertArrayEquals(new String[] { "hello", "world" }, parsed.getResult());
|
||||||
|
assertEquals(expectedRange, parsed.getRange());
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void testDefaultSuggestions() throws CommandSyntaxException {
|
||||||
|
final VelocityArgumentCommandNode<Object, String[]> node =
|
||||||
|
velocityArgument("foo", STRING_ARRAY).build();
|
||||||
|
final Suggestions result = node.listSuggestions(
|
||||||
|
this.contextBuilder.build(""), new SuggestionsBuilder("", 0)).join();
|
||||||
|
|
||||||
|
assertTrue(result.isEmpty());
|
||||||
|
}
|
||||||
|
|
||||||
|
// This only tests delegation to the given SuggestionsProvider; suggestions merging
|
||||||
|
// and filtering is already tested in Brigadier.
|
||||||
|
@Test
|
||||||
|
void testCustomSuggestions() throws CommandSyntaxException {
|
||||||
|
final VelocityArgumentCommandNode<Object, String[]> node =
|
||||||
|
velocityArgument("foo", STRING_ARRAY)
|
||||||
|
.suggests((context, builder) -> {
|
||||||
|
builder.suggest("bar");
|
||||||
|
builder.suggest("baz");
|
||||||
|
return builder.buildFuture();
|
||||||
|
})
|
||||||
|
.build();
|
||||||
|
final Suggestions result = node.listSuggestions(
|
||||||
|
this.contextBuilder.build(""), new SuggestionsBuilder("", 0)).join();
|
||||||
|
|
||||||
|
assertEquals("bar", result.getList().get(0).getText());
|
||||||
|
assertEquals("baz", result.getList().get(1).getText());
|
||||||
|
}
|
||||||
|
}
|
Laden…
In neuem Issue referenzieren
Einen Benutzer sperren