13
0
geforkt von Mirrors/Velocity

Command implementation refactor

Dieser Commit ist enthalten in:
Hugo Manrique 2021-06-05 23:19:01 +02:00
Ursprung ff504c21ef
Commit 703b91e0fa
Es konnte kein GPG-Schlüssel zu dieser Signatur gefunden werden
GPG-Schlüssel-ID: A60730A4A4ACE782
28 geänderte Dateien mit 1726 neuen und 542 gelöschten Zeilen

Datei anzeigen

@ -0,0 +1,123 @@
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) {
// TODO Document redirects to non-Brigadier commands are not supported
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);
}
}

Datei anzeigen

@ -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);
}

Datei anzeigen

@ -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);
}
}

Datei anzeigen

@ -21,14 +21,26 @@ import com.google.common.base.Preconditions;
import com.mojang.brigadier.CommandDispatcher; import com.mojang.brigadier.CommandDispatcher;
import com.mojang.brigadier.ParseResults; import com.mojang.brigadier.ParseResults;
import com.mojang.brigadier.StringReader; import com.mojang.brigadier.StringReader;
import com.mojang.brigadier.context.CommandContext;
import com.mojang.brigadier.context.CommandContextBuilder; import com.mojang.brigadier.context.CommandContextBuilder;
import com.mojang.brigadier.context.StringRange; import com.mojang.brigadier.context.StringRange;
import com.mojang.brigadier.exceptions.CommandSyntaxException;
import com.mojang.brigadier.suggestion.Suggestions; import com.mojang.brigadier.suggestion.Suggestions;
import com.mojang.brigadier.suggestion.SuggestionsBuilder;
import com.mojang.brigadier.tree.CommandNode; import com.mojang.brigadier.tree.CommandNode;
import com.mojang.brigadier.tree.LiteralCommandNode; import com.mojang.brigadier.tree.LiteralCommandNode;
import com.mojang.brigadier.tree.RootCommandNode; import com.velocitypowered.api.command.Command;
import com.velocitypowered.proxy.command.brigadier.VelocityArgumentCommandNode;
import java.util.ArrayList;
import java.util.Collection; 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.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. * Provides suggestions for a given command input.
@ -41,10 +53,16 @@ import java.util.concurrent.CompletableFuture;
*/ */
final class SuggestionsProvider<S> { final class SuggestionsProvider<S> {
private final CommandDispatcher<S> dispatcher; private static final Logger LOGGER = LogManager.getLogger(SuggestionsProvider.class);
SuggestionsProvider(final CommandDispatcher<S> dispatcher) { 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.dispatcher = Preconditions.checkNotNull(dispatcher, "dispatcher");
this.lock = Preconditions.checkNotNull(lock, "lock");
} }
/** /**
@ -69,8 +87,10 @@ final class SuggestionsProvider<S> {
*/ */
private CompletableFuture<Suggestions> provideSuggestions( private CompletableFuture<Suggestions> provideSuggestions(
final StringReader reader, final CommandContextBuilder<S> context) { final StringReader reader, final CommandContextBuilder<S> context) {
lock.lock();
try {
final StringRange aliasRange = this.consumeAlias(reader); final StringRange aliasRange = this.consumeAlias(reader);
final String alias = aliasRange.get(reader); // TODO #toLowerCase and test final String alias = aliasRange.get(reader).toLowerCase(Locale.ENGLISH);
final LiteralCommandNode<S> literal = final LiteralCommandNode<S> literal =
(LiteralCommandNode<S>) context.getRootNode().getChild(alias); (LiteralCommandNode<S>) context.getRootNode().getChild(alias);
@ -82,9 +102,12 @@ final class SuggestionsProvider<S> {
} }
context.withNode(literal, aliasRange); context.withNode(literal, aliasRange);
reader.skip(); // separator reader.skip(); // separator
// TODO Provide arguments suggestions return this.provideArgumentsSuggestions(literal, reader, context);
} else { } else {
// TODO Provide alias suggestions return this.provideAliasSuggestions(reader, context);
}
} finally {
lock.unlock();
} }
} }
@ -97,6 +120,19 @@ final class SuggestionsProvider<S> {
return range; return range;
} }
/**
* Returns whether a literal node with the given name should be considered for
* suggestions given the specified input.
*
* @param name the 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) {
// TODO (perf) If we expect input to be lowercase, no need to ignore case
return name.regionMatches(true, 0, input, 0, input.length());
}
/** /**
* Returns alias suggestions for the given input. * Returns alias suggestions for the given input.
* *
@ -110,11 +146,206 @@ final class SuggestionsProvider<S> {
final String input = reader.getRead(); final String input = reader.getRead();
final Collection<CommandNode<S>> aliases = contextSoFar.getRootNode().getChildren(); final Collection<CommandNode<S>> aliases = contextSoFar.getRootNode().getChildren();
@SuppressWarnings("unchecked")
final CompletableFuture<Suggestions>[] futures = new CompletableFuture[aliases.size()]; 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);
} }
/*private RootCommandNode<S> getRoot() { /**
return this.dispatcher.getRoot(); * 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 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);
return this.dispatcher.getCompletionSuggestions(parse);
}
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 cmdSuggestions;
}
// Parse the hint nodes to get remaining suggestions
reader.setCursor(start);
final CompletableFuture<Suggestions> hintSuggestions =
this.getHintSuggestions(alias, reader, contextSoFar);
final String fullInput = reader.getString();
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) {
// Ugly, ugly swallowing of everything Throwable, because plugins are naughty.
LOGGER.error("Arguments node cannot provide suggestions, skipping", 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);
return this.dispatcher.getCompletionSuggestions(parse);
}
/**
* 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
*/
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()) {
suggestions.add(future.join());
}
}
return Suggestions.merge(fullInput, suggestions);
});
}
} }

