From 9c8c851d12206a80e51ef58ff0001d0919f89e57 Mon Sep 17 00:00:00 2001 From: Mariell Hoversholm Date: Tue, 14 Jul 2020 23:55:34 +0200 Subject: [PATCH] Add async command suggestions --- .../velocitypowered/api/command/Command.java | 13 ++++ .../api/command/RawCommand.java | 13 +++- .../proxy/command/VelocityCommandManager.java | 17 ++--- .../client/ClientPlaySessionHandler.java | 64 ++++++++++--------- .../proxy/console/VelocityConsole.java | 3 +- 5 files changed, 68 insertions(+), 42 deletions(-) diff --git a/api/src/main/java/com/velocitypowered/api/command/Command.java b/api/src/main/java/com/velocitypowered/api/command/Command.java index f9a077262..87e650125 100644 --- a/api/src/main/java/com/velocitypowered/api/command/Command.java +++ b/api/src/main/java/com/velocitypowered/api/command/Command.java @@ -2,6 +2,7 @@ package com.velocitypowered.api.command; import com.google.common.collect.ImmutableList; import java.util.List; +import java.util.concurrent.CompletableFuture; import org.checkerframework.checker.nullness.qual.NonNull; /** @@ -29,6 +30,18 @@ public interface Command { return ImmutableList.of(); } + /** + * Provides tab complete suggestions for a command for a specified {@link CommandSource}. + * + * @param source the source to run the command for + * @param currentArgs the current, partial arguments for this command + * @return tab complete suggestions + */ + default CompletableFuture> suggestAsync(CommandSource source, + String @NonNull [] currentArgs) { + return CompletableFuture.completedFuture(suggest(source, currentArgs)); + } + /** * Tests to check if the {@code source} has permission to use this command with the provided * {@code args}. diff --git a/api/src/main/java/com/velocitypowered/api/command/RawCommand.java b/api/src/main/java/com/velocitypowered/api/command/RawCommand.java index c1d1c7499..8fcf84bcf 100644 --- a/api/src/main/java/com/velocitypowered/api/command/RawCommand.java +++ b/api/src/main/java/com/velocitypowered/api/command/RawCommand.java @@ -2,6 +2,7 @@ package com.velocitypowered.api.command; import com.google.common.collect.ImmutableList; import java.util.List; +import java.util.concurrent.CompletableFuture; import org.checkerframework.checker.nullness.qual.NonNull; /** @@ -29,13 +30,19 @@ public interface RawCommand extends Command { * @param currentLine the current, partial command line for this command * @return tab complete suggestions */ - default List suggest(CommandSource source, String currentLine) { - return ImmutableList.of(); + default CompletableFuture> suggest(CommandSource source, String currentLine) { + return CompletableFuture.completedFuture(ImmutableList.of()); + } + + @Override + default CompletableFuture> suggestAsync(CommandSource source, + String @NonNull [] currentArgs) { + return suggest(source, String.join(" ", currentArgs)); } @Override default List suggest(CommandSource source, String @NonNull [] currentArgs) { - return suggest(source, String.join(" ", currentArgs)); + return suggestAsync(source, currentArgs).join(); } @Override diff --git a/proxy/src/main/java/com/velocitypowered/proxy/command/VelocityCommandManager.java b/proxy/src/main/java/com/velocitypowered/proxy/command/VelocityCommandManager.java index 9a90c2841..86d2f75f9 100644 --- a/proxy/src/main/java/com/velocitypowered/proxy/command/VelocityCommandManager.java +++ b/proxy/src/main/java/com/velocitypowered/proxy/command/VelocityCommandManager.java @@ -157,9 +157,9 @@ public class VelocityCommandManager implements CommandManager { * Offer suggestions to fill in the command. * @param source the source for the command * @param cmdLine the partially completed command - * @return a {@link List}, possibly empty + * @return a {@link CompletableFuture} eventually completed with a {@link List}, possibly empty */ - public List offerSuggestions(CommandSource source, String cmdLine) { + public CompletableFuture> offerSuggestions(CommandSource source, String cmdLine) { Preconditions.checkNotNull(source, "source"); Preconditions.checkNotNull(cmdLine, "cmdLine"); @@ -173,7 +173,7 @@ public class VelocityCommandManager implements CommandManager { availableCommands.add("/" + entry.getKey()); } } - return availableCommands.build(); + return CompletableFuture.completedFuture(availableCommands.build()); } String alias = cmdLine.substring(0, firstSpace); @@ -181,14 +181,15 @@ public class VelocityCommandManager implements CommandManager { RawCommand command = commands.get(alias.toLowerCase(Locale.ENGLISH)); if (command == null) { // No such command, so we can't offer any tab complete suggestions. - return ImmutableList.of(); + return CompletableFuture.completedFuture(ImmutableList.of()); } try { if (!command.hasPermission(source, args)) { - return ImmutableList.of(); + return CompletableFuture.completedFuture(ImmutableList.of()); } - return ImmutableList.copyOf(command.suggest(source, args)); + return command.suggest(source, args) + .thenApply(ImmutableList::copyOf); } catch (Exception e) { throw new RuntimeException( "Unable to invoke suggestions for command " + cmdLine + " for " + source, e); @@ -253,8 +254,8 @@ public class VelocityCommandManager implements CommandManager { } @Override - public List suggest(CommandSource source, String currentLine) { - return delegate.suggest(source, split(currentLine)); + public CompletableFuture> suggest(CommandSource source, String currentLine) { + return delegate.suggestAsync(source, split(currentLine)); } @Override diff --git a/proxy/src/main/java/com/velocitypowered/proxy/connection/client/ClientPlaySessionHandler.java b/proxy/src/main/java/com/velocitypowered/proxy/connection/client/ClientPlaySessionHandler.java index 5dee998dc..bacb098e1 100644 --- a/proxy/src/main/java/com/velocitypowered/proxy/connection/client/ClientPlaySessionHandler.java +++ b/proxy/src/main/java/com/velocitypowered/proxy/connection/client/ClientPlaySessionHandler.java @@ -397,26 +397,28 @@ public class ClientPlaySessionHandler implements MinecraftSessionHandler { return false; } - List suggestions = server.getCommandManager().offerSuggestions(player, command); - if (suggestions.isEmpty()) { - return false; - } + server.getCommandManager().offerSuggestions(player, command) + .thenAcceptAsync(suggestions -> { + if (suggestions.isEmpty()) { + return; + } - List offers = new ArrayList<>(); - for (String suggestion : suggestions) { - offers.add(new Offer(suggestion)); - } + List offers = new ArrayList<>(); + for (String suggestion : suggestions) { + offers.add(new Offer(suggestion)); + } - int startPos = packet.getCommand().lastIndexOf(' ') + 1; - if (startPos > 0) { - TabCompleteResponse resp = new TabCompleteResponse(); - resp.setTransactionId(packet.getTransactionId()); - resp.setStart(startPos); - resp.setLength(packet.getCommand().length() - startPos); - resp.getOffers().addAll(offers); - player.getConnection().write(resp); - } - return true; + int startPos = packet.getCommand().lastIndexOf(' ') + 1; + if (startPos > 0) { + TabCompleteResponse resp = new TabCompleteResponse(); + resp.setTransactionId(packet.getTransactionId()); + resp.setStart(startPos); + resp.setLength(packet.getCommand().length() - startPos); + resp.getOffers().addAll(offers); + player.getConnection().write(resp); + } + }, player.getConnection().eventLoop()); + return true; // Sorry, handler; we're just gonna have to lie to you here. } private boolean handleRegularTabComplete(TabCompleteRequest packet) { @@ -449,18 +451,20 @@ public class ClientPlaySessionHandler implements MinecraftSessionHandler { private void finishCommandTabComplete(TabCompleteRequest request, TabCompleteResponse response) { String command = request.getCommand().substring(1); - try { - List offers = server.getCommandManager().offerSuggestions(player, command); - for (String offer : offers) { - response.getOffers().add(new Offer(offer, null)); - } - response.getOffers().sort(null); - player.getConnection().write(response); - } catch (Exception e) { - logger.error("Unable to provide tab list completions for {} for command '{}'", - player.getUsername(), - command, e); - } + server.getCommandManager().offerSuggestions(player, command) + .thenAcceptAsync(offers -> { + try { + for (String offer : offers) { + response.getOffers().add(new Offer(offer, null)); + } + response.getOffers().sort(null); + player.getConnection().write(response); + } catch (Exception e) { + logger.error("Unable to provide tab list completions for {} for command '{}'", + player.getUsername(), + command, e); + } + }, player.getConnection().eventLoop()); } private void finishRegularTabComplete(TabCompleteRequest request, TabCompleteResponse response) { diff --git a/proxy/src/main/java/com/velocitypowered/proxy/console/VelocityConsole.java b/proxy/src/main/java/com/velocitypowered/proxy/console/VelocityConsole.java index 49e1afc73..4468122ff 100644 --- a/proxy/src/main/java/com/velocitypowered/proxy/console/VelocityConsole.java +++ b/proxy/src/main/java/com/velocitypowered/proxy/console/VelocityConsole.java @@ -73,7 +73,8 @@ public final class VelocityConsole extends SimpleTerminalConsole implements Cons try { boolean isCommand = parsedLine.line().indexOf(' ') == -1; List offers = this.server.getCommandManager() - .offerSuggestions(this, parsedLine.line()); + .offerSuggestions(this, parsedLine.line()) + .join(); // Console doesn't get harmed much by this... for (String offer : offers) { if (isCommand) { list.add(new Candidate(offer.substring(1)));