diff --git a/docs/Config-File.md b/docs/Config-File.md index 0eeda57..ac82045 100644 --- a/docs/Config-File.md +++ b/docs/Config-File.md @@ -26,8 +26,6 @@ formatter: MINEDOWN fallback_enabled: true # The formats to use for the fallback group. fallback_group: default -# Only show other players on a server that is part of the same server group as the player. -only_list_players_in_same_group: true # Define custom names to be shown in the TAB list for specific server names. # If no custom display name is provided for a server, its original name will be used. server_display_names: @@ -79,18 +77,19 @@ groups: prefix: '&f%prefix%' suffix: '&f%suffix%' servers: - - lobby + - ^lobby[^ ]* - survival - creative - minigames - skyblock - prison - - hub sorting_placeholders: - '%role_weight%' - '%username_lower%' + collisions: false header_footer_update_rate: 1000 placeholder_update_rate: 1000 + only_list_players_in_same_server: false ``` diff --git a/src/main/java/net/william278/velocitab/config/Group.java b/src/main/java/net/william278/velocitab/config/Group.java index 5db1f37..1f3a975 100644 --- a/src/main/java/net/william278/velocitab/config/Group.java +++ b/src/main/java/net/william278/velocitab/config/Group.java @@ -27,6 +27,7 @@ import net.william278.velocitab.player.TabPlayer; import net.william278.velocitab.tab.Nametag; import org.apache.commons.text.StringEscapeUtils; import org.jetbrains.annotations.NotNull; +import org.slf4j.event.Level; import java.util.List; import java.util.Set; @@ -43,10 +44,11 @@ public record Group( String format, Nametag nametag, Set servers, - Set sortingPlaceholders, + List sortingPlaceholders, boolean collisions, int headerFooterUpdateRate, - int placeholderUpdateRate + int placeholderUpdateRate, + boolean onlyListPlayersInSameServer ) { @NotNull @@ -63,7 +65,7 @@ public record Group( @NotNull public Set registeredServers(@NotNull Velocitab plugin) { - if (isDefault() && plugin.getSettings().isFallbackEnabled()) { + if (isDefault(plugin) && plugin.getSettings().isFallbackEnabled()) { return Sets.newHashSet(plugin.getServer().getAllServers()); } return getRegexServers(plugin); @@ -73,28 +75,21 @@ public record Group( private Set getRegexServers(@NotNull Velocitab plugin) { final Set totalServers = Sets.newHashSet(); for (String server : servers) { - if (!server.contains("*") && !server.contains(".")) { - plugin.getServer().getServer(server).ifPresent(totalServers::add); - continue; - } - try { - final Matcher matcher = Pattern.compile(server - .replace(".", "\\.") - .replace("*", ".*")) - .matcher(""); + final Matcher matcher = Pattern.compile(server, Pattern.CASE_INSENSITIVE).matcher(""); plugin.getServer().getAllServers().stream() .filter(registeredServer -> matcher.reset(registeredServer.getServerInfo().getName()).matches()) .forEach(totalServers::add); - } catch (PatternSyntaxException ignored) { + } catch (PatternSyntaxException exception) { + plugin.log(Level.WARN, "Invalid regex pattern " + server + " in group " + name, exception); plugin.getServer().getServer(server).ifPresent(totalServers::add); } } return totalServers; } - public boolean isDefault() { - return name.equals("default"); + public boolean isDefault(@NotNull Velocitab plugin) { + return name.equals(plugin.getSettings().getFallbackGroup()); } @NotNull @@ -106,6 +101,16 @@ public record Group( return players; } + @NotNull + public Set getPlayers(@NotNull Velocitab plugin, @NotNull TabPlayer tabPlayer) { + if (onlyListPlayersInSameServer) { + return tabPlayer.getPlayer().getCurrentServer() + .map(s -> Sets.newHashSet(s.getServer().getPlayersConnected())) + .orElseGet(Sets::newHashSet); + } + return getPlayers(plugin); + } + @NotNull public Set getTabPlayers(@NotNull Velocitab plugin) { return plugin.getTabList().getPlayers() @@ -115,6 +120,18 @@ public record Group( .collect(Collectors.toSet()); } + @NotNull + public Set getTabPlayers(@NotNull Velocitab plugin, @NotNull TabPlayer tabPlayer) { + if (onlyListPlayersInSameServer) { + return plugin.getTabList().getPlayers() + .values() + .stream() + .filter(player -> player.getGroup().equals(this) && player.getServerName().equals(tabPlayer.getServerName())) + .collect(Collectors.toSet()); + } + return getTabPlayers(plugin); + } + @Override public boolean equals(Object obj) { if (!(obj instanceof Group group)) { diff --git a/src/main/java/net/william278/velocitab/config/Settings.java b/src/main/java/net/william278/velocitab/config/Settings.java index 86d83c8..d2497f7 100644 --- a/src/main/java/net/william278/velocitab/config/Settings.java +++ b/src/main/java/net/william278/velocitab/config/Settings.java @@ -63,9 +63,6 @@ public class Settings implements ConfigValidator { @Comment("The formats to use for the fallback group.") private String fallbackGroup = "default"; - @Comment("Only show other players on a server that is part of the same server group as the player.") - private boolean onlyListPlayersInSameGroup = true; - @Comment("Define custom names to be shown in the TAB list for specific server names." + "\nIf no custom display name is provided for a server, its original name will be used.") private Map serverDisplayNames = Map.of("very-long-server-name", "VLSN"); diff --git a/src/main/java/net/william278/velocitab/config/TabGroups.java b/src/main/java/net/william278/velocitab/config/TabGroups.java index c1cbe2a..9866b0d 100644 --- a/src/main/java/net/william278/velocitab/config/TabGroups.java +++ b/src/main/java/net/william278/velocitab/config/TabGroups.java @@ -53,10 +53,11 @@ public class TabGroups implements ConfigValidator { "&7[%server%] &f%prefix%%username%", new Nametag("&f%prefix%", "&f%suffix%"), Set.of("lobby", "survival", "creative", "minigames", "skyblock", "prison", "hub"), - Set.of("%role_weight%", "%username_lower%"), + List.of("%role_weight%", "%username_lower%"), false, 1000, - 1000 + 1000, + false ); public List groups = List.of(DEFAULT_GROUP); @@ -77,9 +78,20 @@ public class TabGroups implements ConfigValidator { } @NotNull - public Group getGroupFromServer(@NotNull String server) { + public Group getGroupFromServer(@NotNull String server, @NotNull Velocitab plugin) { + final List groups = new ArrayList<>(this.groups); + final Optional defaultGroup = getGroup("default"); + // Ensure the default group is always checked last + if (defaultGroup.isPresent()) { + groups.remove(defaultGroup.get()); + groups.add(defaultGroup.get()); + } else { + throw new IllegalStateException("No default group found"); + } for (Group group : groups) { - if (group.servers().contains(server)) { + if (group.registeredServers(plugin) + .stream() + .anyMatch(s -> s.getServerInfo().getName().equalsIgnoreCase(server))) { return group; } } @@ -101,7 +113,6 @@ public class TabGroups implements ConfigValidator { } final Multimap missingKeys = getMissingKeys(); - if (missingKeys.isEmpty()) { return; } @@ -148,7 +159,8 @@ public class TabGroups implements ConfigValidator { group.sortingPlaceholders() == null ? DEFAULT_GROUP.sortingPlaceholders() : group.sortingPlaceholders(), group.collisions(), group.headerFooterUpdateRate(), - group.placeholderUpdateRate() + group.placeholderUpdateRate(), + group.onlyListPlayersInSameServer() ); groups.add(group); diff --git a/src/main/java/net/william278/velocitab/packet/ScoreboardManager.java b/src/main/java/net/william278/velocitab/packet/ScoreboardManager.java index b83bec0..f247e59 100644 --- a/src/main/java/net/william278/velocitab/packet/ScoreboardManager.java +++ b/src/main/java/net/william278/velocitab/packet/ScoreboardManager.java @@ -175,13 +175,9 @@ public class ScoreboardManager { } final Player player = tabPlayer.getPlayer(); - final Set siblings = tabPlayer.getGroup().registeredServers(plugin); - final List players = siblings.stream() - .map(RegisteredServer::getPlayersConnected) - .flatMap(Collection::stream) - .toList(); + final Set players = tabPlayer.getGroup().getPlayers(plugin, tabPlayer); - final List roles = new ArrayList<>(); + final Set roles = Sets.newHashSet(); players.forEach(p -> { if (p == player || !p.isActive()) { return; @@ -253,8 +249,8 @@ public class ScoreboardManager { return; } - final Set siblings = tabPlayer.getGroup().registeredServers(plugin); - siblings.forEach(server -> server.getPlayersConnected().forEach(connected -> { + final Set players = tabPlayer.getGroup().getPlayers(plugin); + players.forEach(connected -> { try { final boolean canSee = plugin.getVanishManager().canSee(connected.getUsername(), player.getUsername()); if (!canSee) { @@ -266,7 +262,7 @@ public class ScoreboardManager { } catch (Throwable e) { plugin.log(Level.ERROR, "Failed to dispatch packet (unsupported client or server version)", e); } - })); + }); } public void registerPacket() { diff --git a/src/main/java/net/william278/velocitab/tab/PlayerTabList.java b/src/main/java/net/william278/velocitab/tab/PlayerTabList.java index 2a69969..50c0f97 100644 --- a/src/main/java/net/william278/velocitab/tab/PlayerTabList.java +++ b/src/main/java/net/william278/velocitab/tab/PlayerTabList.java @@ -19,7 +19,6 @@ package net.william278.velocitab.tab; -import com.google.common.collect.Lists; import com.google.common.collect.Maps; import com.velocitypowered.api.proxy.Player; import com.velocitypowered.api.proxy.ServerConnection; @@ -37,7 +36,9 @@ import net.william278.velocitab.config.Group; import net.william278.velocitab.config.Placeholder; import net.william278.velocitab.player.Role; import net.william278.velocitab.player.TabPlayer; +import org.apache.commons.lang3.ObjectUtils; import org.jetbrains.annotations.NotNull; +import org.jetbrains.annotations.Nullable; import org.slf4j.event.Level; import java.util.*; @@ -97,11 +98,13 @@ public class PlayerTabList { public void load() { plugin.getServer().getAllPlayers().forEach(p -> { final Optional server = p.getCurrentServer(); - if (server.isEmpty()) return; + if (server.isEmpty()) { + return; + } final String serverName = server.get().getServerInfo().getName(); final Group group = getGroup(serverName); - final boolean isDefault = !group.servers().contains(serverName); + final boolean isDefault = group.registeredServers(plugin).stream().noneMatch(s -> s.getServerInfo().getName().equals(serverName)); if (isDefault && !plugin.getSettings().isFallbackEnabled()) { return; @@ -146,12 +149,12 @@ public class PlayerTabList { tabPlayer.setGroup(group); players.putIfAbsent(joined.getUniqueId(), tabPlayer); + final String serverName = getServerName(joined); + //store last server, so it's possible to have the last server on disconnect - tabPlayer.setLastServer(joined.getCurrentServer().map(ServerConnection::getServerInfo).map(ServerInfo::getName).orElse("")); + tabPlayer.setLastServer(serverName); final boolean isVanished = plugin.getVanishManager().isVanished(joined.getUsername()); - final boolean isDefault = group.isDefault(); - final boolean isFallback = isDefault && plugin.getSettings().isFallbackEnabled(); tabPlayer.getDisplayName(plugin).thenAccept(d -> { @@ -167,16 +170,20 @@ public class PlayerTabList { return null; }); + final Set serversInGroup = group.registeredServers(plugin).stream() + .map(server -> server.getServerInfo().getName()) + .collect(HashSet::new, HashSet::add, HashSet::addAll); + + serversInGroup.remove(serverName); + // Update lists plugin.getServer().getScheduler() .buildTask(plugin, () -> { final TabList tabList = joined.getTabList(); - for (final TabPlayer player : players.values()) { + final Set tabPlayers = group.getTabPlayers(plugin); + for (final TabPlayer player : tabPlayers) { // Skip players on other servers if the setting is enabled - if (plugin.getSettings().isOnlyListPlayersInSameGroup() - && !isFallback && - !group.servers().contains(player.getServerName()) - ) { + if (group.onlyListPlayersInSameServer() && !serverName.equals(getServerName(player.getPlayer()))) { continue; } // check if current player can see the joined player @@ -187,7 +194,8 @@ public class PlayerTabList { } // check if joined player can see current player if ((plugin.getVanishManager().isVanished(player.getPlayer().getUsername()) && - !plugin.getVanishManager().canSee(joined.getUsername(), player.getPlayer().getUsername())) && player.getPlayer() != joined) { + !plugin.getVanishManager().canSee(joined.getUsername(), player.getPlayer().getUsername())) && + player.getPlayer() != joined) { tabList.removeEntry(player.getPlayer().getUniqueId()); } else { tabList.getEntry(player.getPlayer().getUniqueId()).ifPresentOrElse( @@ -221,16 +229,35 @@ public class PlayerTabList { }); } + @NotNull + private String getServerName(@NotNull Player player) { + return player.getCurrentServer() + .map(serverConnection -> serverConnection.getServerInfo().getName()) + .orElse(""); + } + protected void removePlayer(@NotNull Player target) { + removePlayer(target, null); + } + + protected void removePlayer(@NotNull Player target, @Nullable RegisteredServer server) { final UUID uuid = target.getUniqueId(); plugin.getServer().getAllPlayers().forEach(player -> player.getTabList().removeEntry(uuid)); + final Set currentServerPlayers = Optional.ofNullable(server) + .map(RegisteredServer::getPlayersConnected) + .map(HashSet::new) + .orElseGet(HashSet::new); + currentServerPlayers.add(target); + // Update the tab list of all players plugin.getServer().getScheduler() - .buildTask(plugin, () -> getPlayers().values().forEach(player -> { - player.getPlayer().getTabList().removeEntry(uuid); - player.sendHeaderAndFooter(this); - })) + .buildTask(plugin, () -> getPlayers().values().stream() + .filter(p -> currentServerPlayers.isEmpty() || !currentServerPlayers.contains(p.getPlayer())) + .forEach(player -> { + player.getPlayer().getTabList().removeEntry(uuid); + player.sendHeaderAndFooter(this); + })) .delay(500, TimeUnit.MILLISECONDS) .schedule(); // Delete player team @@ -240,20 +267,15 @@ public class PlayerTabList { } @NotNull - CompletableFuture createEntry(@NotNull TabPlayer player, @NotNull TabList tabList) { - return player.getDisplayName(plugin).thenApply(name -> TabListEntry.builder() - .profile(player.getPlayer().getGameProfile()) - .displayName(name) - .latency(0) - .tabList(tabList) - .build()); + protected CompletableFuture createEntry(@NotNull TabPlayer player, @NotNull TabList tabList) { + return player.getDisplayName(plugin).thenApply(name -> createEntry(player, tabList, name)); } protected TabListEntry createEntry(@NotNull TabPlayer player, @NotNull TabList tabList, @NotNull Component displayName) { return TabListEntry.builder() .profile(player.getPlayer().getGameProfile()) .displayName(displayName) - .latency(0) + .latency(Math.max((int) player.getPlayer().getPing(), 0)) .tabList(tabList) .build(); } @@ -314,15 +336,19 @@ public class PlayerTabList { } final boolean isVanished = plugin.getVanishManager().isVanished(tabPlayer.getPlayer().getUsername()); + final Set players = tabPlayer.getGroup().getTabPlayers(plugin, tabPlayer); - players.values().forEach(player -> { + players.forEach(player -> { if (isVanished && !plugin.getVanishManager().canSee(player.getPlayer().getUsername(), tabPlayer.getPlayer().getUsername())) { return; } player.getPlayer().getTabList().getEntries().stream() .filter(e -> e.getProfile().getId().equals(tabPlayer.getPlayer().getUniqueId())).findFirst() - .ifPresent(entry -> entry.setDisplayName(displayName)); + .ifPresent(entry -> { + entry.setDisplayName(displayName); + entry.setLatency(Math.max((int) tabPlayer.getPlayer().getPing(), 0)); + }); }); }); } @@ -379,7 +405,7 @@ public class PlayerTabList { * @param incrementIndexes Whether to increment the header and footer indexes. */ private void updateGroupPlayers(@NotNull Group group, boolean all, boolean incrementIndexes) { - Set groupPlayers = group.getTabPlayers(plugin); + final Set groupPlayers = group.getTabPlayers(plugin); if (groupPlayers.isEmpty()) { return; } @@ -449,7 +475,7 @@ public class PlayerTabList { @NotNull public Group getGroup(@NotNull String serverName) { - return plugin.getTabGroups().getGroupFromServer(serverName); + return plugin.getTabGroups().getGroupFromServer(serverName, plugin); } diff --git a/src/main/java/net/william278/velocitab/tab/TabListListener.java b/src/main/java/net/william278/velocitab/tab/TabListListener.java index f45acbb..cd68970 100644 --- a/src/main/java/net/william278/velocitab/tab/TabListListener.java +++ b/src/main/java/net/william278/velocitab/tab/TabListListener.java @@ -19,6 +19,7 @@ package net.william278.velocitab.tab; +import com.google.common.collect.Sets; import com.velocitypowered.api.event.PostOrder; import com.velocitypowered.api.event.Subscribe; import com.velocitypowered.api.event.connection.DisconnectEvent; @@ -35,6 +36,7 @@ import net.william278.velocitab.player.TabPlayer; import org.jetbrains.annotations.NotNull; import java.util.Optional; +import java.util.Set; import java.util.UUID; import java.util.concurrent.TimeUnit; @@ -46,20 +48,34 @@ public class TabListListener { private final Velocitab plugin; private final PlayerTabList tabList; + // In 1.8 there is a packet delay problem + private final Set justQuit; public TabListListener(@NotNull Velocitab plugin, @NotNull PlayerTabList tabList) { this.plugin = plugin; this.tabList = tabList; + this.justQuit = Sets.newConcurrentHashSet(); } @Subscribe public void onKick(@NotNull KickedFromServerEvent event) { - event.getPlayer().getTabList().clearAll(); + event.getPlayer().getTabList().getEntries().stream() + .filter(entry -> entry.getProfile() != null && !entry.getProfile().getId().equals(event.getPlayer().getUniqueId())) + .forEach(entry -> event.getPlayer().getTabList().removeEntry(entry.getProfile().getId())); event.getPlayer().getTabList().clearHeaderAndFooter(); - if (event.getResult() instanceof KickedFromServerEvent.DisconnectPlayer || event.getResult() instanceof KickedFromServerEvent.RedirectPlayer) { + if (event.getResult() instanceof KickedFromServerEvent.DisconnectPlayer) { tabList.removePlayer(event.getPlayer()); + } else if (event.getResult() instanceof KickedFromServerEvent.RedirectPlayer redirectPlayer) { + tabList.removePlayer(event.getPlayer(), redirectPlayer.getServer()); } + + justQuit.add(event.getPlayer().getUniqueId()); + + plugin.getServer().getScheduler().buildTask(plugin, + () -> justQuit.remove(event.getPlayer().getUniqueId())) + .delay(300, TimeUnit.MILLISECONDS) + .schedule(); } @SuppressWarnings("UnstableApiUsage") @@ -73,7 +89,8 @@ public class TabListListener { .orElse(""); final Group group = tabList.getGroup(serverName); plugin.getScoreboardManager().ifPresent(manager -> manager.resetCache(joined, group)); - final boolean isDefault = !group.servers().contains(serverName); + final boolean isDefault = group.registeredServers(plugin).stream() + .noneMatch(server -> server.getServerInfo().getName().equalsIgnoreCase(serverName)); // If the server is not in a group, use fallback. // If fallback is disabled, permit the player to switch excluded servers without a header or footer override @@ -92,15 +109,15 @@ public class TabListListener { final Component displayName = tabPlayer.get().getLastDisplayName(); plugin.getServer().getScheduler().buildTask(plugin, () -> { - if (header.equals(event.getPlayer().getPlayerListHeader()) && footer.equals(event.getPlayer().getPlayerListFooter())) { - event.getPlayer().sendPlayerListHeaderAndFooter(header, footer); - event.getPlayer().getCurrentServer().ifPresent(serverConnection -> - serverConnection.getServer().getPlayersConnected().forEach(player -> - player.getTabList().getEntry(joined.getUniqueId()).ifPresent(entry -> { - if (entry.getDisplayNameComponent().isPresent() && entry.getDisplayNameComponent().get().equals(displayName)) { - entry.setDisplayName(Component.text(joined.getUsername())); - } - }))); + final Component currentHeader = joined.getPlayerListHeader(); + final Component currentFooter = joined.getPlayerListFooter(); + if ((header.equals(currentHeader) && footer.equals(currentFooter)) || + (currentHeader.equals(Component.empty()) && currentFooter.equals(Component.empty())) + ) { + joined.sendPlayerListHeaderAndFooter(Component.empty(), Component.empty()); + joined.getCurrentServer().ifPresent(serverConnection -> serverConnection.getServer().getPlayersConnected().forEach(player -> + player.getTabList().getEntry(joined.getUniqueId()) + .ifPresent(entry -> entry.setDisplayName(Component.text(joined.getUsername()))))); } }).delay(500, TimeUnit.MILLISECONDS).schedule(); @@ -108,6 +125,14 @@ public class TabListListener { return; } + if (justQuit.contains(joined.getUniqueId())) { + plugin.getServer().getScheduler().buildTask(plugin, + () -> tabList.joinPlayer(joined, group)) + .delay(250, TimeUnit.MILLISECONDS) + .schedule(); + return; + } + tabList.joinPlayer(joined, group); } @@ -118,9 +143,6 @@ public class TabListListener { return; } - // Remove the player from the tracking list, Print warning if player was not removed - final UUID uuid = event.getPlayer().getUniqueId(); - // Remove the player from the tab list of all other players tabList.removePlayer(event.getPlayer()); } diff --git a/src/main/java/net/william278/velocitab/tab/VanishTabList.java b/src/main/java/net/william278/velocitab/tab/VanishTabList.java index 40a2494..5bf3c4b 100644 --- a/src/main/java/net/william278/velocitab/tab/VanishTabList.java +++ b/src/main/java/net/william278/velocitab/tab/VanishTabList.java @@ -27,15 +27,16 @@ import org.jetbrains.annotations.NotNull; import java.util.Optional; import java.util.Set; import java.util.UUID; +import java.util.stream.Collectors; /** * The VanishTabList handles the tab list for vanished players */ public class VanishTabList { - + private final Velocitab plugin; private final PlayerTabList tabList; - + public VanishTabList(Velocitab plugin, PlayerTabList tabList) { this.plugin = plugin; this.tabList = tabList; @@ -79,8 +80,6 @@ public class VanishTabList { */ public void recalculateVanishForPlayer(@NotNull TabPlayer tabPlayer) { final Player player = tabPlayer.getPlayer(); - final Set serversInGroup = tabPlayer.getGroup().servers(); - plugin.getServer().getAllPlayers().forEach(p -> { if (p.equals(player)) { return; @@ -94,8 +93,8 @@ public class VanishTabList { final TabPlayer target = targetOptional.get(); final String serverName = target.getServerName(); - if (plugin.getSettings().isOnlyListPlayersInSameGroup() - && !serversInGroup.contains(serverName)) { + if (tabPlayer.getGroup().onlyListPlayersInSameServer() + && !tabPlayer.getServerName().equals(serverName)) { return; } @@ -115,5 +114,4 @@ public class VanishTabList { } }); } - }