feat: add plugin message api, GROUP_PLAYERS_ONLINE placeholder (#157)

* Added plugin message api & added LOCAL_GROUP_PLAYERS_ONLINE placeholders

* Fixed conversations, added placeholders to docs and fixed a few bugs

* Solved conversation

* Fixed possible charset problem and moved channels to a map instead of a set

* Changed docs

* Fixed kick issue and fixed problem header/footer on join
This commit is contained in:
AlexDev_ 2024-02-10 00:58:15 +01:00 committed by GitHub
parent a5940e0315
commit 4efc5797b3
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
13 changed files with 232 additions and 42 deletions

View File

@ -2,6 +2,8 @@ The Velocitab API provides methods for vanishing ("hiding") and modifying userna
The API is distributed on Maven through [repo.william278.net](https://repo.william278.net/#/releases/net/william278/velocitab/) and can be included in any Maven, Gradle, etc. project. JavaDocs are [available here](https://repo.william278.net/javadoc/releases/net/william278/velocitab/latest).
Velocitab also provides a plugin message API, which is documented in the [[Plugin Message API Examples]] page.
## Compatibility
[![Maven](https://repo.william278.net/api/badge/latest/releases/net/william278/velocitab?color=00fb9a&name=Maven&prefix=v)](https://repo.william278.net/#/releases/net/william278/velocitab/)

View File

@ -18,6 +18,7 @@ Please click through to the topic you wish to read about.
* 🖼️ [[Custom Logos]]
* 📦 [[API]]
* 📝 [[API Examples]]
* 📝 [[Plugin Message API Examples]]
## Links
* 💻 [GitHub](https://github.com/WiIIiam278/Velocitab)

View File

@ -3,26 +3,28 @@ Velocitab supports a number of Placeholders that will be replaced with their res
## Default placeholders
Placeholders can be included in the header, footer and player name format of the TAB list. The following placeholders are supported out of the box:
| Placeholder | Description | Example |
|--------------------------|------------------------------------------------------|--------------------|
| `%players_online%` | Players online on the proxy | `6` |
| `%max_players_online%` | Player capacity of the proxy | `500` |
| `%local_players_online%` | Players online on the server the player is on | `3` |
| `%current_date%` | Current real-world date of the server | `24 Feb 2023` |
| `%current_time%` | Current real-world time of the server | `21:45:32` |
| `%username%` | The player's username | `William278` |
| `%username_lower%` | The player's username, in lowercase | `william278` |
| `%server%` | Name of the server the player is on | `alpha` |
| `%ping%` | Ping of the player (in ms) | `6` |
| `%prefix%` | The player's prefix (from LuckPerms) | `&4[Admin]` |
| `%suffix%` | The player's suffix (from LuckPerms) | `&c ` |
| `%role%` | The player's primary LuckPerms group name | `admin` |
| `%role_display_name%` | The player's primary LuckPerms group display name | `Admin` |
| `%role_weight%` | Comparable-formatted primary LuckPerms group weight. | `100` |
| `%luckperms_meta_(key)%` | Formats a meta key from the user's LuckPerms group | (varies) |
| `%server_group%` | The name of the server group the player is on | `default` |
| `%server_group_index%` | Indexed order of the server group in the list | `0` |
| `%debug_team_name%` | (Debug) Player's team name, used for [[Sorting]] | `1alphaWilliam278` |
| Placeholder | Description | Example |
|---------------------------------|------------------------------------------------------|--------------------|
| `%players_online%` | Players online on the proxy | `6` |
| `%max_players_online%` | Player capacity of the proxy | `500` |
| `%local_players_online%` | Players online on the server the player is on | `3` |
| `%group_players_online_(name)%` | Players online on the group provided | `11` |
| `%group_players_online%` | Players online on player's group | `15` |
| `%current_date%` | Current real-world date of the server | `24 Feb 2023` |
| `%current_time%` | Current real-world time of the server | `21:45:32` |
| `%username%` | The player's username | `William278` |
| `%username_lower%` | The player's username, in lowercase | `william278` |
| `%server%` | Name of the server the player is on | `alpha` |
| `%ping%` | Ping of the player (in ms) | `6` |
| `%prefix%` | The player's prefix (from LuckPerms) | `&4[Admin]` |
| `%suffix%` | The player's suffix (from LuckPerms) | `&c ` |
| `%role%` | The player's primary LuckPerms group name | `admin` |
| `%role_display_name%` | The player's primary LuckPerms group display name | `Admin` |
| `%role_weight%` | Comparable-formatted primary LuckPerms group weight. | `100` |
| `%luckperms_meta_(key)%` | Formats a meta key from the user's LuckPerms group | (varies) |
| `%server_group%` | The name of the server group the player is on | `default` |
| `%server_group_index%` | Indexed order of the server group in the list | `0` |
| `%debug_team_name%` | (Debug) Player's team name, used for [[Sorting]] | `1alphaWilliam278` |
### Customising server display names
You can make use of the `server_display_names` feature in `config.yml` to customise how server display name appear when using the `%server%` placeholder. In the below example, if a user is connected to a server with the name "`very-long-server-`name" and the player name format for the group that server belongs to includes a `%server%` placeholder, the placeholder would be replaced with "`VSLN`" instead of the full server name.

View File

@ -0,0 +1,27 @@
Velocitab provides a plugin message API.
## API Requests from Backend Plugins
### 1 Changing player's username in the TAB List
To change a player's username in the tablist, you can send a plugin message with the channel `velocitab:main` and as data `UPDATE_CUSTOM_NAME:::customName`.
Remember to replace `customName` with the desired name.
<details>
<summary>Example &mdash; Changing player's username in the TAB List</summary>
```java
player.sendPluginMessage(plugin, "velocitab:update_custom_name", "Steve".getBytes());
```
</details>
### 2 Update team color
To change a player's team color in the TAB List, you can send a plugin message with the channel `velocitab:main` and as data `UPDATE_TEAM_COLOR:::teamColor`.
You can only use legacy color codes, for example `a` for green, `b` for aqua, etc.
This option overrides the glow effect if set
<details>
<summary>Example &mdash; Changing player's team color</summary>
```java
player.sendPluginMessage(plugin, "velocitab:update_team_color", "a".getBytes());
```
</details>

View File

@ -34,6 +34,7 @@ import lombok.Getter;
import lombok.Setter;
import net.william278.desertwell.util.UpdateChecker;
import net.william278.desertwell.util.Version;
import net.william278.velocitab.api.PluginMessageAPI;
import net.william278.velocitab.api.VelocitabAPI;
import net.william278.velocitab.commands.VelocitabCommand;
import net.william278.velocitab.config.ConfigProvider;
@ -86,6 +87,7 @@ public class Velocitab implements ConfigProvider, ScoreboardProvider, LoggerProv
private SortingManager sortingManager;
private VanishManager vanishManager;
private PacketEventManager packetEventManager;
private PluginMessageAPI pluginMessageAPI;
@Inject
public Velocitab(@NotNull ProxyServer server, @NotNull Logger logger, @DataDirectory Path configDirectory) {
@ -114,7 +116,7 @@ public class Velocitab implements ConfigProvider, ScoreboardProvider, LoggerProv
server.getScheduler().tasksByPlugin(this).forEach(ScheduledTask::cancel);
disableScoreboardManager();
getLuckPermsHook().ifPresent(LuckPermsHook::closeEvent);
VelocitabAPI.unregister();
unregisterAPI();
logger.info("Successfully disabled Velocitab");
}
@ -149,6 +151,19 @@ public class Velocitab implements ConfigProvider, ScoreboardProvider, LoggerProv
private void prepareAPI() {
VelocitabAPI.register(this);
if (settings.isEnablePluginMessageApi()) {
pluginMessageAPI = new PluginMessageAPI(this);
pluginMessageAPI.registerChannel();
getLogger().info("Registered Velocitab Plugin Message API");
}
getLogger().info("Registered Velocitab API");
}
private void unregisterAPI() {
VelocitabAPI.unregister();
if (pluginMessageAPI != null) {
pluginMessageAPI.unregisterChannel();
}
}
private void registerCommands() {

View File

@ -0,0 +1,124 @@
/*
* 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.api;
import com.google.common.collect.Maps;
import com.velocitypowered.api.event.connection.PluginMessageEvent;
import com.velocitypowered.api.proxy.Player;
import com.velocitypowered.api.proxy.ServerConnection;
import com.velocitypowered.api.proxy.messages.MinecraftChannelIdentifier;
import net.william278.velocitab.Velocitab;
import net.william278.velocitab.packet.UpdateTeamsPacket;
import net.william278.velocitab.player.TabPlayer;
import org.jetbrains.annotations.NotNull;
import java.util.*;
public class PluginMessageAPI {
private final Velocitab plugin;
private final Map<String, MinecraftChannelIdentifier> channels;
public PluginMessageAPI(@NotNull Velocitab plugin) {
this.plugin = plugin;
this.channels = Maps.newHashMap();
}
public void registerChannel() {
Arrays.stream(PluginMessageRequest.values())
.map(PluginMessageRequest::name)
.map(s -> s.toLowerCase(Locale.ENGLISH))
.forEach(request -> {
final String channelName = "velocitab:" + request;
final MinecraftChannelIdentifier channel = MinecraftChannelIdentifier.from(channelName);
channels.put(channelName, channel);
plugin.getServer().getChannelRegistrar().register(channel);
});
plugin.getServer().getEventManager().register(plugin, PluginMessageEvent.class, this::onPluginMessage);
}
public void unregisterChannel() {
channels.forEach((name, channel) -> plugin.getServer().getChannelRegistrar().unregister(channel));
}
private void onPluginMessage(@NotNull PluginMessageEvent pluginMessageEvent) {
final Optional<MinecraftChannelIdentifier> channel = Optional.ofNullable(channels.get(pluginMessageEvent.getIdentifier().getId()));
if (channel.isEmpty()) {
return;
}
if (!(pluginMessageEvent.getSource() instanceof ServerConnection serverConnection)) {
return;
}
final Player player = serverConnection.getPlayer();
final Optional<TabPlayer> optionalTabPlayer = plugin.getTabList().getTabPlayer(player);
if (optionalTabPlayer.isEmpty()) {
return;
}
final TabPlayer tabPlayer = optionalTabPlayer.get();
if (!tabPlayer.isLoaded()) {
return;
}
final Optional<PluginMessageRequest> request = PluginMessageRequest.get(channel.get());
if (request.isEmpty()) {
return;
}
final String data = new String(pluginMessageEvent.getData());
handleAPIRequest(tabPlayer, request.get(), data);
}
private void handleAPIRequest(@NotNull TabPlayer tabPlayer, @NotNull PluginMessageRequest request, @NotNull String arg) {
switch (request) {
case UPDATE_CUSTOM_NAME -> {
tabPlayer.setCustomName(arg);
plugin.getTabList().updatePlayer(tabPlayer, true);
}
case UPDATE_TEAM_COLOR -> {
final String clean = arg.replaceAll("&", "").replaceAll("§", "");
if (clean.isEmpty()) {
return;
}
final char colorChar = clean.charAt(0);
final Optional<UpdateTeamsPacket.TeamColor> color = Arrays.stream(UpdateTeamsPacket.TeamColor.values())
.filter(teamColor -> teamColor.colorChar() == colorChar)
.findFirst();
color.ifPresent(teamColor -> {
tabPlayer.setTeamColor(teamColor);
plugin.getTabList().updatePlayer(tabPlayer, true);
});
}
}
}
private enum PluginMessageRequest {
UPDATE_CUSTOM_NAME,
UPDATE_TEAM_COLOR;
public static Optional<PluginMessageRequest> get(@NotNull MinecraftChannelIdentifier channelIdentifier) {
return Arrays.stream(values())
.filter(request -> request.name().equalsIgnoreCase(channelIdentifier.getName()))
.findFirst();
}
}
}

View File

@ -45,6 +45,10 @@ public enum Placeholder {
.map(RegisteredServer::getPlayersConnected)
.map(players -> Integer.toString(players.size()))
.orElse("")),
GROUP_PLAYERS_ONLINE_((param, plugin, player) -> plugin.getTabGroups().getGroup(param)
.map(group -> Integer.toString(group.getPlayers(plugin).size()))
.orElse("Group " + param + " not found")),
GROUP_PLAYERS_ONLINE((plugin, player) -> Integer.toString(player.getGroup().getPlayers(plugin).size())),
CURRENT_DATE((plugin, player) -> DateTimeFormatter.ofPattern("dd MMM yyyy").format(LocalDateTime.now())),
CURRENT_TIME((plugin, player) -> DateTimeFormatter.ofPattern("HH:mm:ss").format(LocalDateTime.now())),
USERNAME((plugin, player) -> player.getCustomName().orElse(player.getPlayer().getUsername())),

View File

@ -90,6 +90,9 @@ public class Settings implements ConfigValidator {
@Comment("Remove gamemode spectator effect for other players in the TAB list.")
private boolean removeSpectatorEffect = true;
@Comment("Whether to enable the Plugin Message API (allows backend plugins to perform certain operations)")
private boolean enablePluginMessageApi = true;
/**
* Get display name for the server
*

View File

@ -32,6 +32,7 @@ import org.jetbrains.annotations.NotNull;
import java.util.HashMap;
import java.util.HashSet;
import java.util.List;
import java.util.Optional;
@SuppressWarnings("FieldMayBeFinal")
@Getter
@ -70,6 +71,13 @@ public class TabGroups implements ConfigValidator {
.orElseThrow(() -> new IllegalStateException("No group with name " + name + " found"));
}
@NotNull
public Optional<Group> getGroup(@NotNull String name) {
return groups.stream()
.filter(group -> group.name().equals(name))
.findFirst();
}
@NotNull
public Group getGroupFromServer(@NotNull String server) {
for (Group group : groups) {

View File

@ -195,6 +195,14 @@ public class ScoreboardManager {
return;
}
final Optional<TabPlayer> optionalTabPlayer = plugin.getTabList().getTabPlayer(p);
if (optionalTabPlayer.isEmpty()) {
return;
}
final TabPlayer targetTabPlayer = optionalTabPlayer.get();
// Prevent duplicate packets
if (roles.contains(role)) {
return;
@ -205,7 +213,7 @@ public class ScoreboardManager {
final Nametag tag = nametags.get(role);
if (tag != null) {
final UpdateTeamsPacket packet = UpdateTeamsPacket.create(
plugin, tabPlayer, role, tag, p.getUsername()
plugin, targetTabPlayer, role, tag, p.getUsername()
);
dispatchPacket(packet, player);
}

View File

@ -76,7 +76,7 @@ public class UpdateTeamsPacket implements MinecraftPacket {
.friendlyFlags(List.of(FriendlyFlag.CAN_HURT_FRIENDLY))
.nametagVisibility(isNametagPresent(nametag, plugin) ? NametagVisibility.ALWAYS : NametagVisibility.NEVER)
.collisionRule(tabPlayer.getGroup().collisions() ? CollisionRule.ALWAYS : CollisionRule.NEVER)
.color(getLastColor(nametag.prefix(), plugin))
.color(getLastColor(tabPlayer, nametag.prefix(), plugin))
.prefix(nametag.getPrefixComponent(plugin, tabPlayer))
.suffix(nametag.getSuffixComponent(plugin, tabPlayer))
.entities(Arrays.asList(teamMembers));
@ -101,7 +101,7 @@ public class UpdateTeamsPacket implements MinecraftPacket {
.friendlyFlags(List.of(FriendlyFlag.CAN_HURT_FRIENDLY))
.nametagVisibility(isNametagPresent(nametag, plugin) ? NametagVisibility.ALWAYS : NametagVisibility.NEVER)
.collisionRule(tabPlayer.getGroup().collisions() ? CollisionRule.ALWAYS : CollisionRule.NEVER)
.color(getLastColor(nametag.prefix(), plugin))
.color(getLastColor(tabPlayer, nametag.prefix(), plugin))
.prefix(nametag.getPrefixComponent(plugin, tabPlayer))
.suffix(nametag.getSuffixComponent(plugin, tabPlayer));
}
@ -131,7 +131,11 @@ public class UpdateTeamsPacket implements MinecraftPacket {
.mode(UpdateMode.REMOVE_TEAM);
}
public static int getLastColor(@Nullable String text, @NotNull Velocitab plugin) {
public static int getLastColor(@NotNull TabPlayer tabPlayer, @Nullable String text, @NotNull Velocitab plugin) {
if (tabPlayer.getTeamColor() != null) {
text = "&" + tabPlayer.getTeamColor().colorChar();
}
if (text == null) {
return 15;
}
@ -177,6 +181,7 @@ public class UpdateTeamsPacket implements MinecraftPacket {
ITALIC('f', 20),
RESET('r', 21);
@Getter
private final char colorChar;
private final int id;

View File

@ -22,10 +22,12 @@ package net.william278.velocitab.player;
import com.velocitypowered.api.proxy.Player;
import lombok.Getter;
import lombok.Setter;
import lombok.ToString;
import net.kyori.adventure.text.Component;
import net.william278.velocitab.Velocitab;
import net.william278.velocitab.config.Group;
import net.william278.velocitab.config.Placeholder;
import net.william278.velocitab.packet.UpdateTeamsPacket;
import net.william278.velocitab.tab.Nametag;
import net.william278.velocitab.tab.PlayerTabList;
import org.apache.commons.lang3.ObjectUtils;
@ -36,6 +38,7 @@ import java.util.Optional;
import java.util.concurrent.CompletableFuture;
@Getter
@ToString
public final class TabPlayer implements Comparable<TabPlayer> {
private final Player player;
@ -49,6 +52,9 @@ public final class TabPlayer implements Comparable<TabPlayer> {
private String teamName;
@Nullable
@Setter
private UpdateTeamsPacket.TeamColor teamColor;
@Nullable
@Setter
private String customName;
@Nullable
@Setter
@ -177,19 +183,4 @@ public final class TabPlayer implements Comparable<TabPlayer> {
public boolean equals(Object obj) {
return obj instanceof TabPlayer other && player.getUniqueId().equals(other.player.getUniqueId());
}
@Override
public String toString() {
return "TabPlayer{" +
"player=" + player +
", role=" + role +
", headerIndex=" + headerIndex +
", footerIndex=" + footerIndex +
", lastDisplayname=" + lastDisplayName +
", teamName='" + teamName + '\'' +
", lastServer='" + lastServer + '\'' +
", group=" + group.name() +
", loaded=" + loaded +
'}';
}
}

View File

@ -57,7 +57,7 @@ public class TabListListener {
event.getPlayer().getTabList().clearAll();
event.getPlayer().getTabList().clearHeaderAndFooter();
if (event.getResult() instanceof KickedFromServerEvent.DisconnectPlayer) {
if (event.getResult() instanceof KickedFromServerEvent.DisconnectPlayer || event.getResult() instanceof KickedFromServerEvent.RedirectPlayer) {
tabList.removePlayer(event.getPlayer());
}
}
@ -77,7 +77,7 @@ public class TabListListener {
// 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
if (isDefault && !plugin.getSettings().isFallbackEnabled()) {
if (isDefault && !plugin.getSettings().isFallbackEnabled() && event.getPreviousServer() != null) {
final Optional<TabPlayer> tabPlayer = tabList.getTabPlayer(joined);
if (tabPlayer.isEmpty()) {
return;