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

@ -4,10 +4,12 @@ Velocitab supports a number of Placeholders that will be replaced with their res
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` |
| `%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` |

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;