Datei anzeigen

@ -18,40 +18,61 @@
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.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;
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 +98,34 @@ 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(); for (final CommandRegistrar<?> registrar : this.registrars) {
String primaryAlias = aliasIterator.next(); if (this.tryRegister(registrar, command, meta)) {
return;
LiteralCommandNode<CommandSource> node = null; }
if (command instanceof BrigadierCommand) { }
node = ((BrigadierCommand) command).getNode(); throw new IllegalArgumentException(
} else if (command instanceof SimpleCommand) { command + " does not implement a registrable Command subinterface");
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)) { private <T extends Command> boolean tryRegister(final CommandRegistrar<T> registrar,
for (CommandNode<CommandSource> hint : meta.getHints()) { final Command command, final CommandMeta meta) {
node.addChild(BrigadierUtils.wrapForHinting(hint, node.getCommand())); final Class<T> superInterface = registrar.registrableSuperInterface();
if (!superInterface.isInstance(command)) {
return false;
} }
} try {
registrar.register(superInterface.cast(command), meta);
dispatcher.getRoot().addChild(node); return true;
while (aliasIterator.hasNext()) { } catch (final IllegalArgumentException ignored) {
String currentAlias = aliasIterator.next(); return false;
CommandNode<CommandSource> existingNode = dispatcher.getRoot()
.getChild(currentAlias.toLowerCase(Locale.ENGLISH));
if (existingNode != null) {
dispatcher.getRoot().getChildren().remove(existingNode);
}
dispatcher.getRoot().addChild(BrigadierUtils.buildRedirect(currentAlias, node));
} }
} }
@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 +147,10 @@ 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);
final ParseResults<CommandSource> parse = this.parse(normalizedInput, source);
try { try {
return dispatcher.execute(results) != BrigadierCommand.FORWARD; 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 +204,32 @@ 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) return suggestionsProvider.provideSuggestions(normalizedInput, source)
.thenApply(suggestions -> Lists.transform(suggestions.getList(), Suggestion::getText)); .thenApply(suggestions -> Lists.transform(suggestions.getList(), Suggestion::getText));
} }
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) {
@ -214,6 +238,11 @@ public class VelocityCommandManager implements CommandManager {
} }
public CommandDispatcher<CommandSource> getDispatcher() { public CommandDispatcher<CommandSource> getDispatcher() {
// TODO Can we remove this? This is only used by tests, and constitutes unsafe publication.
return dispatcher; return dispatcher;
} }
public CommandGraphInjector<CommandSource> getInjector() {
return injector;
}
} }

