Added new Brigadier commands related APIs for bungee/paper/cli + a few javadoc

This commit is contained in:
2022-08-08 01:42:11 +02:00
parent 2f141d5f84
commit a885c224a6
22 changed files with 1337 additions and 317 deletions

View File

@@ -31,6 +31,16 @@
<artifactId>pandalib-util</artifactId>
<version>${project.version}</version>
</dependency>
<dependency>
<groupId>fr.pandacube.lib</groupId>
<artifactId>pandalib-chat</artifactId>
<version>${project.version}</version>
</dependency>
<dependency>
<groupId>fr.pandacube.lib</groupId>
<artifactId>pandalib-reflect</artifactId>
<version>${project.version}</version>
</dependency>
<dependency>
<groupId>com.mojang</groupId>

View File

@@ -0,0 +1,186 @@
package fr.pandacube.lib.commands;
import com.mojang.brigadier.LiteralMessage;
import com.mojang.brigadier.arguments.ArgumentType;
import com.mojang.brigadier.builder.LiteralArgumentBuilder;
import com.mojang.brigadier.builder.RequiredArgumentBuilder;
import com.mojang.brigadier.context.CommandContext;
import com.mojang.brigadier.context.ParsedCommandNode;
import com.mojang.brigadier.context.StringRange;
import com.mojang.brigadier.exceptions.CommandSyntaxException;
import com.mojang.brigadier.exceptions.SimpleCommandExceptionType;
import com.mojang.brigadier.suggestion.Suggestion;
import com.mojang.brigadier.suggestion.SuggestionProvider;
import com.mojang.brigadier.suggestion.Suggestions;
import com.mojang.brigadier.suggestion.SuggestionsBuilder;
import com.mojang.brigadier.tree.LiteralCommandNode;
import fr.pandacube.lib.reflect.Reflect;
import fr.pandacube.lib.util.Log;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collection;
import java.util.List;
import java.util.concurrent.CompletableFuture;
import java.util.function.Function;
import java.util.function.Predicate;
/**
* Abstract class that holds the logic of a specific command to be integrated in a Brigadier command dispatcher.
* Subclasses may use any mechanism to integrate this command in the environments Brigadier instance, during the
* instantiation of this object.
* @param <S> the command source (or command sender) type.
*/
public abstract class BrigadierCommand<S> {
protected abstract LiteralArgumentBuilder<S> buildCommand();
/**
* Method to implement if the reference to the command node has to be known when building the subcommands.
* @param commandNode the command node builded from {@link #buildCommand()}.
*/
protected void postBuildCommand(LiteralCommandNode<S> commandNode) {
// default implementation does nothing.
}
protected String[] getAliases() {
return new String[0];
}
public LiteralArgumentBuilder<S> literal(String name) {
return LiteralArgumentBuilder.literal(name);
}
public <T> RequiredArgumentBuilder<S, T> argument(String name, ArgumentType<T> type) {
return RequiredArgumentBuilder.argument(name, type);
}
public abstract boolean isPlayer(S sender);
public abstract boolean isConsole(S sender);
public Predicate<S> isPlayer() {
return this::isPlayer;
}
public Predicate<S> isConsole() {
return this::isConsole;
}
public abstract Predicate<S> hasPermission(String permission);
public static boolean isLiteralParsed(CommandContext<?> context, String literal) {
for (ParsedCommandNode<?> node : context.getNodes()) {
if (node.getNode() instanceof LiteralCommandNode<?> lNode
&& lNode.getLiteral().equals(literal))
return true;
}
return false;
}
public static <T> T tryGetArgument(CommandContext<?> context, String argument, Class<T> type) {
return tryGetArgument(context, argument, type, Function.identity(), null);
}
public static <ST, T> T tryGetArgument(CommandContext<?> context, String argument, Class<ST> sourceType, Function<ST, T> transformIfFound) {
return tryGetArgument(context, argument, sourceType, transformIfFound, null);
}
public static <T> T tryGetArgument(CommandContext<?> context, String argument, Class<T> type, T deflt) {
return tryGetArgument(context, argument, type, Function.identity(), deflt);
}
public static <ST, T> T tryGetArgument(CommandContext<?> context, String argument, Class<ST> sourceType, Function<ST, T> transformIfFound, T deflt) {
ST sourceValue;
try {
sourceValue = context.getArgument(argument, sourceType);
} catch (IllegalArgumentException e) {
return deflt;
}
return transformIfFound.apply(sourceValue);
}
public static CommandSyntaxException newCommandException(String message) {
return new SimpleCommandExceptionType(new LiteralMessage(message)).create();
}
protected <AS> SuggestionProvider<S> wrapSuggestions(SuggestionsSupplier<AS> suggestions, Function<S, AS> senderUnwrapper) {
return (context, builder) -> {
AS sender = senderUnwrapper.apply(context.getSource());
String message = builder.getInput();
try {
int tokenStartPos = builder.getStart();
int firstSpacePos = message.indexOf(" ");
String[] args = (firstSpacePos + 1 > tokenStartPos - 1) ? new String[0]
: message.substring(firstSpacePos + 1, tokenStartPos - 1).split(" ", -1);
args = Arrays.copyOf(args, args.length + 1);
args[args.length - 1] = message.substring(tokenStartPos);
for (String s : suggestions.getSuggestions(sender, args.length - 1, args[args.length - 1], args)) {
if (s != null)
builder.suggest(s);
}
} catch (Throwable e) {
Log.severe("Error while tab-completing '" + message + "' for " + sender, e);
}
return BrigadierSuggestionsUtil.completableFutureSuggestionsKeepsOriginalOrdering(builder);
};
}
}

