From b4e62443c99faaba599bfdc285dbe2fbadf303bf Mon Sep 17 00:00:00 2001 From: Andrew Steinborn Date: Mon, 5 Aug 2019 10:30:55 -0400 Subject: [PATCH] Add arbitrary chat tab complete event. Fixes #236 --- .../api/event/player/TabCompleteEvent.java | 52 +++++++++++ .../client/ClientPlaySessionHandler.java | 86 +++++++++++++------ .../protocol/packet/TabCompleteResponse.java | 4 + 3 files changed, 117 insertions(+), 25 deletions(-) create mode 100644 api/src/main/java/com/velocitypowered/api/event/player/TabCompleteEvent.java diff --git a/api/src/main/java/com/velocitypowered/api/event/player/TabCompleteEvent.java b/api/src/main/java/com/velocitypowered/api/event/player/TabCompleteEvent.java new file mode 100644 index 000000000..ecfbded01 --- /dev/null +++ b/api/src/main/java/com/velocitypowered/api/event/player/TabCompleteEvent.java @@ -0,0 +1,52 @@ +package com.velocitypowered.api.event.player; + +import static com.google.common.base.Preconditions.checkNotNull; + +import com.velocitypowered.api.proxy.Player; +import java.util.ArrayList; +import java.util.List; + +/** + * This event is fired after a tab complete response is sent by the remote server, for clients on + * 1.12.2 and below. You have the opportunity to modify the response sent to the remote player. + */ +public class TabCompleteEvent { + private final Player player; + private final String partialMessage; + private final List suggestions; + + public TabCompleteEvent(Player player, String partialMessage, List suggestions) { + this.player = checkNotNull(player, "player"); + this.partialMessage = checkNotNull(partialMessage, "partialMessage"); + this.suggestions = new ArrayList<>(checkNotNull(suggestions, "suggestions")); + } + + /** + * Returns the player requesting the tab completion. + * @return the requesting player + */ + public Player getPlayer() { + return player; + } + + /** + * Returns the message being partially completed. + * @return + */ + public String getPartialMessage() { + return partialMessage; + } + + public List getSuggestions() { + return suggestions; + } + + @Override + public String toString() { + return "TabCompleteEvent{" + + "player=" + player + + ", partialMessage='" + partialMessage + '\'' + + ", suggestions=" + suggestions + + '}'; + } +} 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 69d5ec3a3..103389abb 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 @@ -6,6 +6,7 @@ import static com.velocitypowered.proxy.protocol.util.PluginMessageUtil.construc import com.velocitypowered.api.event.connection.PluginMessageEvent; import com.velocitypowered.api.event.player.PlayerChatEvent; import com.velocitypowered.api.event.player.PlayerResourcePackStatusEvent; +import com.velocitypowered.api.event.player.TabCompleteEvent; import com.velocitypowered.api.network.ProtocolVersion; import com.velocitypowered.api.proxy.messages.ChannelIdentifier; import com.velocitypowered.proxy.VelocityServer; @@ -35,12 +36,11 @@ import io.netty.util.ReferenceCountUtil; import java.util.ArrayDeque; import java.util.ArrayList; import java.util.Collection; -import java.util.HashSet; import java.util.List; import java.util.Optional; import java.util.Queue; -import java.util.Set; import java.util.UUID; +import net.kyori.text.Component; import net.kyori.text.TextComponent; import net.kyori.text.format.TextColor; import org.apache.logging.log4j.LogManager; @@ -60,7 +60,7 @@ public class ClientPlaySessionHandler implements MinecraftSessionHandler { private final List serverBossBars = new ArrayList<>(); private final Queue loginPluginMessages = new ArrayDeque<>(); private final VelocityServer server; - private @Nullable TabCompleteRequest legacyCommandTabComplete; + private @Nullable TabCompleteRequest outstandingTabComplete; /** * Constructs a client play session handler. @@ -156,25 +156,27 @@ public class ClientPlaySessionHandler implements MinecraftSessionHandler { public boolean handle(TabCompleteRequest packet) { boolean isCommand = !packet.isAssumeCommand() && packet.getCommand().startsWith("/"); - if (!isCommand) { - // We can't deal with anything else. - return false; + if (isCommand) { + return this.handleTabCompleteForCommand(packet); + } else { + return this.handleRegularTabComplete(packet); } + } + private boolean handleTabCompleteForCommand(TabCompleteRequest packet) { // In 1.13+, we need to do additional work for the richer suggestions available. String command = packet.getCommand().substring(1); int spacePos = command.indexOf(' '); if (spacePos == -1) { - return false; + spacePos = command.length(); } String commandLabel = command.substring(0, spacePos); if (!server.getCommandManager().hasCommand(commandLabel)) { if (player.getProtocolVersion().compareTo(MINECRAFT_1_13) < 0) { // Outstanding tab completes are recorded for use with 1.12 clients and below to provide - // tab list completion support for command names. In 1.13, Brigadier handles everything for - // us. - legacyCommandTabComplete = packet; + // additional tab completion support. + outstandingTabComplete = packet; } return false; } @@ -213,6 +215,15 @@ public class ClientPlaySessionHandler implements MinecraftSessionHandler { return true; } + private boolean handleRegularTabComplete(TabCompleteRequest packet) { + if (player.getProtocolVersion().compareTo(MINECRAFT_1_13) < 0) { + // Outstanding tab completes are recorded for use with 1.12 clients and below to provide + // additional tab completion support. + outstandingTabComplete = packet; + } + return false; + } + @Override public boolean handle(PluginMessage packet) { VelocityServerConnection serverConn = player.getConnectedServer(); @@ -412,28 +423,53 @@ public class ClientPlaySessionHandler implements MinecraftSessionHandler { } /** - * Handles additional tab complete for 1.12 and lower clients. + * Handles additional tab complete. * * @param response the tab complete response from the backend */ public void handleTabCompleteResponse(TabCompleteResponse response) { - if (legacyCommandTabComplete != null) { - String command = legacyCommandTabComplete.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); - } catch (Exception e) { - logger.error("Unable to provide tab list completions for {} for command '{}'", - player.getUsername(), - command, e); + if (outstandingTabComplete != null) { + if (outstandingTabComplete.isAssumeCommand()) { + return; // used for command blocks which can't run Velocity commands anyway } - legacyCommandTabComplete = null; + if (outstandingTabComplete.getCommand().startsWith("/")) { + this.finishCommandTabComplete(outstandingTabComplete, response); + } else { + this.finishRegularTabComplete(outstandingTabComplete, response); + } + outstandingTabComplete = null; } + } - player.getMinecraftConnection().write(response); + 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.getMinecraftConnection().write(response); + } catch (Exception e) { + logger.error("Unable to provide tab list completions for {} for command '{}'", + player.getUsername(), + command, e); + } + } + + private void finishRegularTabComplete(TabCompleteRequest request, TabCompleteResponse response) { + List offers = new ArrayList<>(); + for (Offer offer : response.getOffers()) { + offers.add(offer.getText()); + } + server.getEventManager().fire(new TabCompleteEvent(player, request.getCommand(), offers)) + .thenAcceptAsync(e -> { + response.getOffers().clear(); + for (String s : e.getSuggestions()) { + response.getOffers().add(new Offer(s)); + } + player.getMinecraftConnection().write(response); + }, player.getMinecraftConnection().eventLoop()); } /** diff --git a/proxy/src/main/java/com/velocitypowered/proxy/protocol/packet/TabCompleteResponse.java b/proxy/src/main/java/com/velocitypowered/proxy/protocol/packet/TabCompleteResponse.java index c6db498d2..3975b3235 100644 --- a/proxy/src/main/java/com/velocitypowered/proxy/protocol/packet/TabCompleteResponse.java +++ b/proxy/src/main/java/com/velocitypowered/proxy/protocol/packet/TabCompleteResponse.java @@ -133,5 +133,9 @@ public class TabCompleteResponse implements MinecraftPacket { public int compareTo(Offer o) { return this.text.compareTo(o.text); } + + public String getText() { + return text; + } } }