diff --git a/proxy/src/main/java/com/velocitypowered/proxy/command/CommandManager.java b/proxy/src/main/java/com/velocitypowered/proxy/command/CommandManager.java index 60eeed2ab..a9871a08d 100644 --- a/proxy/src/main/java/com/velocitypowered/proxy/command/CommandManager.java +++ b/proxy/src/main/java/com/velocitypowered/proxy/command/CommandManager.java @@ -5,10 +5,7 @@ import com.google.common.collect.ImmutableList; import com.velocitypowered.api.command.CommandExecutor; import com.velocitypowered.api.command.CommandInvoker; -import java.util.Arrays; -import java.util.HashMap; -import java.util.List; -import java.util.Map; +import java.util.*; import java.util.stream.Collectors; public class CommandManager { @@ -49,30 +46,30 @@ public class CommandManager { } } - public List offerSuggestions(CommandInvoker invoker, String cmdLine) { + public Optional> offerSuggestions(CommandInvoker invoker, String cmdLine) { Preconditions.checkNotNull(invoker, "invoker"); Preconditions.checkNotNull(cmdLine, "cmdLine"); String[] split = cmdLine.split(" ", -1); if (split.length == 0) { - return ImmutableList.of(); + return Optional.empty(); } String command = split[0]; if (split.length == 1) { - return executors.keySet().stream() + return Optional.of(executors.keySet().stream() .filter(cmd -> cmd.regionMatches(true, 0, command, 0, command.length())) - .collect(Collectors.toList()); + .collect(Collectors.toList())); } String[] actualArgs = Arrays.copyOfRange(split, 1, split.length); CommandExecutor executor = executors.get(command); if (executor == null) { - return ImmutableList.of(); + return Optional.empty(); } try { - return executor.suggest(invoker, actualArgs); + return Optional.of(executor.suggest(invoker, actualArgs)); } catch (Exception e) { throw new RuntimeException("Unable to invoke suggestions for command " + command + " for " + invoker, e); } 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 cecc1c80f..7aa02bd6a 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 @@ -95,6 +95,34 @@ public class ClientPlaySessionHandler implements MinecraftSessionHandler { return; } + if (packet instanceof TabCompleteRequest) { + TabCompleteRequest req = (TabCompleteRequest) packet; + int lastSpace = req.getCommand().indexOf(' '); + if (!req.isAssumeCommand() && lastSpace != -1) { + String command = req.getCommand().substring(1); + try { + Optional> offers = VelocityServer.getServer().getCommandManager().offerSuggestions(player, command); + if (offers.isPresent()) { + TabCompleteResponse response = new TabCompleteResponse(); + response.setTransactionId(req.getTransactionId()); + response.setStart(lastSpace); + response.setLength(req.getCommand().length() - lastSpace); + for (String s : offers.get()) { + response.getOffers().add(new TabCompleteResponse.Offer(s, null)); + } + player.getConnection().write(response); + return; + } + } catch (Exception e) { + logger.error("Unable to provide tab list completions for " + player.getUsername() + " for command '" + req.getCommand() + "'", e); + TabCompleteResponse response = new TabCompleteResponse(); + response.setTransactionId(req.getTransactionId()); + player.getConnection().write(response); + return; + } + } + } + if (packet instanceof PluginMessage) { handleClientPluginMessage((PluginMessage) packet); return; 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 835996148..4c46764fa 100644 --- a/proxy/src/main/java/com/velocitypowered/proxy/console/VelocityConsole.java +++ b/proxy/src/main/java/com/velocitypowered/proxy/console/VelocityConsole.java @@ -7,6 +7,7 @@ import net.minecrell.terminalconsole.SimpleTerminalConsole; import org.jline.reader.*; import java.util.List; +import java.util.Optional; public final class VelocityConsole extends SimpleTerminalConsole { @@ -21,10 +22,12 @@ public final class VelocityConsole extends SimpleTerminalConsole { return super.buildReader(builder .appName("Velocity") .completer((reader, parsedLine, list) -> { - List offers = server.getCommandManager().offerSuggestions(server.getConsoleCommandInvoker(), parsedLine.line()); - for (String offer : offers) { - if (offer.isEmpty()) continue; - list.add(new Candidate(offer)); + Optional> offers = server.getCommandManager().offerSuggestions(server.getConsoleCommandInvoker(), parsedLine.line()); + if (offers.isPresent()) { + for (String offer : offers.get()) { + if (offer.isEmpty()) continue; + list.add(new Candidate(offer)); + } } }) ); diff --git a/proxy/src/main/java/com/velocitypowered/proxy/protocol/StateRegistry.java b/proxy/src/main/java/com/velocitypowered/proxy/protocol/StateRegistry.java index 80d0d842c..fea896527 100644 --- a/proxy/src/main/java/com/velocitypowered/proxy/protocol/StateRegistry.java +++ b/proxy/src/main/java/com/velocitypowered/proxy/protocol/StateRegistry.java @@ -32,6 +32,9 @@ public enum StateRegistry { }, PLAY { { + SERVERBOUND.register(TabCompleteRequest.class, TabCompleteRequest::new, + map(0x01, MINECRAFT_1_12_2), + map(0x05, MINECRAFT_1_13)); SERVERBOUND.register(Chat.class, Chat::new, map(0x01, MINECRAFT_1_8), map(0x02, MINECRAFT_1_9), @@ -65,6 +68,9 @@ public enum StateRegistry { map(0x0F, MINECRAFT_1_9), map(0x0F, MINECRAFT_1_12), map(0x0E, MINECRAFT_1_13)); + CLIENTBOUND.register(TabCompleteResponse.class, TabCompleteResponse::new, + map(0x0E, MINECRAFT_1_12), + map(0x10, MINECRAFT_1_13)); CLIENTBOUND.register(PluginMessage.class, PluginMessage::new, map(0x3F, MINECRAFT_1_8), map(0x18, MINECRAFT_1_9), diff --git a/proxy/src/main/java/com/velocitypowered/proxy/protocol/packet/TabCompleteRequest.java b/proxy/src/main/java/com/velocitypowered/proxy/protocol/packet/TabCompleteRequest.java new file mode 100644 index 000000000..94d80b66e --- /dev/null +++ b/proxy/src/main/java/com/velocitypowered/proxy/protocol/packet/TabCompleteRequest.java @@ -0,0 +1,97 @@ +package com.velocitypowered.proxy.protocol.packet; + +import com.velocitypowered.proxy.protocol.MinecraftPacket; +import com.velocitypowered.proxy.protocol.ProtocolConstants; +import com.velocitypowered.proxy.protocol.ProtocolUtils; +import io.netty.buffer.ByteBuf; + +import static com.velocitypowered.proxy.protocol.ProtocolConstants.MINECRAFT_1_13; + +public class TabCompleteRequest implements MinecraftPacket { + private int transactionId; + private String command; + private boolean assumeCommand; + private boolean hasPosition; + private long position; + + public int getTransactionId() { + return transactionId; + } + + public void setTransactionId(int transactionId) { + this.transactionId = transactionId; + } + + public String getCommand() { + return command; + } + + public void setCommand(String command) { + this.command = command; + } + + public boolean isAssumeCommand() { + return assumeCommand; + } + + public void setAssumeCommand(boolean assumeCommand) { + this.assumeCommand = assumeCommand; + } + + public boolean isHasPosition() { + return hasPosition; + } + + public void setHasPosition(boolean hasPosition) { + this.hasPosition = hasPosition; + } + + public long getPosition() { + return position; + } + + public void setPosition(long position) { + this.position = position; + } + + @Override + public String toString() { + return "TabCompleteRequest{" + + "transactionId=" + transactionId + + ", command='" + command + '\'' + + ", assumeCommand=" + assumeCommand + + ", hasPosition=" + hasPosition + + ", position=" + position + + '}'; + } + + @Override + public void decode(ByteBuf buf, ProtocolConstants.Direction direction, int protocolVersion) { + if (protocolVersion >= MINECRAFT_1_13) { + this.transactionId = ProtocolUtils.readVarInt(buf); + this.command = ProtocolUtils.readString(buf); + } else { + this.command = ProtocolUtils.readString(buf); + this.assumeCommand = buf.readBoolean(); + this.hasPosition = buf.readBoolean(); + if (hasPosition) { + this.position = buf.readLong(); + } + } + } + + @Override + public void encode(ByteBuf buf, ProtocolConstants.Direction direction, int protocolVersion) { + if (protocolVersion >= MINECRAFT_1_13) { + ProtocolUtils.writeVarInt(buf, transactionId); + ProtocolUtils.writeString(buf, command); + } else { + ProtocolUtils.writeString(buf, command); + buf.writeBoolean(assumeCommand); + buf.writeBoolean(hasPosition); + if (hasPosition) { + buf.writeLong(position); + } + } + } +} 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 new file mode 100644 index 000000000..df80fb626 --- /dev/null +++ b/proxy/src/main/java/com/velocitypowered/proxy/protocol/packet/TabCompleteResponse.java @@ -0,0 +1,128 @@ +package com.velocitypowered.proxy.protocol.packet; + +import com.velocitypowered.proxy.protocol.MinecraftPacket; +import com.velocitypowered.proxy.protocol.ProtocolConstants; +import com.velocitypowered.proxy.protocol.ProtocolUtils; +import io.netty.buffer.ByteBuf; +import net.kyori.text.Component; +import net.kyori.text.TextComponent; +import net.kyori.text.serializer.ComponentSerializers; + +import java.util.ArrayList; +import java.util.List; + +import static com.velocitypowered.proxy.protocol.ProtocolConstants.MINECRAFT_1_13; + +public class TabCompleteResponse implements MinecraftPacket { + private int transactionId; + private int start; + private int length; + private final List offers = new ArrayList<>(); + + public int getTransactionId() { + return transactionId; + } + + public void setTransactionId(int transactionId) { + this.transactionId = transactionId; + } + + public int getStart() { + return start; + } + + public void setStart(int start) { + this.start = start; + } + + public int getLength() { + return length; + } + + public void setLength(int length) { + this.length = length; + } + + public List getOffers() { + return offers; + } + + @Override + public String toString() { + return "TabCompleteResponse{" + + "transactionId=" + transactionId + + ", start=" + start + + ", length=" + length + + ", offers=" + offers + + '}'; + } + + @Override + public void decode(ByteBuf buf, ProtocolConstants.Direction direction, int protocolVersion) { + if (protocolVersion >= MINECRAFT_1_13) { + this.transactionId = ProtocolUtils.readVarInt(buf); + this.start = ProtocolUtils.readVarInt(buf); + this.length = ProtocolUtils.readVarInt(buf); + int offersAvailable = ProtocolUtils.readVarInt(buf); + for (int i = 0; i < offersAvailable; i++) { + String entry = ProtocolUtils.readString(buf); + Component component = buf.readBoolean() ? ComponentSerializers.JSON.deserialize(ProtocolUtils.readString(buf)) : + null; + offers.add(new Offer(entry, component)); + } + } else { + int offersAvailable = ProtocolUtils.readVarInt(buf); + for (int i = 0; i < offersAvailable; i++) { + offers.add(new Offer(ProtocolUtils.readString(buf), null)); + } + } + } + + @Override + public void encode(ByteBuf buf, ProtocolConstants.Direction direction, int protocolVersion) { + if (protocolVersion >= MINECRAFT_1_13) { + ProtocolUtils.writeVarInt(buf, transactionId); + ProtocolUtils.writeVarInt(buf, start); + ProtocolUtils.writeVarInt(buf, length); + ProtocolUtils.writeVarInt(buf, offers.size()); + for (Offer offer : offers) { + ProtocolUtils.writeString(buf, offer.entry); + buf.writeBoolean(offer.tooltip != null); + if (offer.tooltip != null) { + ProtocolUtils.writeString(buf, ComponentSerializers.JSON.serialize(offer.tooltip)); + } + } + } else { + ProtocolUtils.writeVarInt(buf, offers.size()); + for (Offer offer : offers) { + ProtocolUtils.writeString(buf, offer.entry); + } + } + } + + public static class Offer { + private final String entry; + private final Component tooltip; + + public Offer(String entry, Component tooltip) { + this.entry = entry; + this.tooltip = tooltip; + } + + public String getEntry() { + return entry; + } + + public Component getTooltip() { + return tooltip; + } + + @Override + public String toString() { + return "Offer{" + + "entry='" + entry + '\'' + + ", tooltip=" + tooltip + + '}'; + } + } +}