View File

@@ -0,0 +1,73 @@
package fr.pandacube.lib.commands;
import com.mojang.brigadier.CommandDispatcher;
import com.mojang.brigadier.ParseResults;
import com.mojang.brigadier.exceptions.CommandSyntaxException;
import com.mojang.brigadier.suggestion.Suggestions;
import com.mojang.brigadier.tree.LiteralCommandNode;
import fr.pandacube.lib.chat.Chat;
import fr.pandacube.lib.util.Log;
import net.kyori.adventure.text.ComponentLike;
import java.util.concurrent.CompletableFuture;
public abstract class BrigadierDispatcher<S> {
private final CommandDispatcher<S> dispatcher = new CommandDispatcher<>();
public void register(LiteralCommandNode<S> node) {
dispatcher.getRoot().addChild(node);
}
public CommandDispatcher<S> getDispatcher() {
return dispatcher;
}
public int execute(S sender, String commandWithoutSlash) {
ParseResults<S> parsed = dispatcher.parse(commandWithoutSlash, sender);
try {
return dispatcher.execute(parsed);
} catch (CommandSyntaxException e) {
sendSenderMessage(sender, Chat.failureText("Error while using the command: " + e.getMessage()));
return 0;
} catch (Throwable e) {
sendSenderMessage(sender, Chat.failureText("Error while running the command: " + e.getMessage()));
Log.severe(e);
return 0;
}
}
public Suggestions getSuggestions(S sender, String buffer) {
ParseResults<S> parsed = dispatcher.parse(buffer, sender);
try {
CompletableFuture<Suggestions> futureSuggestions = BrigadierSuggestionsUtil.buildSuggestionBrigadier(parsed);
return futureSuggestions.join();
} catch (Throwable e) {
sendSenderMessage(sender, Chat.failureText("Error while generating the suggestions:\n" + e.getMessage()));
Log.severe(e);
return Suggestions.empty().join();
}
}
protected abstract void sendSenderMessage(S sender, ComponentLike message);
}

View File

