diff --git a/api/src/main/java/com/velocitypowered/api/event/player/ServerUpdateTabListEvent.java b/api/src/main/java/com/velocitypowered/api/event/player/ServerUpdateTabListEvent.java new file mode 100644 index 000000000..a10fd137c --- /dev/null +++ b/api/src/main/java/com/velocitypowered/api/event/player/ServerUpdateTabListEvent.java @@ -0,0 +1,193 @@ +/* + * Copyright (C) 2019-2024 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.event.player; + +import com.google.common.base.Preconditions; +import com.velocitypowered.api.event.ResultedEvent; +import com.velocitypowered.api.event.annotation.AwaitingEvent; +import com.velocitypowered.api.proxy.Player; +import com.velocitypowered.api.proxy.player.TabListEntry; +import java.util.Collections; +import java.util.List; +import java.util.Set; +import java.util.UUID; + +/** + * This event is fired, when a {@link com.velocitypowered.api.proxy.player.TabList Tablist} is updated. + * It can be used to override or cancel updates for {@link TabListEntry}s. + * Velocity will wait for this event to finish firing before forwarding it to the server. + * + *

Note: If the {@code actions} contain {@link Action#REMOVE_PLAYER Action.REMOVE_PLAYER}, that may be the only action. + * + *

Version-specific behavior: + *

  • For versions below 1.19.3, {@code actions} may only contain one action, and if that action + * is {@link Action#ADD_PLAYER Action.ADD_PLAYER}, the values normally set by other actions + * (e.g., {@link Action#UPDATE_GAME_MODE Action.UPDATE_GAME_MODE}) may still be set. + *
  • For versions below 1.8, {@code actions} may only contain {@link Action#ADD_PLAYER Action.ADD_PLAYER} + * or {@link Action#REMOVE_PLAYER Action.REMOVE_PLAYER}. {@link Action#ADD_PLAYER Action.ADD_PLAYER} may also act as a replacement + * for actions like {@link Action#UPDATE_LATENCY Action.UPDATE_LATENCY}}. + */ +@AwaitingEvent +public class ServerUpdateTabListEvent implements ResultedEvent { + + private final Player player; + private final Set actions; + private final List entries; + private TabListUpdateResult result; + + /** + * Constructs a {@link ServerUpdateTabListEvent} instance. + * + * @param player the player for whom the tab list is being updated + * @param actions the {@link Action Action}s from the server for this tab list update + * @param entries the {@link TabListEntry}s in their updated form + */ + public ServerUpdateTabListEvent(Player player, Set actions, List entries) { + this.player = Preconditions.checkNotNull(player, "player"); + this.actions = Preconditions.checkNotNull(actions, "actions"); + this.entries = Preconditions.checkNotNull(entries, "entries"); + this.result = TabListUpdateResult.allowed(); + } + + public Player getPlayer() { + return player; + } + + public Set getActions() { + return actions; + } + + /** + * The updated {@link TabListEntry}s that will be applied to the {@link com.velocitypowered.api.proxy.player.TabList Tablist} + * of the {@code player} (or in the case of {@link Action#REMOVE_PLAYER Action.REMOVE_PLAYER} removed). + * + * @return the updated entries, normally immutable + */ + public List getEntries() { + return entries; + } + + @Override + public TabListUpdateResult getResult() { + return result; + } + + @Override + public void setResult(TabListUpdateResult result) { + this.result = Preconditions.checkNotNull(result, "result"); + } + + @Override + public String toString() { + return "ServerUpdateTabListEvent{" + + "player=" + player + + ", actions=" + actions + + ", entries=" + entries + + '}'; + } + + /** + * Represents an action of the {@link ServerUpdateTabListEvent}. + */ + public enum Action { + /** + * Add new players to the player list. + */ + ADD_PLAYER, + /** + * Initialize the chat session for the entries. + */ + INITIALIZE_CHAT, + /** + * Update the gamemode for the entries. + */ + UPDATE_GAME_MODE, + /** + * Update the latency for the entries. + */ + UPDATE_LISTED, + /** + * Update the latency for the entries. + */ + UPDATE_LATENCY, + /** + * Update the display name for the specific entries. + */ + UPDATE_DISPLAY_NAME, + /** + * Remove players from the player list. + */ + REMOVE_PLAYER + } + + /** + * Represents the result of the {@link ServerUpdateTabListEvent}. + */ + public static final class TabListUpdateResult implements ResultedEvent.Result { + + private static final TabListUpdateResult ALLOWED = new TabListUpdateResult(true); + private static final TabListUpdateResult DENIED = new TabListUpdateResult(false); + + private final boolean status; + private final Set ids; + + public TabListUpdateResult(boolean status) { + this.status = status; + ids = Collections.emptySet(); + } + + public TabListUpdateResult(boolean status, Set ids) { + this.status = status; + this.ids = ids; + } + + @Override + public boolean isAllowed() { + return status; + } + + public Set getIds() { + return ids; + } + + /** + * Allows the {@link TabListEntry}s to be updated, with or without modification. + * + * @return the allowed result + */ + public static TabListUpdateResult allowed() { + return ALLOWED; + } + + /** + * Prevents the {@link TabListEntry}s from being updated. + * + * @return the denied result + */ + public static TabListUpdateResult denied() { + return DENIED; + } + + /** + * Only allows specific {@link TabListEntry}s to be updated. + * The updates for the remaining entries will be dropped. + * + *

    Note: You can get the id of an entry with {@link TabListEntry#getProfile()}{@link com.velocitypowered.api.util.GameProfile#getId() .getId()} + * + * @param allowedOnly A non-empty set of ids of the entries that should be updated + * @return a result with the specified entries to be updated + */ + public static TabListUpdateResult allowedSpecific(final Set allowedOnly) { + Preconditions.checkNotNull(allowedOnly, "allowedOnly"); + Preconditions.checkArgument(!allowedOnly.isEmpty(), "allowedOnly empty"); + return new TabListUpdateResult(true, allowedOnly); + } + + } + +} 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 128d5c370..533b314fd 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 @@ -334,20 +334,20 @@ public class BackendPlaySessionHandler implements MinecraftSessionHandler { @Override public boolean handle(LegacyPlayerListItemPacket packet) { - serverConn.getPlayer().getTabList().processLegacy(packet); - return false; + serverConn.getPlayer().getTabList().processLegacyUpdate(packet); + return true; } @Override public boolean handle(UpsertPlayerInfoPacket packet) { serverConn.getPlayer().getTabList().processUpdate(packet); - return false; + return true; } @Override public boolean handle(RemovePlayerInfoPacket packet) { serverConn.getPlayer().getTabList().processRemove(packet); - return false; + return true; } @Override 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 2b22fc7bc..ccbbf6e44 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 @@ -202,7 +202,7 @@ public class ConnectedPlayer implements MinecraftConnectionAssociation, Player, this.onlineMode = onlineMode; if (connection.getProtocolVersion().noLessThan(ProtocolVersion.MINECRAFT_1_19_3)) { - this.tabList = new VelocityTabList(this); + this.tabList = new VelocityTabList(this, server); } else if (connection.getProtocolVersion().noLessThan(ProtocolVersion.MINECRAFT_1_8)) { this.tabList = new KeyedVelocityTabList(this, server); } else { diff --git a/proxy/src/main/java/com/velocitypowered/proxy/tablist/InternalTabList.java b/proxy/src/main/java/com/velocitypowered/proxy/tablist/InternalTabList.java index 566ea6d66..206c9c44a 100644 --- a/proxy/src/main/java/com/velocitypowered/proxy/tablist/InternalTabList.java +++ b/proxy/src/main/java/com/velocitypowered/proxy/tablist/InternalTabList.java @@ -30,7 +30,7 @@ public interface InternalTabList extends TabList { Player getPlayer(); - default void processLegacy(LegacyPlayerListItemPacket packet) { + default void processLegacyUpdate(LegacyPlayerListItemPacket packet) { } default void processUpdate(UpsertPlayerInfoPacket infoPacket) { diff --git a/proxy/src/main/java/com/velocitypowered/proxy/tablist/KeyedVelocityTabList.java b/proxy/src/main/java/com/velocitypowered/proxy/tablist/KeyedVelocityTabList.java index 61967fbc2..61b17b097 100644 --- a/proxy/src/main/java/com/velocitypowered/proxy/tablist/KeyedVelocityTabList.java +++ b/proxy/src/main/java/com/velocitypowered/proxy/tablist/KeyedVelocityTabList.java @@ -18,6 +18,7 @@ package com.velocitypowered.proxy.tablist; import com.google.common.base.Preconditions; +import com.velocitypowered.api.event.player.ServerUpdateTabListEvent; import com.velocitypowered.api.proxy.Player; import com.velocitypowered.api.proxy.ProxyServer; import com.velocitypowered.api.proxy.crypto.IdentifiedKey; @@ -35,6 +36,7 @@ import java.util.List; import java.util.Map; import java.util.Objects; import java.util.Optional; +import java.util.Set; import java.util.UUID; import java.util.concurrent.ConcurrentHashMap; import net.kyori.adventure.text.Component; @@ -78,20 +80,27 @@ public class KeyedVelocityTabList implements InternalTabList { } @Override - public void addEntry(TabListEntry entry) { + public void addEntry(TabListEntry entry1) { + KeyedVelocityTabListEntry entry; + if (entry1 instanceof KeyedVelocityTabListEntry) { + entry = (KeyedVelocityTabListEntry) entry1; + } else { + entry = new KeyedVelocityTabListEntry(this, entry1.getProfile(), + entry1.getDisplayNameComponent().orElse(null), + entry1.getLatency(), entry1.getGameMode(), entry1.getIdentifiedKey()); + } + 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().getId()), "this TabList already contains an entry with the same uuid"); - Preconditions.checkArgument(entry instanceof KeyedVelocityTabListEntry, - "Not a Velocity tab list entry"); LegacyPlayerListItemPacket.Item packetItem = LegacyPlayerListItemPacket.Item.from(entry); connection.write( new LegacyPlayerListItemPacket(LegacyPlayerListItemPacket.ADD_PLAYER, Collections.singletonList(packetItem))); - entries.put(entry.getProfile().getId(), (KeyedVelocityTabListEntry) entry); + entries.put(entry.getProfile().getId(), entry); } @Override @@ -166,20 +175,117 @@ public class KeyedVelocityTabList implements InternalTabList { } @Override - public void processLegacy(LegacyPlayerListItemPacket packet) { - // Packets are already forwarded on, so no need to do that here - for (LegacyPlayerListItemPacket.Item item : packet.getItems()) { - UUID uuid = item.getUuid(); - assert uuid != null : "1.7 tab list entry given to modern tab list handler!"; + public void processLegacyUpdate(LegacyPlayerListItemPacket packet) { + ServerUpdateTabListEvent.Action action = mapToEventAction(packet.getAction()); + Preconditions.checkNotNull(action, "action"); - if (packet.getAction() != LegacyPlayerListItemPacket.ADD_PLAYER - && !entries.containsKey(uuid)) { + List entries = mapToEventEntries(packet.getAction(), packet.getItems()); + + proxyServer.getEventManager().fire( + new ServerUpdateTabListEvent( + player, + Set.of(action), + Collections.unmodifiableList(entries) + ) + ).thenAcceptAsync(event -> { + if (event.getResult().isAllowed()) { + if (event.getResult().getIds().isEmpty()) { + boolean rewrite = false; + for (UpdateEventTabListEntry entry : entries) { + if (entry.isRewrite()) { + rewrite = true; + break; + } + } + + if (rewrite) { + //listeners have modified entries, requires manual processing + if (action != ServerUpdateTabListEvent.Action.REMOVE_PLAYER) { + for (UpdateEventTabListEntry entry : entries) { + if (this.entries.containsKey(entry.getProfile().getId())) { + removeEntry(entry.getProfile().getId()); + } + + addEntry(entry); + } + } else { + for (UpdateEventTabListEntry entry : entries) { + removeEntry(entry.getProfile().getId()); + } + } + } else { + //listeners haven't modified entries + for (LegacyPlayerListItemPacket.Item item : packet.getItems()) { + processLegacy(packet.getAction(), item); + } + + connection.write(packet); + } + } else { + //listeners have denied entries (and may have modified others), requires manual processing + if (action != ServerUpdateTabListEvent.Action.REMOVE_PLAYER) { + for (UpdateEventTabListEntry entry : entries) { + if (event.getResult().getIds().contains(entry.getProfile().getId())) { + if (this.entries.containsKey(entry.getProfile().getId())) { + removeEntry(entry.getProfile().getId()); + } + + addEntry(entry); + } + } + } else { + for (UpdateEventTabListEntry entry : entries) { + if (event.getResult().getIds().contains(entry.getProfile().getId())) { + removeEntry(entry.getProfile().getId()); + } + } + } + } + } + }); + } + + protected ServerUpdateTabListEvent.@Nullable Action mapToEventAction(int action) { + return switch (action) { + case LegacyPlayerListItemPacket.ADD_PLAYER -> ServerUpdateTabListEvent.Action.ADD_PLAYER; + case LegacyPlayerListItemPacket.REMOVE_PLAYER -> ServerUpdateTabListEvent.Action.REMOVE_PLAYER; + case LegacyPlayerListItemPacket.UPDATE_GAMEMODE -> ServerUpdateTabListEvent.Action.UPDATE_GAME_MODE; + case LegacyPlayerListItemPacket.UPDATE_LATENCY -> ServerUpdateTabListEvent.Action.UPDATE_LATENCY; + case LegacyPlayerListItemPacket.UPDATE_DISPLAY_NAME -> ServerUpdateTabListEvent.Action.UPDATE_DISPLAY_NAME; + default -> null; + }; + } + + private List mapToEventEntries(int action, List packetItems) { + List entries = new ArrayList<>(packetItems.size()); + + for (LegacyPlayerListItemPacket.Item item : packetItems) { + UUID uuid = item.getUuid(); + Preconditions.checkNotNull(uuid, "1.7 tab list entry given to modern tab list handler!"); + + if (action != LegacyPlayerListItemPacket.ADD_PLAYER + && !this.entries.containsKey(uuid)) { // Sometimes UPDATE_GAMEMODE is sent before ADD_PLAYER so don't want to warn here continue; } - switch (packet.getAction()) { - case LegacyPlayerListItemPacket.ADD_PLAYER: { + UpdateEventTabListEntry currentEntry = null; + KeyedVelocityTabListEntry oldCurrentEntry = this.entries.get(uuid); + + if (oldCurrentEntry != null) { + currentEntry = new UpdateEventTabListEntry( + this, + oldCurrentEntry.getProfile(), + oldCurrentEntry.getDisplayNameComponent().orElse(null), + oldCurrentEntry.getLatency(), + oldCurrentEntry.getGameMode(), + oldCurrentEntry.getChatSession(), + oldCurrentEntry.isListed() + ); + } + + switch (action) { + case LegacyPlayerListItemPacket.ADD_PLAYER -> { // ensure that name and properties are available String name = item.getName(); List properties = item.getProperties(); @@ -187,43 +293,97 @@ public class KeyedVelocityTabList implements InternalTabList { throw new IllegalStateException("Got null game profile for ADD_PLAYER"); } - entries.putIfAbsent(item.getUuid(), (KeyedVelocityTabListEntry) TabListEntry.builder() - .tabList(this) - .profile(new GameProfile(uuid, name, properties)) - .displayName(item.getDisplayName()) - .latency(item.getLatency()) - .chatSession(new RemoteChatSession(null, item.getPlayerKey())) - .gameMode(item.getGameMode()) - .build()); - break; + currentEntry = new UpdateEventTabListEntry( + this, + new GameProfile(uuid, name, properties), + item.getDisplayName(), + item.getLatency(), + item.getGameMode(), + new RemoteChatSession(null, item.getPlayerKey()), + true + ); } - case LegacyPlayerListItemPacket.REMOVE_PLAYER: - entries.remove(uuid); - break; - case LegacyPlayerListItemPacket.UPDATE_DISPLAY_NAME: { - KeyedVelocityTabListEntry entry = entries.get(uuid); - if (entry != null) { - entry.setDisplayNameInternal(item.getDisplayName()); + case LegacyPlayerListItemPacket.REMOVE_PLAYER -> { + //Nothing should be done here, as all entries which are not allowed are removed + // if the action is ServerUpdateTabListEvent.Action.REMOVE_PLAYER + } + case LegacyPlayerListItemPacket.UPDATE_DISPLAY_NAME -> { + if (currentEntry != null) { + currentEntry.setDisplayNameWithoutRewrite(item.getDisplayName()); } - break; } - case LegacyPlayerListItemPacket.UPDATE_LATENCY: { - KeyedVelocityTabListEntry entry = entries.get(uuid); - if (entry != null) { - entry.setLatencyInternal(item.getLatency()); + case LegacyPlayerListItemPacket.UPDATE_LATENCY -> { + if (currentEntry != null) { + currentEntry.setLatencyWithoutRewrite(item.getLatency()); } - break; } - case LegacyPlayerListItemPacket.UPDATE_GAMEMODE: { - KeyedVelocityTabListEntry entry = entries.get(uuid); - if (entry != null) { - entry.setGameModeInternal(item.getGameMode()); + case LegacyPlayerListItemPacket.UPDATE_GAMEMODE -> { + if (currentEntry != null) { + currentEntry.setGameModeWithoutRewrite(item.getGameMode()); } - break; } - default: + default -> { // Nothing we can do here - break; + } + } + + if (currentEntry != null) { + entries.add(currentEntry); + } + } + + return entries; + } + + private void processLegacy(int action, LegacyPlayerListItemPacket.Item item) { + UUID uuid = item.getUuid(); + assert uuid != null : "1.7 tab list entry given to modern tab list handler!"; + + if (action != LegacyPlayerListItemPacket.ADD_PLAYER + && !entries.containsKey(uuid)) { + // Sometimes UPDATE_GAMEMODE is sent before ADD_PLAYER so don't want to warn here + return; + } + + switch (action) { + case LegacyPlayerListItemPacket.ADD_PLAYER -> { + // ensure that name and properties are available + String name = item.getName(); + List properties = item.getProperties(); + if (name == null || properties == null) { + throw new IllegalStateException("Got null game profile for ADD_PLAYER"); + } + + entries.putIfAbsent(item.getUuid(), (KeyedVelocityTabListEntry) TabListEntry.builder() + .tabList(this) + .profile(new GameProfile(uuid, name, properties)) + .displayName(item.getDisplayName()) + .latency(item.getLatency()) + .chatSession(new RemoteChatSession(null, item.getPlayerKey())) + .gameMode(item.getGameMode()) + .build()); + } + case LegacyPlayerListItemPacket.REMOVE_PLAYER -> entries.remove(uuid); + case LegacyPlayerListItemPacket.UPDATE_DISPLAY_NAME -> { + KeyedVelocityTabListEntry entry = entries.get(uuid); + if (entry != null) { + entry.setDisplayNameInternal(item.getDisplayName()); + } + } + case LegacyPlayerListItemPacket.UPDATE_LATENCY -> { + KeyedVelocityTabListEntry entry = entries.get(uuid); + if (entry != null) { + entry.setLatencyInternal(item.getLatency()); + } + } + case LegacyPlayerListItemPacket.UPDATE_GAMEMODE -> { + KeyedVelocityTabListEntry entry = entries.get(uuid); + if (entry != null) { + entry.setGameModeInternal(item.getGameMode()); + } + } + default -> { + // Nothing we can do here } } } diff --git a/proxy/src/main/java/com/velocitypowered/proxy/tablist/UpdateEventTabListEntry.java b/proxy/src/main/java/com/velocitypowered/proxy/tablist/UpdateEventTabListEntry.java new file mode 100644 index 000000000..70f0ba194 --- /dev/null +++ b/proxy/src/main/java/com/velocitypowered/proxy/tablist/UpdateEventTabListEntry.java @@ -0,0 +1,145 @@ +/* + * Copyright (C) 2018-2024 Velocity Contributors + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +package com.velocitypowered.proxy.tablist; + +import com.velocitypowered.api.event.player.ServerUpdateTabListEvent; +import com.velocitypowered.api.proxy.player.ChatSession; +import com.velocitypowered.api.proxy.player.TabList; +import com.velocitypowered.api.proxy.player.TabListEntry; +import com.velocitypowered.api.util.GameProfile; +import java.util.Optional; +import net.kyori.adventure.text.Component; +import org.checkerframework.checker.nullness.qual.Nullable; + +/** + * Represents a {@link TabListEntry} of the {@link ServerUpdateTabListEvent}. + */ +public class UpdateEventTabListEntry implements TabListEntry { + + private final TabList tabList; + private final GameProfile profile; + private @Nullable Component displayName; + private int latency; + private int gameMode; + private boolean listed; + private @Nullable ChatSession session; + private boolean rewrite = false; + + /** + * Constructs an instance. + */ + public UpdateEventTabListEntry(TabList tabList, GameProfile profile, @Nullable Component displayName, + int latency, + int gameMode, @Nullable ChatSession session, boolean listed) { + this.tabList = tabList; + this.profile = profile; + this.displayName = displayName; + this.latency = latency; + this.gameMode = gameMode; + this.session = session; + this.listed = listed; + } + + @Override + public @Nullable ChatSession getChatSession() { + return this.session; + } + + @Override + public TabList getTabList() { + return this.tabList; + } + + @Override + public GameProfile getProfile() { + return this.profile; + } + + @Override + public Optional getDisplayNameComponent() { + return Optional.ofNullable(displayName); + } + + @Override + public TabListEntry setDisplayName(@Nullable Component displayName) { + this.displayName = displayName; + rewrite = true; + return this; + } + + void setDisplayNameWithoutRewrite(@Nullable Component displayName) { + this.displayName = displayName; + } + + @Override + public int getLatency() { + return this.latency; + } + + @Override + public TabListEntry setLatency(int latency) { + this.latency = latency; + rewrite = true; + return this; + } + + void setLatencyWithoutRewrite(int latency) { + this.latency = latency; + } + + @Override + public int getGameMode() { + return this.gameMode; + } + + @Override + public TabListEntry setGameMode(int gameMode) { + this.gameMode = gameMode; + rewrite = true; + return this; + } + + void setGameModeWithoutRewrite(int gameMode) { + this.gameMode = gameMode; + } + + void setChatSessionWithoutRewrite(@Nullable ChatSession session) { + this.session = session; + } + + @Override + public boolean isListed() { + return listed; + } + + @Override + public TabListEntry setListed(boolean listed) { + this.listed = listed; + rewrite = true; + return this; + } + + void setListedWithoutRewrite(boolean listed) { + this.listed = listed; + } + + boolean isRewrite() { + return rewrite; + } + +} diff --git a/proxy/src/main/java/com/velocitypowered/proxy/tablist/VelocityTabList.java b/proxy/src/main/java/com/velocitypowered/proxy/tablist/VelocityTabList.java index d6b4143ce..fba14e343 100644 --- a/proxy/src/main/java/com/velocitypowered/proxy/tablist/VelocityTabList.java +++ b/proxy/src/main/java/com/velocitypowered/proxy/tablist/VelocityTabList.java @@ -19,7 +19,9 @@ package com.velocitypowered.proxy.tablist; import com.google.common.base.Preconditions; import com.google.common.collect.Maps; +import com.velocitypowered.api.event.player.ServerUpdateTabListEvent; import com.velocitypowered.api.proxy.Player; +import com.velocitypowered.api.proxy.ProxyServer; import com.velocitypowered.api.proxy.player.ChatSession; import com.velocitypowered.api.proxy.player.TabListEntry; import com.velocitypowered.api.util.GameProfile; @@ -32,11 +34,13 @@ import com.velocitypowered.proxy.protocol.packet.chat.ComponentHolder; import com.velocitypowered.proxy.protocol.packet.chat.RemoteChatSession; import java.util.ArrayList; import java.util.Collection; +import java.util.Collections; import java.util.EnumSet; import java.util.List; import java.util.Map; import java.util.Objects; import java.util.Optional; +import java.util.Set; import java.util.UUID; import net.kyori.adventure.text.Component; import org.apache.logging.log4j.LogManager; @@ -51,6 +55,7 @@ public class VelocityTabList implements InternalTabList { private static final Logger logger = LogManager.getLogger(VelocityConsole.class); private final ConnectedPlayer player; private final MinecraftConnection connection; + protected final ProxyServer proxyServer; private final Map entries; /** @@ -58,8 +63,9 @@ public class VelocityTabList implements InternalTabList { * * @param player player associated with this tab list */ - public VelocityTabList(ConnectedPlayer player) { + public VelocityTabList(ConnectedPlayer player, final ProxyServer proxyServer) { this.player = player; + this.proxyServer = proxyServer; this.connection = player.getConnection(); this.entries = Maps.newConcurrentMap(); } @@ -214,9 +220,45 @@ public class VelocityTabList implements InternalTabList { @Override public void processUpdate(UpsertPlayerInfoPacket infoPacket) { - for (UpsertPlayerInfoPacket.Entry entry : infoPacket.getEntries()) { - processUpsert(infoPacket.getActions(), entry); - } + List entries = mapToEventEntries(infoPacket.getActions(), infoPacket.getEntries()); + + proxyServer.getEventManager().fire(new ServerUpdateTabListEvent(player, + Collections.unmodifiableSet(mapToEventActions(infoPacket.getActions())), + Collections.unmodifiableList(entries)) + ).thenAcceptAsync(event -> { + if (event.getResult().isAllowed()) { + if (event.getResult().getIds().isEmpty()) { + boolean rewrite = false; + for (UpdateEventTabListEntry entry : entries) { + if (entry.isRewrite()) { + rewrite = true; + break; + } + } + + if (rewrite) { + //listeners have modified entries, requires manual processing + for (UpdateEventTabListEntry entry : entries) { + addEntry(entry); + } + } else { + //listeners haven't modified entries + for (UpsertPlayerInfoPacket.Entry entry : infoPacket.getEntries()) { + processUpsert(infoPacket.getActions(), entry); + } + + connection.write(infoPacket); + } + } else { + //listeners have denied entries (and may have modified others), requires manual processing + for (UpdateEventTabListEntry entry : entries) { + if (event.getResult().getIds().contains(entry.getProfile().getId())) { + addEntry(entry); + } + } + } + } + }); } protected UpsertPlayerInfoPacket.Entry createRawEntry(VelocityTabListEntry entry) { @@ -231,6 +273,134 @@ public class VelocityTabList implements InternalTabList { this.connection.write(new UpsertPlayerInfoPacket(EnumSet.of(action), List.of(entry))); } + private EnumSet mapToEventActions(EnumSet packetActions) { + EnumSet actions = EnumSet.noneOf(ServerUpdateTabListEvent.Action.class); + + for (UpsertPlayerInfoPacket.Action packetAction : packetActions) { + switch (packetAction) { + case ADD_PLAYER -> { + actions.add(ServerUpdateTabListEvent.Action.ADD_PLAYER); + } + case INITIALIZE_CHAT -> { + actions.add(ServerUpdateTabListEvent.Action.INITIALIZE_CHAT); + } + case UPDATE_GAME_MODE -> { + actions.add(ServerUpdateTabListEvent.Action.UPDATE_GAME_MODE); + } + case UPDATE_LISTED -> { + actions.add(ServerUpdateTabListEvent.Action.UPDATE_LISTED); + } + case UPDATE_LATENCY -> { + actions.add(ServerUpdateTabListEvent.Action.UPDATE_LATENCY); + } + case UPDATE_DISPLAY_NAME -> { + actions.add(ServerUpdateTabListEvent.Action.UPDATE_DISPLAY_NAME); + } + default -> { + // Nothing we can do here + } + } + } + + return actions; + } + + private List mapToEventEntries(EnumSet actions, + List packetEntries) { + List entries = new ArrayList<>(packetEntries.size()); + + for (UpsertPlayerInfoPacket.Entry rawEntry : packetEntries) { + Preconditions.checkNotNull(rawEntry.getProfileId(), "Profile ID cannot be null"); + UUID profileId = rawEntry.getProfileId(); + UpdateEventTabListEntry currentEntry = null; + VelocityTabListEntry oldCurrentEntry = this.entries.get(profileId); + + if (oldCurrentEntry != null) { + currentEntry = new UpdateEventTabListEntry( + this, + oldCurrentEntry.getProfile(), + oldCurrentEntry.getDisplayNameComponent().orElse(null), + oldCurrentEntry.getLatency(), + oldCurrentEntry.getGameMode(), + oldCurrentEntry.getChatSession(), + oldCurrentEntry.isListed() + ); + } + + if (actions.contains(UpsertPlayerInfoPacket.Action.ADD_PLAYER)) { + if (currentEntry == null) { + currentEntry = new UpdateEventTabListEntry( + this, + rawEntry.getProfile(), + null, + 0, + -1, + null, + false + ); + } else { + logger.debug("Received an add player packet for an existing entry; this does nothing."); + } + } else if (currentEntry == null) { + logger.debug( + "Received a partial player before an ADD_PLAYER action; profile could not be built. {}", + rawEntry); + continue; + } else { + currentEntry = new UpdateEventTabListEntry( + this, + currentEntry.getProfile(), + currentEntry.getDisplayNameComponent().orElse(null), + currentEntry.getLatency(), + currentEntry.getGameMode(), + currentEntry.getChatSession(), + currentEntry.isListed() + ); + } + if (actions.contains(UpsertPlayerInfoPacket.Action.UPDATE_GAME_MODE)) { + currentEntry.setGameModeWithoutRewrite(rawEntry.getGameMode()); + } + if (actions.contains(UpsertPlayerInfoPacket.Action.UPDATE_LATENCY)) { + currentEntry.setLatencyWithoutRewrite(rawEntry.getLatency()); + } + if (actions.contains(UpsertPlayerInfoPacket.Action.UPDATE_DISPLAY_NAME)) { + currentEntry.setDisplayNameWithoutRewrite(rawEntry.getDisplayName() != null + ? rawEntry.getDisplayName().getComponent() : null); + } + if (actions.contains(UpsertPlayerInfoPacket.Action.INITIALIZE_CHAT)) { + currentEntry.setChatSessionWithoutRewrite(rawEntry.getChatSession()); + } + if (actions.contains(UpsertPlayerInfoPacket.Action.UPDATE_LISTED)) { + currentEntry.setListedWithoutRewrite(rawEntry.isListed()); + } + entries.add(currentEntry); + } + + return entries; + } + + private List mapToEventEntries(Collection uuids) { + List entries = new ArrayList<>(); + + for (Map.Entry entry : this.entries.entrySet()) { + if (uuids.contains(entry.getKey())) { + entries.add( + new UpdateEventTabListEntry( + this, + entry.getValue().getProfile(), + entry.getValue().getDisplayNameComponent().orElse(null), + entry.getValue().getLatency(), + entry.getValue().getGameMode(), + entry.getValue().getChatSession(), + entry.getValue().isListed() + ) + ); + } + } + + return entries; + } + private void processUpsert(EnumSet actions, UpsertPlayerInfoPacket.Entry entry) { Preconditions.checkNotNull(entry.getProfileId(), "Profile ID cannot be null"); @@ -278,8 +448,34 @@ public class VelocityTabList implements InternalTabList { @Override public void processRemove(RemovePlayerInfoPacket infoPacket) { - for (UUID uuid : infoPacket.getProfilesToRemove()) { - this.entries.remove(uuid); - } + List entries = mapToEventEntries(infoPacket.getProfilesToRemove()); + + proxyServer.getEventManager().fire( + new ServerUpdateTabListEvent( + player, + Set.of(ServerUpdateTabListEvent.Action.REMOVE_PLAYER), + Collections.unmodifiableList(entries) + ) + ).thenAcceptAsync(event -> { //not sure what should be used here! + if (event.getResult().isAllowed()) { + if (event.getResult().getIds().isEmpty()) { + for (UUID uuid : infoPacket.getProfilesToRemove()) { + this.entries.remove(uuid); + } + + connection.write(infoPacket); + } else { + List uuids = new ArrayList<>(); + for (UUID uuid : infoPacket.getProfilesToRemove()) { + if (event.getResult().getIds().contains(uuid)) { + this.entries.remove(uuid); + uuids.add(uuid); + } + } + + this.connection.write(new RemovePlayerInfoPacket(uuids)); + } + } + }); } -} \ No newline at end of file +} diff --git a/proxy/src/main/java/com/velocitypowered/proxy/tablist/VelocityTabListEntry.java b/proxy/src/main/java/com/velocitypowered/proxy/tablist/VelocityTabListEntry.java index 4e036504a..9d826fb06 100644 --- a/proxy/src/main/java/com/velocitypowered/proxy/tablist/VelocityTabListEntry.java +++ b/proxy/src/main/java/com/velocitypowered/proxy/tablist/VelocityTabListEntry.java @@ -34,7 +34,7 @@ public class VelocityTabListEntry implements TabListEntry { private final VelocityTabList tabList; private final GameProfile profile; - private Component displayName; + private @Nullable Component displayName; private int latency; private int gameMode; private boolean listed; @@ -43,7 +43,7 @@ public class VelocityTabListEntry implements TabListEntry { /** * Constructs the instance. */ - public VelocityTabListEntry(VelocityTabList tabList, GameProfile profile, Component displayName, + public VelocityTabListEntry(VelocityTabList tabList, GameProfile profile, @Nullable Component displayName, int latency, int gameMode, @Nullable ChatSession session, boolean listed) { this.tabList = tabList; diff --git a/proxy/src/main/java/com/velocitypowered/proxy/tablist/VelocityTabListLegacy.java b/proxy/src/main/java/com/velocitypowered/proxy/tablist/VelocityTabListLegacy.java index 1eeb180ed..989cd9ff0 100644 --- a/proxy/src/main/java/com/velocitypowered/proxy/tablist/VelocityTabListLegacy.java +++ b/proxy/src/main/java/com/velocitypowered/proxy/tablist/VelocityTabListLegacy.java @@ -17,16 +17,18 @@ package com.velocitypowered.proxy.tablist; +import com.google.common.base.Preconditions; import com.google.common.collect.ImmutableList; +import com.velocitypowered.api.event.player.ServerUpdateTabListEvent; import com.velocitypowered.api.proxy.ProxyServer; import com.velocitypowered.api.proxy.player.TabListEntry; import com.velocitypowered.api.util.GameProfile; import com.velocitypowered.proxy.connection.client.ConnectedPlayer; import com.velocitypowered.proxy.protocol.packet.LegacyPlayerListItemPacket; -import com.velocitypowered.proxy.protocol.packet.LegacyPlayerListItemPacket.Item; import java.util.Collections; import java.util.Map; import java.util.Optional; +import java.util.Set; import java.util.UUID; import java.util.concurrent.ConcurrentHashMap; import net.kyori.adventure.text.Component; @@ -82,11 +84,111 @@ public class VelocityTabListLegacy extends KeyedVelocityTabList { } @Override - public void processLegacy(LegacyPlayerListItemPacket packet) { - Item item = packet.getItems().get(0); // Only one item per packet in 1.7 + public void processLegacyUpdate(LegacyPlayerListItemPacket packet) { + ServerUpdateTabListEvent.Action action = mapToEventAction(packet.getAction()); + Preconditions.checkNotNull(action, "action"); - switch (packet.getAction()) { - case LegacyPlayerListItemPacket.ADD_PLAYER: + UpdateEventTabListEntry entry = mapToEventEntry(packet.getAction(), packet.getItems().get(0)); // Only one item per packet in 1.7 + + proxyServer.getEventManager().fire( + new ServerUpdateTabListEvent( + player, + Set.of(action), + Collections.singletonList(entry) + ) + ).thenAcceptAsync(event -> { + if (event.getResult().isAllowed()) { + if (event.getResult().getIds().isEmpty()) { + if (entry.isRewrite()) { + //listeners have modified the entry, requires manual processing + if (action != ServerUpdateTabListEvent.Action.REMOVE_PLAYER) { + if (this.entries.containsKey(entry.getProfile().getId())) { + removeEntry(entry.getProfile().getId()); + } + + addEntry(entry); + } else { + removeEntry(entry.getProfile().getId()); + } + } else { + //listeners haven't modified the entry + processLegacy(packet.getAction(), packet.getItems().get(0)); + + connection.write(packet); + } + } else { + //listeners have denied entries (and may have modified others), requires manual processing + // (doesn't make much sense as there can only be one entry) + if (action != ServerUpdateTabListEvent.Action.REMOVE_PLAYER) { + if (event.getResult().getIds().contains(entry.getProfile().getId())) { + if (this.entries.containsKey(entry.getProfile().getId())) { + removeEntry(entry.getProfile().getId()); + } + + addEntry(entry); + } + } else { + if (event.getResult().getIds().contains(entry.getProfile().getId())) { + removeEntry(entry.getProfile().getId()); + } + } + } + } + }); + } + + private UpdateEventTabListEntry mapToEventEntry(int action, LegacyPlayerListItemPacket.Item packetItem) { + UpdateEventTabListEntry currentEntry = null; + + switch (action) { + case LegacyPlayerListItemPacket.ADD_PLAYER -> { + if (nameMapping.containsKey(packetItem.getName())) { // ADD_PLAYER also used for updating ping + KeyedVelocityTabListEntry oldCurrentEntry = this.entries.get(nameMapping.get(packetItem.getName())); + + if (oldCurrentEntry != null) { + currentEntry = new UpdateEventTabListEntry( + this, + oldCurrentEntry.getProfile(), + oldCurrentEntry.getDisplayNameComponent().orElse(null), + oldCurrentEntry.getLatency(), + oldCurrentEntry.getGameMode(), + oldCurrentEntry.getChatSession(), + oldCurrentEntry.isListed() + ); + } + + if (currentEntry != null) { + currentEntry.setLatencyWithoutRewrite(packetItem.getLatency()); + } + } else { + UUID uuid = UUID.randomUUID(); // Use a fake uuid to preserve function of custom entries + + nameMapping.put(packetItem.getName(), uuid); + currentEntry = new UpdateEventTabListEntry( + this, + new GameProfile(uuid, packetItem.getName(), ImmutableList.of()), + null, + packetItem.getLatency(), + 0, + null, + true + ); + } + } + case LegacyPlayerListItemPacket.REMOVE_PLAYER -> { + //Nothing should be done here as all entries which are not allowed are removed if the action is ServerUpdateTabListEvent.Action.REMOVE_PLAYER + } + default -> { + // For 1.7 there is only add and remove + } + } + + return currentEntry; + } + + private void processLegacy(int action, LegacyPlayerListItemPacket.Item item) { + switch (action) { + case LegacyPlayerListItemPacket.ADD_PLAYER -> { if (nameMapping.containsKey(item.getName())) { // ADD_PLAYER also used for updating ping KeyedVelocityTabListEntry entry = entries.get(nameMapping.get(item.getName())); if (entry != null) { @@ -101,16 +203,16 @@ public class VelocityTabListLegacy extends KeyedVelocityTabList { .latency(item.getLatency()) .build()); } - break; - case LegacyPlayerListItemPacket.REMOVE_PLAYER: + } + case LegacyPlayerListItemPacket.REMOVE_PLAYER -> { UUID removedUuid = nameMapping.remove(item.getName()); if (removedUuid != null) { entries.remove(removedUuid); } - break; - default: + } + default -> { // For 1.7 there is only add and remove - break; + } } }