diff --git a/api/src/main/java/com/velocitypowered/api/command/VelocityBrigadierMessage.java b/api/src/main/java/com/velocitypowered/api/command/VelocityBrigadierMessage.java new file mode 100644 index 000000000..3e9e3dde5 --- /dev/null +++ b/api/src/main/java/com/velocitypowered/api/command/VelocityBrigadierMessage.java @@ -0,0 +1,51 @@ +/* + * Copyright (C) 2018 Velocity Contributors + * + * The Velocity API is licensed under the terms of the MIT License. For more details, + * reference the LICENSE file in the api top-level directory. + */ + +package com.velocitypowered.api.command; + +import com.google.common.base.Preconditions; +import com.mojang.brigadier.Message; +import net.kyori.adventure.text.Component; +import net.kyori.adventure.text.ComponentLike; +import net.kyori.adventure.text.serializer.plain.PlainTextComponentSerializer; +import org.jetbrains.annotations.NotNull; + +/** + * Represents an implementation of brigadier's {@link Message}, providing support for {@link + * Component} messages. + */ +public final class VelocityBrigadierMessage implements Message, ComponentLike { + + public static VelocityBrigadierMessage tooltip(Component message) { + return new VelocityBrigadierMessage(message); + } + + private final Component message; + + private VelocityBrigadierMessage(Component message) { + this.message = Preconditions.checkNotNull(message, "message"); + } + + /** + * {@inheritDoc} + */ + @Override + @NotNull + public Component asComponent() { + return message; + } + + /** + * Returns the message as a plain text. + * + * @return message as plain text + */ + @Override + public String getString() { + return PlainTextComponentSerializer.plainText().serialize(message); + } +} 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 2feb88dce..7baa22fe7 100644 --- a/proxy/src/main/java/com/velocitypowered/proxy/command/VelocityCommandManager.java +++ b/proxy/src/main/java/com/velocitypowered/proxy/command/VelocityCommandManager.java @@ -24,9 +24,9 @@ import com.mojang.brigadier.CommandDispatcher; import com.mojang.brigadier.ParseResults; import com.mojang.brigadier.exceptions.CommandSyntaxException; import com.mojang.brigadier.suggestion.Suggestion; +import com.mojang.brigadier.suggestion.Suggestions; import com.mojang.brigadier.tree.CommandNode; import com.mojang.brigadier.tree.RootCommandNode; -import com.spotify.futures.CompletableFutures; import com.velocitypowered.api.command.BrigadierCommand; import com.velocitypowered.api.command.Command; import com.velocitypowered.api.command.CommandManager; @@ -47,7 +47,6 @@ import java.util.concurrent.CompletableFuture; import java.util.concurrent.ConcurrentHashMap; import java.util.concurrent.locks.ReadWriteLock; import java.util.concurrent.locks.ReentrantReadWriteLock; -import java.util.stream.Collectors; import net.kyori.adventure.identity.Identity; import net.kyori.adventure.text.Component; import net.kyori.adventure.text.format.NamedTextColor; @@ -259,14 +258,26 @@ public class VelocityCommandManager implements CommandManager { */ public CompletableFuture> offerSuggestions(final CommandSource source, final String cmdLine) { + return offerBrigadierSuggestions(source, cmdLine) + .thenApply(suggestions -> Lists.transform(suggestions.getList(), Suggestion::getText)); + } + + /** + * Returns suggestions to fill in the given command. + * + * @param source the source to execute the command for + * @param cmdLine the partially completed command + * @return a {@link CompletableFuture} eventually completed with {@link Suggestions}, + * possibly empty + */ + public CompletableFuture offerBrigadierSuggestions( + final CommandSource source, final String cmdLine) { Preconditions.checkNotNull(source, "source"); Preconditions.checkNotNull(cmdLine, "cmdLine"); final String normalizedInput = VelocityCommands.normalizeInput(cmdLine, false); try { - return suggestionsProvider.provideSuggestions(normalizedInput, source) - .thenApply(suggestions -> - Lists.transform(suggestions.getList(), Suggestion::getText)); + return suggestionsProvider.provideSuggestions(normalizedInput, source); } catch (final Throwable e) { // Again, plugins are naughty return CompletableFuture.failedFuture( 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 217b22bf3..3c36ad33b 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 @@ -23,6 +23,8 @@ import static com.velocitypowered.api.network.ProtocolVersion.MINECRAFT_1_8; import static com.velocitypowered.proxy.protocol.util.PluginMessageUtil.constructChannelsPacket; import com.google.common.collect.ImmutableList; +import com.mojang.brigadier.suggestion.Suggestion; +import com.velocitypowered.api.command.VelocityBrigadierMessage; import com.velocitypowered.api.event.command.CommandExecuteEvent.CommandResult; import com.velocitypowered.api.event.connection.PluginMessageEvent; import com.velocitypowered.api.event.player.PlayerChannelRegisterEvent; @@ -497,15 +499,21 @@ public class ClientPlaySessionHandler implements MinecraftSessionHandler { return false; } - server.getCommandManager().offerSuggestions(player, command) + server.getCommandManager().offerBrigadierSuggestions(player, command) .thenAcceptAsync(suggestions -> { if (suggestions.isEmpty()) { return; } List offers = new ArrayList<>(); - for (String offer : suggestions) { - offers.add(new Offer(offer)); + for (Suggestion suggestion : suggestions.getList()) { + String offer = suggestion.getText(); + Component tooltip = null; + if (suggestion.getTooltip() != null + && suggestion.getTooltip() instanceof VelocityBrigadierMessage) { + tooltip = ((VelocityBrigadierMessage) suggestion.getTooltip()).asComponent(); + } + offers.add(new Offer(offer, tooltip)); } int startPos = packet.getCommand().lastIndexOf(' ') + 1; if (startPos > 0) { @@ -555,16 +563,22 @@ public class ClientPlaySessionHandler implements MinecraftSessionHandler { private void finishCommandTabComplete(TabCompleteRequest request, TabCompleteResponse response) { String command = request.getCommand().substring(1); - server.getCommandManager().offerSuggestions(player, command) + server.getCommandManager().offerBrigadierSuggestions(player, command) .thenAcceptAsync(offers -> { boolean legacy = player.getProtocolVersion().compareTo(MINECRAFT_1_13) < 0; try { - for (String offer : offers) { + for (Suggestion suggestion : offers.getList()) { + String offer = suggestion.getText(); offer = legacy && !offer.startsWith("/") ? "/" + offer : offer; if (legacy && offer.startsWith(command)) { offer = offer.substring(command.length()); } - response.getOffers().add(new Offer(offer, null)); + Component tooltip = null; + if (suggestion.getTooltip() != null + && suggestion.getTooltip() instanceof VelocityBrigadierMessage) { + tooltip = ((VelocityBrigadierMessage) suggestion.getTooltip()).asComponent(); + } + response.getOffers().add(new Offer(offer, tooltip)); } response.getOffers().sort(null); player.getConnection().write(response);