@@ -0,0 +1,115 @@
package fr.pandacube.lib.commands;
import com.mojang.brigadier.ParseResults;
import com.mojang.brigadier.context.CommandContextBuilder;
import com.mojang.brigadier.context.StringRange;
import com.mojang.brigadier.context.SuggestionContext;
import com.mojang.brigadier.exceptions.CommandSyntaxException;
import com.mojang.brigadier.suggestion.Suggestion;
import com.mojang.brigadier.suggestion.Suggestions;
import com.mojang.brigadier.suggestion.SuggestionsBuilder;
import com.mojang.brigadier.tree.CommandNode;
import fr.pandacube.lib.reflect.Reflect;
import java.util.ArrayList;
import java.util.Collection;
import java.util.List;
import java.util.concurrent.CompletableFuture;
/**
* Utility methods to replace some functionalities of Brigadier, especialy suggestion sorting that we dont like.
*/
public class BrigadierSuggestionsUtil {
public static <S> CompletableFuture<Suggestions> buildSuggestionBrigadier(ParseResults<S> parsed) {
int cursor = parsed.getReader().getTotalLength();
final CommandContextBuilder<S> context = parsed.getContext();
final SuggestionContext<S> nodeBeforeCursor = context.findSuggestionContext(cursor);
final CommandNode<S> parent = nodeBeforeCursor.parent;
final int start = Math.min(nodeBeforeCursor.startPos, cursor);
final String fullInput = parsed.getReader().getString();
final String truncatedInput = fullInput.substring(0, cursor);
@SuppressWarnings("unchecked") final CompletableFuture<Suggestions>[] futures = new CompletableFuture[parent.getChildren().size()];
int i = 0;
for (final CommandNode<S> node : parent.getChildren()) {
CompletableFuture<Suggestions> future = Suggestions.empty();
try {
future = node.listSuggestions(context.build(truncatedInput), new SuggestionsBuilder(truncatedInput, start));
} catch (final CommandSyntaxException ignored) {
}
futures[i++] = future;
}
final CompletableFuture<Suggestions> result = new CompletableFuture<>();
CompletableFuture.allOf(futures).thenRun(() -> {
final List<Suggestions> suggestions = new ArrayList<>();
for (final CompletableFuture<Suggestions> future : futures) {
suggestions.add(future.join());
}
result.complete(mergeSuggestionsOriginalOrdering(fullInput, suggestions));
});
return result;
}
// inspired from com.mojang.brigadier.suggestion.Suggestions#merge, but without the sorting part
public static Suggestions mergeSuggestionsOriginalOrdering(String input, Collection<Suggestions> suggestions) {
if (suggestions.isEmpty()) {
return new Suggestions(StringRange.at(0), new ArrayList<>(0));
} else if (suggestions.size() == 1) {
return suggestions.iterator().next();
}
final List<Suggestion> texts = new ArrayList<>();
for (final Suggestions suggestions1 : suggestions) {
texts.addAll(suggestions1.getList());
}
return createSuggestionsOriginalOrdering(input, texts);
}
// inspired from com.mojang.brigadier.suggestion.Suggestions#create, but without the sorting part
public static Suggestions createSuggestionsOriginalOrdering(String command, Collection<Suggestion> suggestions) {
if (suggestions.isEmpty()) {
return new Suggestions(StringRange.at(0), new ArrayList<>(0));
}
int start = Integer.MAX_VALUE;
int end = Integer.MIN_VALUE;
for (final Suggestion suggestion : suggestions) {
start = Math.min(suggestion.getRange().getStart(), start);
end = Math.max(suggestion.getRange().getEnd(), end);
}
final StringRange range = new StringRange(start, end);
final List<Suggestion> texts = new ArrayList<>(suggestions.size());
for (final Suggestion suggestion : suggestions) {
texts.add(suggestion.expand(command, range));
}
return new Suggestions(range, texts);
}
public static CompletableFuture<Suggestions> completableFutureSuggestionsKeepsOriginalOrdering(SuggestionsBuilder builder) {
return CompletableFuture.completedFuture(
createSuggestionsOriginalOrdering(builder.getInput(), getSuggestionsFromSuggestionsBuilder(builder))
);
}
@SuppressWarnings("unchecked")
private static List<Suggestion> getSuggestionsFromSuggestionsBuilder(SuggestionsBuilder builder) {
try {
return (List<Suggestion>) Reflect.ofClass(SuggestionsBuilder.class).field("result").getValue(builder);
} catch (ReflectiveOperationException e) {
throw new RuntimeException(e);
}
}
}