From 61bd17859153e209f287b850954f76169747cc7f Mon Sep 17 00:00:00 2001 From: Desetude Date: Sun, 30 Sep 2018 20:54:50 +0100 Subject: [PATCH] Initial tablist implementation --- .../com/velocitypowered/api/proxy/Player.java | 7 + .../api/proxy/player/TabList.java | 37 ++++ .../api/proxy/player/TabListEntry.java | 75 +++++++ .../connection/MinecraftSessionHandler.java | 1 + .../backend/BackendPlaySessionHandler.java | 7 + .../backend/LoginSessionHandler.java | 13 +- .../client/ClientPlaySessionHandler.java | 1 + .../connection/client/ConnectedPlayer.java | 20 +- .../proxy/protocol/ProtocolUtils.java | 34 +++ .../proxy/protocol/StateRegistry.java | 6 + .../proxy/protocol/packet/PlayerListItem.java | 196 ++++++++++++++++++ .../proxy/tablist/VelocityTabList.java | 113 ++++++++++ .../proxy/tablist/VelocityTabListEntry.java | 72 +++++++ 13 files changed, 565 insertions(+), 17 deletions(-) create mode 100644 api/src/main/java/com/velocitypowered/api/proxy/player/TabList.java create mode 100644 api/src/main/java/com/velocitypowered/api/proxy/player/TabListEntry.java create mode 100644 proxy/src/main/java/com/velocitypowered/proxy/protocol/packet/PlayerListItem.java create mode 100644 proxy/src/main/java/com/velocitypowered/proxy/tablist/VelocityTabList.java create mode 100644 proxy/src/main/java/com/velocitypowered/proxy/tablist/VelocityTabListEntry.java diff --git a/api/src/main/java/com/velocitypowered/api/proxy/Player.java b/api/src/main/java/com/velocitypowered/api/proxy/Player.java index 8e61d2cc5..4621f500b 100644 --- a/api/src/main/java/com/velocitypowered/api/proxy/Player.java +++ b/api/src/main/java/com/velocitypowered/api/proxy/Player.java @@ -6,6 +6,7 @@ import com.velocitypowered.api.proxy.messages.ChannelMessageSource; import com.velocitypowered.api.proxy.player.PlayerSettings; import com.velocitypowered.api.proxy.server.RegisteredServer; import com.velocitypowered.api.util.GameProfile; +import com.velocitypowered.api.proxy.player.TabList; import com.velocitypowered.api.util.MessagePosition; import com.velocitypowered.api.util.title.Title; import java.util.List; @@ -88,14 +89,20 @@ public interface Player extends CommandSource, InboundConnection, ChannelMessage * Sets the tab list header and footer for the player. * @param header the header component * @param footer the footer component + * @deprecated Use {@link TabList#setHeaderAndFooter(Component, Component)}. */ + @Deprecated void setHeaderAndFooter(Component header, Component footer); /** * Clears the tab list header and footer for the player. + * @deprecated Use {@link TabList#clearHeaderAndFooter()}. */ + @Deprecated void clearHeaderAndFooter(); + TabList getTabList(); + /** * Disconnects the player with the specified reason. Once this method is called, further calls to other {@link Player} * methods will become undefined. diff --git a/api/src/main/java/com/velocitypowered/api/proxy/player/TabList.java b/api/src/main/java/com/velocitypowered/api/proxy/player/TabList.java new file mode 100644 index 000000000..6de6f007a --- /dev/null +++ b/api/src/main/java/com/velocitypowered/api/proxy/player/TabList.java @@ -0,0 +1,37 @@ +package com.velocitypowered.api.proxy.player; + +import com.velocitypowered.api.proxy.Player; +import com.velocitypowered.api.util.GameProfile; +import net.kyori.text.Component; + +import java.util.Collection; +import java.util.Optional; +import java.util.UUID; + +/** + * Represents the tab list of a {@link Player}. + * TODO: Desetude + */ +public interface TabList { + /** + * Sets the tab list header and footer for the player. + * @param header the header component + * @param footer the footer component + */ + void setHeaderAndFooter(Component header, Component footer); + + /** + * Clears the tab list header and footer for the player. + */ + void clearHeaderAndFooter(); + + void addEntry(TabListEntry entry); + + Optional removeEntry(UUID uuid); + + Collection getEntries(); + + //Necessary because the TabListEntry implementation isn't in the api + @Deprecated + TabListEntry buildEntry(GameProfile profile, Component displayName, int latency, int gameMode); +} diff --git a/api/src/main/java/com/velocitypowered/api/proxy/player/TabListEntry.java b/api/src/main/java/com/velocitypowered/api/proxy/player/TabListEntry.java new file mode 100644 index 000000000..b6d567c12 --- /dev/null +++ b/api/src/main/java/com/velocitypowered/api/proxy/player/TabListEntry.java @@ -0,0 +1,75 @@ +package com.velocitypowered.api.proxy.player; + +import com.google.common.base.Preconditions; +import com.velocitypowered.api.util.GameProfile; +import net.kyori.text.Component; +import org.checkerframework.checker.nullness.qual.Nullable; + +import java.util.Optional; + +/** + * TODO: Desetude + */ +public interface TabListEntry { + TabList getTabList(); + + GameProfile getProfile(); + + Optional getDisplayName(); + + TabListEntry setDisplayName(@Nullable Component displayName); + + int getLatency(); + + TabListEntry setLatency(int latency); + + int getGameMode(); + + TabListEntry setGameMode(int gameMode); + + static Builder builder() { + return new Builder(); + } + + class Builder { + private TabList tabList; + private GameProfile profile; + private Component displayName; + private int latency = 0; + private int gameMode = 0; + + private Builder() {} + + public Builder tabList(TabList tabList) { + this.tabList = tabList; + return this; + } + + public Builder profile(GameProfile profile) { + this.profile = profile; + return this; + } + + public Builder displayName(@Nullable Component displayName) { + this.displayName = displayName; + return this; + } + + public Builder latency(int latency) { + this.latency = latency; + return this; + } + + public Builder gameMode(int gameMode) { + this.gameMode = gameMode; + return this; + } + + public TabListEntry build() { + Preconditions.checkState(tabList != null, "The Tablist must be set when building a TabListEntry"); + Preconditions.checkState(profile != null, "The GameProfile must be set when building a TabListEntry"); + + return tabList.buildEntry(profile, displayName, latency, gameMode); + } + } +} diff --git a/proxy/src/main/java/com/velocitypowered/proxy/connection/MinecraftSessionHandler.java b/proxy/src/main/java/com/velocitypowered/proxy/connection/MinecraftSessionHandler.java index 2c07b9e03..c756b31cf 100644 --- a/proxy/src/main/java/com/velocitypowered/proxy/connection/MinecraftSessionHandler.java +++ b/proxy/src/main/java/com/velocitypowered/proxy/connection/MinecraftSessionHandler.java @@ -66,4 +66,5 @@ public interface MinecraftSessionHandler { default boolean handle(TabCompleteRequest packet) { return false; } default boolean handle(TabCompleteResponse packet) { return false; } default boolean handle(TitlePacket packet) { return false; } + default boolean handle(PlayerListItem packet) { return false; } } diff --git a/proxy/src/main/java/com/velocitypowered/proxy/connection/backend/BackendPlaySessionHandler.java b/proxy/src/main/java/com/velocitypowered/proxy/connection/backend/BackendPlaySessionHandler.java index 284c89e2b..1e4d7a0ca 100644 --- a/proxy/src/main/java/com/velocitypowered/proxy/connection/backend/BackendPlaySessionHandler.java +++ b/proxy/src/main/java/com/velocitypowered/proxy/connection/backend/BackendPlaySessionHandler.java @@ -12,6 +12,7 @@ import com.velocitypowered.proxy.protocol.MinecraftPacket; import com.velocitypowered.proxy.protocol.ProtocolConstants; import com.velocitypowered.proxy.protocol.packet.*; import com.velocitypowered.proxy.protocol.util.PluginMessageUtil; +import com.velocitypowered.proxy.tablist.VelocityTabList; import io.netty.buffer.ByteBuf; public class BackendPlaySessionHandler implements MinecraftSessionHandler { @@ -118,6 +119,12 @@ public class BackendPlaySessionHandler implements MinecraftSessionHandler { return true; } + @Override + public boolean handle(PlayerListItem packet) { + serverConn.getPlayer().getTabList().processBackendPacket(packet); + return false; //Forward packet to player + } + @Override public void handleGeneric(MinecraftPacket packet) { if (!serverConn.getPlayer().isActive()) { diff --git a/proxy/src/main/java/com/velocitypowered/proxy/connection/backend/LoginSessionHandler.java b/proxy/src/main/java/com/velocitypowered/proxy/connection/backend/LoginSessionHandler.java index f56303d32..6e4cefe40 100644 --- a/proxy/src/main/java/com/velocitypowered/proxy/connection/backend/LoginSessionHandler.java +++ b/proxy/src/main/java/com/velocitypowered/proxy/connection/backend/LoginSessionHandler.java @@ -126,18 +126,7 @@ public class LoginSessionHandler implements MinecraftSessionHandler { ProtocolUtils.writeString(dataToForward, address); ProtocolUtils.writeUuid(dataToForward, profile.idAsUuid()); ProtocolUtils.writeString(dataToForward, profile.getName()); - ProtocolUtils.writeVarInt(dataToForward, profile.getProperties().size()); - for (GameProfile.Property property : profile.getProperties()) { - ProtocolUtils.writeString(dataToForward, property.getName()); - ProtocolUtils.writeString(dataToForward, property.getValue()); - String signature = property.getSignature(); - if (signature != null) { - dataToForward.writeBoolean(true); - ProtocolUtils.writeString(dataToForward, signature); - } else { - dataToForward.writeBoolean(false); - } - } + ProtocolUtils.writeProperties(dataToForward, profile.getProperties()); SecretKey key = new SecretKeySpec(hmacSecret, "HmacSHA256"); Mac mac = Mac.getInstance("HmacSHA256"); 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 440b689b7..5fd667463 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 @@ -12,6 +12,7 @@ import com.velocitypowered.proxy.protocol.MinecraftPacket; import com.velocitypowered.proxy.protocol.ProtocolConstants; import com.velocitypowered.proxy.protocol.packet.*; import com.velocitypowered.proxy.protocol.util.PluginMessageUtil; +import com.velocitypowered.proxy.tablist.VelocityTabList; import com.velocitypowered.proxy.util.ThrowableUtils; import io.netty.buffer.ByteBuf; import net.kyori.text.TextComponent; diff --git a/proxy/src/main/java/com/velocitypowered/proxy/connection/client/ConnectedPlayer.java b/proxy/src/main/java/com/velocitypowered/proxy/connection/client/ConnectedPlayer.java index b9e44833b..b36203c5d 100644 --- a/proxy/src/main/java/com/velocitypowered/proxy/connection/client/ConnectedPlayer.java +++ b/proxy/src/main/java/com/velocitypowered/proxy/connection/client/ConnectedPlayer.java @@ -15,6 +15,7 @@ import com.velocitypowered.api.proxy.messages.ChannelIdentifier; import com.velocitypowered.api.proxy.player.PlayerSettings; import com.velocitypowered.api.proxy.server.RegisteredServer; import com.velocitypowered.api.util.GameProfile; +import com.velocitypowered.api.proxy.player.TabList; import com.velocitypowered.api.util.MessagePosition; import com.velocitypowered.api.util.title.TextTitle; import com.velocitypowered.api.util.title.Title; @@ -29,6 +30,10 @@ import com.velocitypowered.proxy.connection.util.ConnectionRequestResults; import com.velocitypowered.proxy.protocol.ProtocolConstants; import com.velocitypowered.proxy.protocol.packet.*; import com.velocitypowered.proxy.server.VelocityRegisteredServer; +import com.velocitypowered.proxy.protocol.packet.Chat; +import com.velocitypowered.proxy.protocol.packet.ClientSettings; +import com.velocitypowered.proxy.protocol.packet.PluginMessage; +import com.velocitypowered.proxy.tablist.VelocityTabList; import com.velocitypowered.proxy.util.ThrowableUtils; import net.kyori.text.Component; import net.kyori.text.TextComponent; @@ -62,10 +67,12 @@ public class ConnectedPlayer implements MinecraftConnectionAssociation, Player { private VelocityServerConnection connectedServer; private VelocityServerConnection connectionInFlight; private PlayerSettings settings; + private final VelocityTabList tabList; private final VelocityServer server; ConnectedPlayer(VelocityServer server, GameProfile profile, MinecraftConnection connection, InetSocketAddress virtualHost) { this.server = server; + this.tabList = new VelocityTabList(connection); this.profile = profile; this.connection = connection; this.virtualHost = virtualHost; @@ -185,15 +192,18 @@ public class ConnectedPlayer implements MinecraftConnectionAssociation, Player { } @Override - public void setHeaderAndFooter(@NonNull Component header, @NonNull Component footer) { - Preconditions.checkNotNull(header, "header"); - Preconditions.checkNotNull(footer, "footer"); - connection.write(HeaderAndFooter.create(header, footer)); + public void setHeaderAndFooter(Component header, Component footer) { + tabList.setHeaderAndFooter(header, footer); } @Override public void clearHeaderAndFooter() { - connection.write(HeaderAndFooter.reset()); + tabList.clearHeaderAndFooter(); + } + + @Override + public VelocityTabList getTabList() { + return tabList; } @Override diff --git a/proxy/src/main/java/com/velocitypowered/proxy/protocol/ProtocolUtils.java b/proxy/src/main/java/com/velocitypowered/proxy/protocol/ProtocolUtils.java index 33a3a0ac5..35614c817 100644 --- a/proxy/src/main/java/com/velocitypowered/proxy/protocol/ProtocolUtils.java +++ b/proxy/src/main/java/com/velocitypowered/proxy/protocol/ProtocolUtils.java @@ -1,10 +1,13 @@ package com.velocitypowered.proxy.protocol; import com.google.common.base.Preconditions; +import com.velocitypowered.api.util.GameProfile; import io.netty.buffer.ByteBuf; import io.netty.buffer.ByteBufUtil; import java.nio.charset.StandardCharsets; +import java.util.ArrayList; +import java.util.List; import java.util.UUID; public enum ProtocolUtils { ; @@ -79,4 +82,35 @@ public enum ProtocolUtils { ; buf.writeLong(uuid.getMostSignificantBits()); buf.writeLong(uuid.getLeastSignificantBits()); } + + public static void writeProperties(ByteBuf buf, List properties) { + writeVarInt(buf, properties.size()); + for (GameProfile.Property property : properties) { + writeString(buf, property.getName()); + writeString(buf, property.getValue()); + String signature = property.getSignature(); + if (signature != null) { + buf.writeBoolean(true); + writeString(buf, signature); + } else { + buf.writeBoolean(false); + } + } + } + + public static List readProperties(ByteBuf buf) { + List properties = new ArrayList<>(); + int size = readVarInt(buf); + for (int i = 0; i < size; i++) { + String name = readString(buf); + String value = readString(buf); + String signature = ""; + boolean hasSignature = buf.readBoolean(); + if (hasSignature) { + signature = readString(buf); + } + properties.add(new GameProfile.Property(name, value, signature)); + } + return properties; + } } 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 8e6d1fe78..56bd3c76e 100644 --- a/proxy/src/main/java/com/velocitypowered/proxy/protocol/StateRegistry.java +++ b/proxy/src/main/java/com/velocitypowered/proxy/protocol/StateRegistry.java @@ -118,6 +118,12 @@ public enum StateRegistry { map(0x47, MINECRAFT_1_12, true), map(0x48, MINECRAFT_1_12_1, true), map(0x4B, MINECRAFT_1_13, true)); + CLIENTBOUND.register(PlayerListItem.class, PlayerListItem::new, + map(0x38, MINECRAFT_1_8, false), + map(0x2D, MINECRAFT_1_9, false), + map(0x2D, MINECRAFT_1_12, false), + map(0x2E, MINECRAFT_1_12_1, false), + map(0x30, MINECRAFT_1_13, false)); } }, LOGIN { diff --git a/proxy/src/main/java/com/velocitypowered/proxy/protocol/packet/PlayerListItem.java b/proxy/src/main/java/com/velocitypowered/proxy/protocol/packet/PlayerListItem.java new file mode 100644 index 000000000..6a8ba0944 --- /dev/null +++ b/proxy/src/main/java/com/velocitypowered/proxy/protocol/packet/PlayerListItem.java @@ -0,0 +1,196 @@ +package com.velocitypowered.proxy.protocol.packet; + +import com.velocitypowered.api.proxy.player.TabListEntry; +import com.velocitypowered.api.util.GameProfile; +import com.velocitypowered.proxy.connection.MinecraftSessionHandler; +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.serializer.ComponentSerializers; + +import java.util.ArrayList; +import java.util.List; +import java.util.UUID; + +public class PlayerListItem implements MinecraftPacket { + private Action action; + private List items; + + public PlayerListItem(Action action, List items) { + this.action = action; + this.items = items; + } + + public PlayerListItem() {} + + public Action getAction() { + return action; + } + + public List getItems() { + return items; + } + + @Override + public void decode(ByteBuf buf, ProtocolConstants.Direction direction, int protocolVersion) { + action = Action.values()[ProtocolUtils.readVarInt(buf)]; + items = new ArrayList<>(); + int length = ProtocolUtils.readVarInt(buf); + + for (int i = 0; i < length; i++) { + Item item = new Item(ProtocolUtils.readUuid(buf)); + items.add(item); + switch (action) { + case ADD_PLAYER: { + item.setName(ProtocolUtils.readString(buf)); + item.setProperties(ProtocolUtils.readProperties(buf)); + item.setGameMode(ProtocolUtils.readVarInt(buf)); + item.setLatency(ProtocolUtils.readVarInt(buf)); + boolean hasDisplayName = buf.readBoolean(); + if (hasDisplayName) { + item.setDisplayName(ComponentSerializers.JSON.deserialize(ProtocolUtils.readString(buf))); + } + } break; + case UPDATE_GAMEMODE: + item.setGameMode(ProtocolUtils.readVarInt(buf)); + break; + case UPDATE_LATENCY: + item.setLatency(ProtocolUtils.readVarInt(buf)); + break; + case UPDATE_DISPLAY_NAME: { + boolean hasDisplayName = buf.readBoolean(); + if (hasDisplayName) { + item.setDisplayName(ComponentSerializers.JSON.deserialize(ProtocolUtils.readString(buf))); + } + } break; + case REMOVE_PLAYER: + //Do nothing, all that is needed is the uuid + break; + } + } + } + + @Override + public void encode(ByteBuf buf, ProtocolConstants.Direction direction, int protocolVersion) { + ProtocolUtils.writeVarInt(buf, action.ordinal()); + ProtocolUtils.writeVarInt(buf, items.size()); + for (Item item: items) { + ProtocolUtils.writeUuid(buf, item.getUuid()); + switch (action) { + case ADD_PLAYER: + ProtocolUtils.writeString(buf, item.getName()); + ProtocolUtils.writeProperties(buf, item.getProperties()); + ProtocolUtils.writeVarInt(buf, item.getGameMode()); + ProtocolUtils.writeVarInt(buf, item.getLatency()); + + writeDisplayName(buf, item.getDisplayName()); + break; + case UPDATE_GAMEMODE: + ProtocolUtils.writeVarInt(buf, item.getGameMode()); + break; + case UPDATE_LATENCY: + ProtocolUtils.writeVarInt(buf, item.getLatency()); + break; + case UPDATE_DISPLAY_NAME: + writeDisplayName(buf, item.getDisplayName()); + break; + case REMOVE_PLAYER: + //Do nothing, all that is needed is the uuid + break; + } + } + } + + @Override + public boolean handle(MinecraftSessionHandler handler) { + return handler.handle(this); + } + + private void writeDisplayName(ByteBuf buf, Component displayName) { + buf.writeBoolean(displayName != null); + if (displayName != null) { + ProtocolUtils.writeString(buf, ComponentSerializers.JSON.serialize(displayName)); + } + } + + public static class Item { + private final UUID uuid; + private String name; + private List properties; + private int gameMode; + private int latency; + private Component displayName; + + public Item(UUID uuid) { + this.uuid = uuid; + } + + public static Item from(TabListEntry entry) { + return new Item(entry.getProfile().idAsUuid()) + .setName(entry.getProfile().getName()) + .setProperties(entry.getProfile().getProperties()) + .setLatency(entry.getLatency()) + .setGameMode(entry.getGameMode()) + .setDisplayName(entry.getDisplayName().orElse(null)); + } + + public UUID getUuid() { + return uuid; + } + + public String getName() { + return name; + } + + public Item setName(String name) { + this.name = name; + return this; + } + + public List getProperties() { + return properties; + } + + public Item setProperties(List properties) { + this.properties = properties; + return this; + } + + public int getGameMode() { + return gameMode; + } + + public Item setGameMode(int gamemode) { + this.gameMode = gamemode; + return this; + } + + public int getLatency() { + return latency; + } + + public Item setLatency(int latency) { + this.latency = latency; + return this; + } + + public Component getDisplayName() { + return displayName; + } + + public Item setDisplayName(Component displayName) { + this.displayName = displayName; + return this; + } + } + + public enum Action { + ADD_PLAYER, + UPDATE_GAMEMODE, + UPDATE_LATENCY, + UPDATE_DISPLAY_NAME, + REMOVE_PLAYER + } +} diff --git a/proxy/src/main/java/com/velocitypowered/proxy/tablist/VelocityTabList.java b/proxy/src/main/java/com/velocitypowered/proxy/tablist/VelocityTabList.java new file mode 100644 index 000000000..dffbaaba4 --- /dev/null +++ b/proxy/src/main/java/com/velocitypowered/proxy/tablist/VelocityTabList.java @@ -0,0 +1,113 @@ +package com.velocitypowered.proxy.tablist; + +import com.google.common.base.Preconditions; +import com.velocitypowered.api.proxy.player.TabList; +import com.velocitypowered.api.proxy.player.TabListEntry; +import com.velocitypowered.api.util.GameProfile; +import com.velocitypowered.api.util.UuidUtils; +import com.velocitypowered.proxy.connection.MinecraftConnection; +import com.velocitypowered.proxy.protocol.packet.HeaderAndFooter; +import com.velocitypowered.proxy.protocol.packet.PlayerListItem; +import net.kyori.text.Component; + +import java.util.Collection; +import java.util.Collections; +import java.util.HashMap; +import java.util.Map; +import java.util.Optional; +import java.util.UUID; + +public class VelocityTabList implements TabList { + private final MinecraftConnection connection; + private final Map entries = new HashMap<>(); + + public VelocityTabList(MinecraftConnection connection) { + this.connection = connection; + } + + @Override + public void setHeaderAndFooter(Component header, Component footer) { + Preconditions.checkNotNull(header, "header"); + Preconditions.checkNotNull(footer, "footer"); + connection.write(HeaderAndFooter.create(header, footer)); + } + + @Override + public void clearHeaderAndFooter() { + connection.write(HeaderAndFooter.reset()); + } + + @Override + public void addEntry(TabListEntry entry) { + Preconditions.checkNotNull(entry, "entry"); + Preconditions.checkArgument(entry.getTabList().equals(this), "The provided entry was not created by this tab list"); + Preconditions.checkArgument(!entries.containsKey(entry.getProfile().idAsUuid()), "this TabList already contains an entry with the same uuid"); + + PlayerListItem.Item packetItem = PlayerListItem.Item.from(entry); + connection.write(new PlayerListItem(PlayerListItem.Action.ADD_PLAYER, Collections.singletonList(packetItem))); + entries.put(entry.getProfile().idAsUuid(), entry); + } + + @Override + public Optional removeEntry(UUID uuid) { + TabListEntry entry = entries.remove(uuid); + if (entry != null) { + PlayerListItem.Item packetItem = PlayerListItem.Item.from(entry); + connection.write(new PlayerListItem(PlayerListItem.Action.REMOVE_PLAYER, Collections.singletonList(packetItem))); + } + + return Optional.ofNullable(entry); + } + + @Override + public Collection getEntries() { + return Collections.unmodifiableCollection(this.entries.values()); + } + + @Override + public TabListEntry buildEntry(GameProfile profile, Component displayName, int latency, int gameMode) { + return new VelocityTabListEntry(this, profile, displayName, latency, gameMode); + } + + public void processBackendPacket(PlayerListItem packet) { + //Packets are already forwarded on, so no need to do that here + for (PlayerListItem.Item item : packet.getItems()) { + UUID uuid = item.getUuid(); + if (packet.getAction() != PlayerListItem.Action.ADD_PLAYER && !entries.containsKey(uuid)) { + //Sometimes UPDATE_GAMEMODE is sent before ADD_PLAYER so don't want to warn here + continue; + } + + switch (packet.getAction()) { + case ADD_PLAYER: + entries.put(item.getUuid(), TabListEntry.builder() + .tabList(this) + .profile(new GameProfile(UuidUtils.toUndashed(uuid), item.getName(), item.getProperties())) + .displayName(item.getDisplayName()) + .latency(item.getLatency()) + .gameMode(item.getGameMode()) + .build()); + break; + case REMOVE_PLAYER: + entries.remove(uuid); + break; + case UPDATE_DISPLAY_NAME: + entries.get(uuid).setDisplayName(item.getDisplayName()); + break; + case UPDATE_LATENCY: + entries.get(uuid).setLatency(item.getLatency()); + break; + case UPDATE_GAMEMODE: + entries.get(uuid).setGameMode(item.getGameMode()); + break; + } + } + } + + void updateEntry(PlayerListItem.Action action, TabListEntry entry) { + if (entries.containsKey(entry.getProfile().idAsUuid())) { + PlayerListItem.Item packetItem = PlayerListItem.Item.from(entry); + connection.write(new PlayerListItem(action, Collections.singletonList(packetItem))); + } + } +} diff --git a/proxy/src/main/java/com/velocitypowered/proxy/tablist/VelocityTabListEntry.java b/proxy/src/main/java/com/velocitypowered/proxy/tablist/VelocityTabListEntry.java new file mode 100644 index 000000000..43862c874 --- /dev/null +++ b/proxy/src/main/java/com/velocitypowered/proxy/tablist/VelocityTabListEntry.java @@ -0,0 +1,72 @@ +package com.velocitypowered.proxy.tablist; + +import com.velocitypowered.api.proxy.player.TabList; +import com.velocitypowered.api.proxy.player.TabListEntry; +import com.velocitypowered.api.util.GameProfile; +import com.velocitypowered.proxy.protocol.packet.PlayerListItem; +import net.kyori.text.Component; +import org.checkerframework.checker.nullness.qual.Nullable; + +import java.util.Optional; + +public class VelocityTabListEntry implements TabListEntry { + private final VelocityTabList tabList; + private final GameProfile profile; + private Component displayName; + private int latency; + private int gameMode; + + VelocityTabListEntry(VelocityTabList tabList, GameProfile profile, Component displayName, int latency, int gameMode) { + this.tabList = tabList; + this.profile = profile; + this.displayName = displayName; + this.latency = latency; + this.gameMode = gameMode; + } + + @Override + public TabList getTabList() { + return tabList; + } + + @Override + public GameProfile getProfile() { + return profile; + } + + @Override + public Optional getDisplayName() { + return Optional.ofNullable(displayName); + } + + @Override + public TabListEntry setDisplayName(@Nullable Component displayName) { + this.displayName = displayName; + tabList.updateEntry(PlayerListItem.Action.UPDATE_DISPLAY_NAME, this); + return this; + } + + @Override + public int getLatency() { + return latency; + } + + @Override + public TabListEntry setLatency(int latency) { + this.latency = latency; + tabList.updateEntry(PlayerListItem.Action.UPDATE_LATENCY, this); + return this; + } + + @Override + public int getGameMode() { + return gameMode; + } + + @Override + public TabListEntry setGameMode(int gameMode) { + this.gameMode = gameMode; + tabList.updateEntry(PlayerListItem.Action.UPDATE_GAMEMODE, this); + return this; + } +}