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;
+ }
}
}