Datei anzeigen

@ -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;
} }
} }

Datei anzeigen

@ -0,0 +1,174 @@
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();
}
}

Datei anzeigen

@ -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;
}
}

Datei anzeigen

@ -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;
}
}

Datei anzeigen

@ -0,0 +1,46 @@
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;
}
}

Datei anzeigen

@ -0,0 +1,61 @@
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>> {
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;
public 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);
}
}

Datei anzeigen

@ -0,0 +1,121 @@
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;
public 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 + ">";
}
}

Datei anzeigen

@ -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;
} }
} }

Datei anzeigen

@ -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);
}

Datei anzeigen

@ -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() + '\''
+ '}';
}
}

Datei anzeigen

@ -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()) + '\''
+ '}';
}
}

Datei anzeigen

@ -0,0 +1,44 @@
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);
}
}

Datei anzeigen

@ -0,0 +1,46 @@
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 BrigadierCommand command, final CommandMeta meta) {
// 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;
}
}

Datei anzeigen

@ -0,0 +1,33 @@
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 command the command to register
* @param meta the command metadata, including the case-insensitive aliases
* @throws IllegalArgumentException if the given command cannot be registered
*/
void register(final T command, final CommandMeta meta);
/**
* Returns the superclass or superinterface of all {@link Command} classes
* compatible with this registrar. Note that {@link #register(Command, CommandMeta)}
* may impose additional restrictions on individual {@link Command} instances.
*
* @return the superclass of all the classes compatible with this registrar
*/
Class<T> registrableSuperInterface();
}

Datei anzeigen

@ -0,0 +1,105 @@
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 T command, final CommandMeta meta) {
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;
}
}

Datei anzeigen

@ -0,0 +1,24 @@
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;
}
}

Datei anzeigen

@ -0,0 +1,24 @@
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;
}
}

Datei anzeigen

@ -29,6 +29,7 @@ import com.velocitypowered.api.event.connection.PluginMessageEvent;
import com.velocitypowered.api.network.ProtocolVersion; import com.velocitypowered.api.network.ProtocolVersion;
import com.velocitypowered.api.proxy.messages.ChannelIdentifier; import com.velocitypowered.api.proxy.messages.ChannelIdentifier;
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;
@ -201,18 +202,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(
@ -225,50 +216,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) {

Datei anzeigen

@ -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();
}
}

Datei anzeigen

@ -0,0 +1,53 @@
package com.velocitypowered.proxy.command;
import com.mojang.brigadier.CommandDispatcher;
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
public class CommandGraphInjectorTests {
private CommandDispatcher<Object> dispatcher;
private Lock lock;
private CommandGraphInjector<Object> injector;
@BeforeEach
void setUp() {
this.dispatcher = new CommandDispatcher<>();
this.lock = new ReentrantLock();
this.injector = new CommandGraphInjector<>(this.dispatcher, this.lock);
}
// TODO
@Test
void testInjectInvocableCommand() {
// Preserves arguments node and hints
}
@Test
void testFiltersImpermissibleAlias() {
}
@Test
void testInjectsBrigadierCommand() {
}
@Test
void testFiltersImpermissibleBrigadierCommandChildren() {
}
@Test
void testInjectFiltersBrigadierCommandRedirects() {
}
@Test
void testInjectOverridesAliasInDestination() {
}
}

Datei anzeigen

@ -0,0 +1,5 @@
package com.velocitypowered.proxy.command;
public class SuggestionsProviderTests {
// TODO
}

Datei anzeigen

@ -0,0 +1,94 @@
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());
}
}

Datei anzeigen

@ -0,0 +1,81 @@
package com.velocitypowered.proxy.command.brigadier;
import static com.velocitypowered.proxy.command.brigadier.VelocityArgumentBuilder.velocityArgument;
import static org.junit.jupiter.api.Assertions.*;
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());
}
}