Fixes, logic simplification, update docs for nametags (#101)

This commit is contained in:
William 2023-10-12 11:44:27 +01:00 committed by GitHub
parent 1f1e69ebca
commit 8224cd0ff1
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
16 changed files with 157 additions and 202 deletions

View File

@ -65,6 +65,9 @@ Which formatting and the header/footer to use for a player's TAB list is determi
### Formatting
Velocitab supports the full range of modern color formatting, including RGB colors and gradients, through either MineDown or MiniMessage syntax. See [[Formatting]] for more information.
## Nametags
As well as updating the text in the TAB menu, Velocitab supports updating player nametags (the text displayed above their heads). See [[Nametags]] for more information.
### Animations
Velocitab supports basic header and footer animations by adding multiple frames of animation and setting the update rate to a value greater than 0.

View File

@ -11,6 +11,7 @@ Please click through to the topic you wish to read about.
## Documentation
* 👥 [[Server Groups]]
* 🎨 [[Formatting]]
* 📛 [[Nametags]]
* 📊 [[Sorting]]
* ✍️ [[Placeholders]]
* ✨ [[Animations]]

33
docs/Nametags.md Normal file
View File

@ -0,0 +1,33 @@
Velocitab supports formatting the nametags of players (the text displayed above their heads). This can be used to display a player's rank, group, or other information using placeholders. Please note some limitations apply.
[Nametags being updated by Velocitab in-game](https://raw.githubusercontent.com/WiIIiam278/Velocitab/master/images/nametags.png)
> **Note:** This feature requires sending Update Teams packets. `send_scoreboard_packets` must be enabled in the [`config.yml` file](config-file) for this to work. [More details...](sorting#compatibility-issues)
## Setting name tags
You can configure nametags per-group using the `nametags` section of the config file. Each group should have one nametag format associated with it, which will be applied to all players on servers in that group.
<details>
<summary>Editing nametags (config.yml)</summary>
```yaml
# Nametag(s) to display above players' heads for each server group. Set to empty to disable.
# Nametag formats must contain a %username%. Docs: https://william278.net/docs/velocitab/nametags
nametags:
default: '&f%prefix%%username%&f%suffix%'
# (...)
# Whether to send scoreboard teams packets. Required for player list sorting and nametag formatting.
# Turn this off if you're using scoreboard teams on backend servers.
send_scoreboard_packets: true
```
</details>
Only players on servers which are part of groups that specify nametag formats will have their nametag formatted. To disable nametag formatting, remove all groups from the `nametags` section of the config file (leaving it empty).
## Formatting limitations
Nametags must adhere to the following restrictions:
* A %username% placeholder must be present. This is used for delimiting the scoreboard prefix, name, and suffix to facilitate formatting.
* Only legacy colors can be used in formats. If RGB colors are specified, they will automatically be downsampled to the nearest legacy color. This is a limitation of the scoreboard team system.
* Gradients are not supported.

View File

@ -1,4 +1,4 @@
Velocitab supports defining multiple server groups, each providing distinct formatting for players in the TAB list, alongside unique headers and footers. This is useful if you wish to display different information in TAB depending on the server a player is on.
Velocitab supports defining multiple server groups, each providing distinct formatting for players in the TAB list, alongside unique headers and footers. This is useful if you wish to display different information in TAB depending on the server a player is on. You can also set formatting to use for [[Nametags]] above players' heads per-group.
## Defining groups
Groups are defined in the `server_groups` section of `config.yml`, as a list of servers following the group name (by default, a group `default` will be present, alongside a list of servers on your network.
@ -34,8 +34,8 @@ server_groups:
```
</details>
## Mapping headers, footers & player formats to groups
Once you've defined your groups, you can modify the `headers`, `footers` and `formats` section of the file with different formats for each group.
## Mapping headers, footers, user formats, and nametags to groups
Once you've defined your groups, you can modify the `headers`, `footers`, `formats` and [`nametags`](nametags) section of the file with different formats for each group.
<details>
<summary>Per-group formats</summary>
@ -59,10 +59,14 @@ formats:
lobbies: '&8[Lobby] &7%username%'
creative: '&e[Creative] &7[%server%] &f%prefix%%username%'
survival: '&2[Survival (%server%)] &f%prefix%%username%'
nametags:
lobbies: '&8[Lobby] &7%prefix%%username%&7%suffix%'
creative: '&e%prefix%%username%&7%suffix%'
survival: '&7%prefix%%username%&7%suffix%'
```
</details>
See [[Placeholders]] for how to use placeholders in these formats, and [[Formatting]] for how to format text with colors, and see [[Animations]] for how to create basic animations by adding more headers/footers to each group's list.
See [[Placeholders]] for how to use placeholders in these formats, and [[Formatting]] for how to format text with colors, and see [[Animations]] for how to create basic animations by adding more headers/footers to each group's list. Note that some formatting limitations apply to nametags &mdash; [[Nametags]] for more information.
### Adding new lines
If you want to add a new line to your header or footer format, you can use `\n` to insert one &mdash; but since this gets messy quickly, there's an easier way using the YAML markup pipe character to declare a multiline string:
@ -80,7 +84,7 @@ footers:
```
</details>
Player name formats may only utilize one line.
Player name formats and nametags may only utilize one line.
## Default group
If a player isn't connected to a server on your network, their TAB menu will be formatted as per the formats defined by `fallback_group` set in `config.yml`, provided `fallback_enabled` is set to `true`.

View File

@ -1,5 +1,7 @@
Velocitab can sort players in the TAB list by a number of "sorting elements." Sorting is enabled by default, and can be disabled with the `sort_players` option in the [`config.yml`](Config-File) file.
> > **Note:** This feature requires sending Update Teams packets. `send_scoreboard_packets` must be enabled in the [`config.yml` file](config-file) for this to work. [More details...](#compatibility-issues)
## Sortable elements
To modify what players are sorted by, modify the `sorting_placeholders` list in the [`config.yml`](Config-File) file. This option accepts an ordered list; the first element in the list is what players will be sorted by first, with subsequent elements being used to break ties. The default sorting strategy is to sort first by `%role_weight%` followed by `%username%`.
@ -37,4 +39,4 @@ There are a few compatibility caveats to bear in mind with sorting players in th
* Some mods can interfere with scoreboard team packets, particularly if they internally deal with managing packets or scoreboard teams.
* Sending fake scoreboard team packets might not work correctly on some Minecraft server implementations such as [Quilt](https://quiltmc.org/).
In these cases, you may need to disable sorting through the `sort_players` option detailed earlier.
In these cases, you may need to disable the use of scoreboard packets through the `send_scoreboard_packets` option detailed earlier.

View File

@ -5,6 +5,7 @@
## Documentation
* 👥 [[Server Groups]]
* 🎨 [[Formatting]]
* 📛 [[Nametags]]
* 📊 [[Sorting]]
* ✍️ [[Placeholders]]
* ✨ [[Animations]]

BIN
images/nametags.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.2 MiB

View File

@ -44,7 +44,6 @@ import net.william278.velocitab.hook.PAPIProxyBridgeHook;
import net.william278.velocitab.packet.ScoreboardManager;
import net.william278.velocitab.player.Role;
import net.william278.velocitab.player.TabPlayer;
import net.william278.velocitab.sorting.SortingManager;
import net.william278.velocitab.tab.PlayerTabList;
import org.bstats.charts.SimplePie;
import org.bstats.velocity.Metrics;
@ -74,7 +73,6 @@ public class Velocitab {
private PlayerTabList tabList;
private List<Hook> hooks;
private ScoreboardManager scoreboardManager;
private SortingManager sortingManager;
@Inject
public Velocitab(@NotNull ProxyServer server, @NotNull Logger logger, @DataDirectory Path dataDirectory) {
@ -87,7 +85,6 @@ public class Velocitab {
public void onProxyInitialization(@NotNull ProxyInitializeEvent event) {
loadSettings();
loadHooks();
prepareSortingManager();
prepareScoreboardManager();
prepareTabList();
registerCommands();
@ -160,21 +157,15 @@ public class Velocitab {
Hook.AVAILABLE.forEach(availableHook -> availableHook.apply(this).ifPresent(hooks::add));
}
private void prepareSortingManager() {
if (settings.isSortPlayers()) {
this.sortingManager = new SortingManager(this);
}
}
private void prepareScoreboardManager() {
if (settings.isSortPlayers()) {
if (settings.isSendScoreboardPackets()) {
this.scoreboardManager = new ScoreboardManager(this);
scoreboardManager.registerPacket();
}
}
private void disableScoreboardManager() {
if (scoreboardManager != null && settings.isSortPlayers()) {
if (scoreboardManager != null && settings.isSendScoreboardPackets()) {
scoreboardManager.unregisterPacket();
}
}
@ -184,10 +175,6 @@ public class Velocitab {
return Optional.ofNullable(scoreboardManager);
}
public Optional<SortingManager> getSortingManager() {
return Optional.ofNullable(sortingManager);
}
@NotNull
public PlayerTabList getTabList() {
return tabList;

View File

@ -50,7 +50,7 @@ public enum Placeholder {
SUFFIX((plugin, player) -> player.getRole().getSuffix().orElse("")),
ROLE((plugin, player) -> player.getRole().getName().orElse("")),
ROLE_DISPLAY_NAME((plugin, player) -> player.getRole().getDisplayName().orElse("")),
ROLE_WEIGHT((plugin, player) -> Integer.toString(player.getRole().getWeight())),
ROLE_WEIGHT((plugin, player) -> player.getRoleWeightString()),
SERVER_GROUP((plugin, player) -> player.getServerGroup(plugin)),
SERVER_GROUP_INDEX((plugin, player) -> Integer.toString(player.getServerGroupPosition(plugin))),
DEBUG_TEAM_NAME((plugin, player) -> plugin.getFormatter().escape(player.getLastTeamName().orElse("")));
@ -65,7 +65,8 @@ public enum Placeholder {
this.replacer = replacer;
}
public static CompletableFuture<String> replace(@NotNull String format, @NotNull Velocitab plugin, @NotNull TabPlayer player) {
public static CompletableFuture<String> replace(@NotNull String format, @NotNull Velocitab plugin,
@NotNull TabPlayer player) {
for (Placeholder placeholder : values()) {
format = format.replace("%" + placeholder.name().toLowerCase() + "%", placeholder.replacer.apply(plugin, player));
}
@ -83,4 +84,9 @@ public enum Placeholder {
});
}
@NotNull
public static String formatSortableInt(int value, int maxValue) {
return String.format("%0" + Integer.toString(maxValue).length() + "d", maxValue - value);
}
}

View File

@ -46,22 +46,27 @@ public class Settings {
private boolean checkForUpdates = true;
@YamlKey("headers")
@YamlComment("Header(s) to display above the TAB list for each server group." +
"\nList multiple headers and set update_rate to the number of ticks between frames for basic animations")
private Map<String, List<String>> headers = Map.of("default", List.of("&rainbow&Running Velocitab by William278"));
@YamlComment("Header(s) to display above the TAB list for each server group."
+ "\nList multiple headers and set update_rate to the number of ticks between frames for basic animations")
private Map<String, List<String>> headers = Map.of(
"default",
List.of("&rainbow&Running Velocitab by William278")
);
@YamlKey("footers")
@YamlComment("Footer(s) to display below the TAB list for each server group, same as headers.")
private Map<String, List<String>> footers = Map.of(
"default",
List.of("[There are currently %players_online%/%max_players_online% players online](gray)"));
List.of("[There are currently %players_online%/%max_players_online% players online](gray)")
);
@YamlKey("formats")
private Map<String, String> formats = Map.of("default", "&7[%server%] &f%prefix%%username%");
@Getter
@YamlKey("nametags")
@YamlComment("Nametag(s) to display above players' heads for each server group. To disable, set to empty")
@YamlComment("Nametag(s) to display above players' heads for each server group. Set to empty to disable."
+ "\nNametag formats must contain a %username%. Docs: https://william278.net/docs/velocitab/nametags")
private Map<String, String> nametags = Map.of("default", "&f%prefix%%username%&f%suffix%");
@Getter
@ -72,12 +77,15 @@ public class Settings {
@Getter
@YamlKey("server_groups")
@YamlComment("The servers in each group of servers. The order of groups is important when sorting by SERVER_GROUP.")
private LinkedHashMap<String, List<String>> serverGroups = new LinkedHashMap<>(Map.of("default", List.of("lobby1", "lobby2", "lobby3")));
private LinkedHashMap<String, List<String>> serverGroups = new LinkedHashMap<>(Map.of(
"default",
List.of("lobby1", "lobby2", "lobby3"))
);
@Getter
@YamlKey("fallback_enabled")
@YamlComment("All servers which are not in other groups will be put in the fallback group.\n" +
"\"false\" will exclude them from Velocitab.")
@YamlComment("All servers which are not in other groups will be put in the fallback group."
+ "\n\"false\" will exclude them from Velocitab.")
private boolean fallbackEnabled = true;
@Getter
@ -92,8 +100,8 @@ public class Settings {
@Getter
@YamlKey("server_display_names")
@YamlComment("Define custom names to be shown in the TAB list for specific server names.\n" +
"If no custom display name is provided for a server, its original name will be used.")
@YamlComment("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<String, String> serverDisplayNames = Map.of("very-long-server-name", "VLSN");
@Getter
@ -111,6 +119,12 @@ public class Settings {
@YamlComment("If you are using MINIMESSAGE formatting, enable this to support MiniPlaceholders in formatting.")
private boolean enableMiniPlaceholdersHook = true;
@Getter
@YamlKey("send_scoreboard_packets")
@YamlComment("Whether to send scoreboard teams packets. Required for player list sorting and nametag formatting."
+ "\nTurn this off if you're using scoreboard teams on backend servers.")
private boolean sendScoreboardPackets = true;
@Getter
@YamlKey("sort_players")
@YamlComment("Whether to sort players in the TAB list.")
@ -176,7 +190,7 @@ public class Settings {
nametags.getOrDefault(serverGroup, ""));
}
public boolean areNametagsEnabled() {
public boolean doNametags() {
return !nametags.isEmpty();
}

View File

@ -38,6 +38,7 @@ import static com.velocitypowered.api.network.ProtocolVersion.*;
public class ScoreboardManager {
private static final String NAMETAG_DELIMITER = ":::";
private PacketRegistration<UpdateTeamsPacket> packetRegistration;
private final Velocitab plugin;
private final Set<TeamsPacketAdapter> versions;
@ -67,7 +68,7 @@ public class ScoreboardManager {
}
public void resetCache(@NotNull Player player) {
String team = createdTeams.remove(player.getUniqueId());
final String team = createdTeams.remove(player.getUniqueId());
if (team != null) {
dispatchGroupPacket(UpdateTeamsPacket.removeTeam(plugin, team), player);
}
@ -87,16 +88,15 @@ public class ScoreboardManager {
String suffix = split.length > 1 ? split[1] : "";
if (!createdTeams.getOrDefault(player.getUniqueId(), "").equals(role)) {
if (createdTeams.containsKey(player.getUniqueId())) {
dispatchGroupPacket(UpdateTeamsPacket.removeTeam(plugin, createdTeams.get(player.getUniqueId())), player);
}
createdTeams.put(player.getUniqueId(), role);
this.nametags.put(role, prefix + ":::" + suffix);
this.nametags.put(role, prefix + NAMETAG_DELIMITER + suffix);
dispatchGroupPacket(UpdateTeamsPacket.create(plugin, role, "", prefix, suffix, name), player);
} else if (!this.nametags.getOrDefault(role, "").equals(prefix + ":::" + suffix)) {
this.nametags.put(role, prefix + ":::" + suffix);
} else if (!this.nametags.getOrDefault(role, "").equals(prefix + NAMETAG_DELIMITER + suffix)) {
this.nametags.put(role, prefix + NAMETAG_DELIMITER + suffix);
dispatchGroupPacket(UpdateTeamsPacket.changeNameTag(plugin, role, prefix, suffix), player);
}
}).exceptionally(e -> {
@ -107,8 +107,7 @@ public class ScoreboardManager {
public void resendAllNameTags(Player player) {
if (!plugin.getSettings().areNametagsEnabled()) {
if (!plugin.getSettings().doNametags()) {
return;
}
@ -117,12 +116,12 @@ public class ScoreboardManager {
return;
}
RegisteredServer serverInfo = optionalServerConnection.get().getServer();
List<RegisteredServer> siblings = plugin.getTabList().getGroupServers(serverInfo.getServerInfo().getName());
List<Player> players = siblings.stream().map(RegisteredServer::getPlayersConnected).flatMap(Collection::stream).toList();
final RegisteredServer serverInfo = optionalServerConnection.get().getServer();
final List<RegisteredServer> siblings = plugin.getTabList().getGroupServers(serverInfo.getServerInfo().getName());
final List<Player> players = siblings.stream()
.map(RegisteredServer::getPlayersConnected)
.flatMap(Collection::stream)
.toList();
players.forEach(p -> {
if (p == player || !p.isActive()) {
return;
@ -138,10 +137,9 @@ public class ScoreboardManager {
return;
}
String[] split = nametag.split(":::", 2);
String prefix = split[0];
String suffix = split.length > 1 ? split[1] : "";
final String[] split = nametag.split(NAMETAG_DELIMITER, 2);
final String prefix = split[0];
final String suffix = split.length > 1 ? split[1] : "";
dispatchPacket(UpdateTeamsPacket.create(plugin, role, "", prefix, suffix, p.getUsername()), player);
});
}
@ -156,7 +154,7 @@ public class ScoreboardManager {
final ConnectedPlayer connectedPlayer = (ConnectedPlayer) player;
connectedPlayer.getConnection().write(packet);
} catch (Throwable e) {
plugin.log(Level.ERROR, "Failed to dispatch packet (is the client or server modded or using an illegal version?)", e);
plugin.log(Level.ERROR, "Failed to dispatch packet (unsupported client or server version)", e);
}
}

View File

@ -121,13 +121,12 @@ public class UpdateTeamsPacket implements MinecraftPacket {
if (text == null) {
return 15;
}
int intvar = text.lastIndexOf("§");
if (intvar == -1 || intvar == text.length() - 1) {
int lastFormatIndex = text.lastIndexOf("§");
if (lastFormatIndex == -1 || lastFormatIndex == text.length() - 1) {
return 15;
}
String last = text.substring(intvar, intvar + 2);
final String last = text.substring(lastFormatIndex, lastFormatIndex + 2);
return TeamColor.getColorId(last.charAt(1));
}
@ -159,7 +158,7 @@ public class UpdateTeamsPacket implements MinecraftPacket {
private final int id;
TeamColor(char colorChar, int id) {
this.colorChar= colorChar;
this.colorChar = colorChar;
this.id = id;
}
@ -183,7 +182,6 @@ public class UpdateTeamsPacket implements MinecraftPacket {
if (optionalManager.isEmpty()) {
return;
}
optionalManager.get().getPacketAdapter(protocolVersion).encode(byteBuf, this);
}

View File

@ -20,6 +20,7 @@
package net.william278.velocitab.player;
import lombok.Getter;
import net.william278.velocitab.config.Placeholder;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
@ -70,7 +71,7 @@ public class Role implements Comparable<Role> {
@NotNull
protected String getWeightString(int highestWeight) {
return String.format("%0" + Integer.toString(highestWeight).length() + "d", highestWeight - weight);
return Placeholder.formatSortableInt(weight, highestWeight);
}
}

View File

@ -28,10 +28,8 @@ import net.william278.velocitab.tab.PlayerTabList;
import org.jetbrains.annotations.NotNull;
import org.slf4j.event.Level;
import java.util.Arrays;
import java.util.Optional;
import java.util.concurrent.CompletableFuture;
import java.util.function.BiFunction;
public final class TabPlayer implements Comparable<TabPlayer> {
private final Player player;
@ -61,6 +59,11 @@ public final class TabPlayer implements Comparable<TabPlayer> {
return role;
}
@NotNull
public String getRoleWeightString() {
return getRole().getWeightString(highestWeight);
}
/**
* Get the server name the player is currently on.
* Isn't affected by server aliases defined in the config.
@ -125,13 +128,17 @@ public final class TabPlayer implements Comparable<TabPlayer> {
@NotNull
public CompletableFuture<String> getTeamName(@NotNull Velocitab plugin) {
return plugin.getSortingManager().map(sortingManager -> sortingManager.getTeamName(this))
.orElseGet(() -> CompletableFuture.completedFuture(""))
.thenApply(teamName -> {
this.teamName = teamName;
return teamName;
}).exceptionally(e -> {
plugin.log(Level.ERROR, "Failed to get team name for " + player.getUsername(), e);
if (!plugin.getSettings().isSortPlayers()) {
return CompletableFuture.completedFuture("");
}
final String sortingFormat = String.join("", plugin.getSettings().getSortingElements());
return Placeholder.replace(sortingFormat, plugin, this) // Replace placeholders
.thenApply(formatted -> formatted.length() > 12 ? formatted.substring(0, 12) : formatted) // Truncate
.thenApply(truncated -> truncated + getPlayer().getUniqueId().toString().substring(0, 4)) // Make unique
.thenApply(teamName -> this.teamName = teamName)
.exceptionally(e -> {
plugin.log(Level.ERROR, "Failed to get team name for " + player.getUsername(), e);
return "";
});
}
@ -174,39 +181,4 @@ public final class TabPlayer implements Comparable<TabPlayer> {
return obj instanceof TabPlayer other && player.getUniqueId().equals(other.player.getUniqueId());
}
/**
* Elements for sorting players
*/
@SuppressWarnings("unused")
public enum SortableElement {
ROLE_WEIGHT((player, plugin) -> player.getRole().getWeightString(player.highestWeight)),
ROLE_NAME((player, plugin) -> player.getRole().getName()
.map(name -> name.length() > 3 ? name.substring(0, 3) : name)
.orElse("")),
SERVER_NAME((player, plugin) -> player.getServerName()),
SERVER_GROUP((player, plugin) -> {
int orderSize = plugin.getSettings().getServerGroups().size();
int position = player.getServerGroupPosition(plugin);
return position >= 0
? String.format("%0" + Integer.toString(orderSize).length() + "d", position)
: String.valueOf(orderSize);
}),
SERVER_GROUP_NAME(TabPlayer::getServerGroup);
private final BiFunction<TabPlayer, Velocitab, String> elementResolver;
SortableElement(@NotNull BiFunction<TabPlayer, Velocitab, String> elementResolver) {
this.elementResolver = elementResolver;
}
@NotNull
private String resolve(@NotNull TabPlayer tabPlayer, @NotNull Velocitab plugin) {
return elementResolver.apply(tabPlayer, plugin);
}
public static Optional<SortableElement> parse(@NotNull String s) {
return Arrays.stream(values()).filter(element -> element.name().equalsIgnoreCase(s)).findFirst();
}
}
}

View File

@ -1,78 +0,0 @@
/*
* This file is part of Velocitab, licensed under the Apache License 2.0.
*
* Copyright (c) William278 <will27528@gmail.com>
* Copyright (c) contributors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package net.william278.velocitab.sorting;
import com.google.common.base.Strings;
import net.william278.velocitab.Velocitab;
import net.william278.velocitab.config.Placeholder;
import net.william278.velocitab.player.TabPlayer;
import org.slf4j.event.Level;
import java.util.Arrays;
import java.util.List;
import java.util.concurrent.CompletableFuture;
import java.util.stream.Collectors;
public class SortingManager {
private final Velocitab plugin;
private static final String DELIMITER = ":::";
public SortingManager(Velocitab plugin) {
this.plugin = plugin;
}
public CompletableFuture<String> getTeamName(TabPlayer player) {
return Placeholder.replace(String.join(DELIMITER, plugin.getSettings().getSortingElements()), plugin, player)
.thenApply(s -> Arrays.asList(s.split(DELIMITER)))
.thenApply(v -> v.stream().map(this::adaptValue).collect(Collectors.toList()))
.thenApply(v -> handleList(player, v));
}
private String handleList(TabPlayer player, List<String> values) {
String result = String.join("", values);
if (result.length() > 12) {
result = result.substring(0, 12);
plugin.log(Level.WARN, "Sorting element list is too long, truncating to 16 characters");
}
result += player.getPlayer().getUniqueId().toString().substring(0, 4);
return result;
}
private String adaptValue(String value) {
if (value.isEmpty()) {
return "";
}
if (value.matches("[0-9]+")) {
int integer = Integer.parseInt(value);
int intSortSize = 3;
return (integer >= 0 ? 0 : 1) + String.format("%0" + intSortSize + "d", Integer.parseInt(Strings.repeat("9", intSortSize)) - Math.abs(integer));
}
if (value.length() > 6) {
return value.substring(0, 4);
}
return value;
}
}

View File

@ -38,10 +38,14 @@ import org.jetbrains.annotations.NotNull;
import java.util.List;
import java.util.Optional;
import java.util.UUID;
import java.util.concurrent.CompletableFuture;
import java.util.concurrent.ConcurrentLinkedQueue;
import java.util.concurrent.TimeUnit;
/**
* The main class for tracking the server TAB list
*/
public class PlayerTabList {
private final Velocitab plugin;
private final ConcurrentLinkedQueue<TabPlayer> players;
@ -65,22 +69,23 @@ public class PlayerTabList {
final Player joined = event.getPlayer();
plugin.getScoreboardManager().ifPresent(manager -> manager.resetCache(joined));
// Remove the player from the tracking list if they are switching servers
final RegisteredServer previousServer = event.getPreviousServer();
if (previousServer == null) {
if (previousServer != null) {
players.removeIf(player -> player.getPlayer().getUniqueId().equals(joined.getUniqueId()));
}
// Get the servers in the group from the joined server name
// If the server is not in a group, use fallback
Optional<List<String>> serversInGroup = getGroupNames(joined.getCurrentServer()
final Optional<List<String>> serversInGroup = getGroupNames(joined.getCurrentServer()
.map(ServerConnection::getServerInfo)
.map(ServerInfo::getName)
.orElse("?"));
// If the server is not in a group, use fallback.
// If fallback is disabled, permit the player to switch excluded servers without header or footer override
if (serversInGroup.isEmpty() && (previousServer != null && !this.fallbackServers.contains(previousServer.getServerInfo().getName()))) {
// If fallback is disabled, permit the player to switch excluded servers without a header or footer override
if (serversInGroup.isEmpty() &&
(previousServer != null && !this.fallbackServers.contains(previousServer.getServerInfo().getName()))) {
event.getPlayer().sendPlayerListHeaderAndFooter(Component.empty(), Component.empty());
return;
}
@ -100,22 +105,21 @@ public class PlayerTabList {
continue;
}
// Create or update TAB list entries for all players
tabList.getEntries().stream()
.filter(e -> e.getProfile().getId().equals(player.getPlayer().getUniqueId())).findFirst()
.ifPresentOrElse(
.filter(e -> e.getProfile().getId().equals(player.getPlayer().getUniqueId()))
.findFirst().ifPresentOrElse(
entry -> player.getDisplayName(plugin).thenAccept(entry::setDisplayName),
() -> createEntry(player, tabList).thenAccept(tabList::addEntry)
);
addPlayerToTabList(player, tabPlayer);
player.sendHeaderAndFooter(this);
}
plugin.getScoreboardManager().ifPresent(s -> {
s.resendAllNameTags(joined);
plugin.getTabPlayer(joined).getTeamName(plugin)
.thenAccept(t -> s.updateRole(joined, t));
plugin.getTabPlayer(joined).getTeamName(plugin).thenAccept(t -> s.updateRole(joined, t));
});
})
.delay(500, TimeUnit.MILLISECONDS)
@ -150,20 +154,24 @@ public class PlayerTabList {
@Subscribe
public void onPlayerQuit(@NotNull DisconnectEvent event) {
if (event.getLoginStatus() != DisconnectEvent.LoginStatus.SUCCESSFUL_LOGIN) return;
if (event.getLoginStatus() != DisconnectEvent.LoginStatus.SUCCESSFUL_LOGIN) {
return;
}
// Remove the player from the tracking list, Print warning if player was not removed
if (!players.removeIf(player -> player.getPlayer().getUniqueId().equals(event.getPlayer().getUniqueId()))) {
plugin.log("Failed to remove disconnecting player " + event.getPlayer().getUsername() + " (UUID: " + event.getPlayer().getUniqueId() + ")");
final UUID uuid = event.getPlayer().getUniqueId();
if (!players.removeIf(listed -> listed.getPlayer().getUniqueId().equals(uuid))) {
plugin.log(String.format("Failed to remove disconnecting player %s (UUID: %s)",
event.getPlayer().getUsername(), uuid.toString()));
}
// Remove the player from the tab list of all other players
plugin.getServer().getAllPlayers().forEach(player -> player.getTabList().removeEntry(event.getPlayer().getUniqueId()));
plugin.getServer().getAllPlayers().forEach(player -> player.getTabList().removeEntry(uuid));
// Update the tab list of all players
plugin.getServer().getScheduler()
.buildTask(plugin, () -> players.forEach(player -> {
player.getPlayer().getTabList().removeEntry(event.getPlayer().getUniqueId());
player.getPlayer().getTabList().removeEntry(uuid);
player.sendHeaderAndFooter(this);
}))
.delay(500, TimeUnit.MILLISECONDS)
@ -187,19 +195,21 @@ public class PlayerTabList {
}
tabPlayer.getTeamName(plugin).thenAccept(teamName -> {
if (teamName == null) return;
if (teamName.isBlank()) {
return;
}
plugin.getScoreboardManager().ifPresent(manager -> manager.updateRole(
tabPlayer.getPlayer(),
teamName
tabPlayer.getPlayer(), teamName
));
});
}
public void updatePlayerDisplayName(TabPlayer tabPlayer) {
Component lastDisplayName = tabPlayer.getLastDisplayname();
final Component lastDisplayName = tabPlayer.getLastDisplayname();
tabPlayer.getDisplayName(plugin).thenAccept(displayName -> {
if (displayName == null || displayName.equals(lastDisplayName)) return;
if (displayName == null || displayName.equals(lastDisplayName)) {
return;
}
players.forEach(player ->
player.getPlayer().getTabList().getEntries().stream()
@ -209,10 +219,12 @@ public class PlayerTabList {
}
// Update the display names of all listed players
public void updateDisplayNames() {
players.forEach(this::updatePlayerDisplayName);
}
// Get the component for the TAB list header
public CompletableFuture<Component> getHeader(@NotNull TabPlayer player) {
final String header = plugin.getSettings().getHeader(player.getServerGroup(plugin), player.getHeaderIndex());
player.incrementHeaderIndex(plugin);
@ -221,6 +233,7 @@ public class PlayerTabList {
.thenApply(replaced -> plugin.getFormatter().format(replaced, player, plugin));
}
// Get the component for the TAB list footer
public CompletableFuture<Component> getFooter(@NotNull TabPlayer player) {
final String footer = plugin.getSettings().getFooter(player.getServerGroup(plugin), player.getFooterIndex());
player.incrementFooterIndex(plugin);