forked from Upstream/Velocitab
Fixes, logic simplification, update docs for nametags (#101)
This commit is contained in:
parent
1f1e69ebca
commit
8224cd0ff1
@ -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.
|
||||
|
||||
|
@ -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
33
docs/Nametags.md
Normal 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.
|
@ -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 — [[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 — 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`.
|
||||
|
@ -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.
|
@ -5,6 +5,7 @@
|
||||
## Documentation
|
||||
* 👥 [[Server Groups]]
|
||||
* 🎨 [[Formatting]]
|
||||
* 📛 [[Nametags]]
|
||||
* 📊 [[Sorting]]
|
||||
* ✍️ [[Placeholders]]
|
||||
* ✨ [[Animations]]
|
||||
|
BIN
images/nametags.png
Normal file
BIN
images/nametags.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 2.2 MiB |
@ -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;
|
||||
|
@ -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);
|
||||
}
|
||||
|
||||
}
|
||||
|
@ -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();
|
||||
}
|
||||
|
||||
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -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);
|
||||
}
|
||||
|
||||
|
@ -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);
|
||||
}
|
||||
|
||||
}
|
||||
|
@ -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();
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
@ -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;
|
||||
}
|
||||
}
|
@ -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);
|
||||
|
Loading…
Reference in New Issue
Block a user