From 4efc5797b3f5d83ad7849c517888f6f8feaed26e Mon Sep 17 00:00:00 2001 From: AlexDev_ <56083016+alexdev03@users.noreply.github.com> Date: Sat, 10 Feb 2024 00:58:15 +0100 Subject: [PATCH] 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 --- docs/API.md | 2 + docs/Home.md | 1 + docs/Placeholders.md | 42 +++--- docs/Plugin-Message-API-Examples.md | 27 ++++ .../net/william278/velocitab/Velocitab.java | 17 ++- .../velocitab/api/PluginMessageAPI.java | 124 ++++++++++++++++++ .../velocitab/config/Placeholder.java | 4 + .../william278/velocitab/config/Settings.java | 3 + .../velocitab/config/TabGroups.java | 8 ++ .../velocitab/packet/ScoreboardManager.java | 10 +- .../velocitab/packet/UpdateTeamsPacket.java | 11 +- .../velocitab/player/TabPlayer.java | 21 +-- .../velocitab/tab/TabListListener.java | 4 +- 13 files changed, 232 insertions(+), 42 deletions(-) create mode 100644 docs/Plugin-Message-API-Examples.md create mode 100644 src/main/java/net/william278/velocitab/api/PluginMessageAPI.java diff --git a/docs/API.md b/docs/API.md index f2b0ea7..613c417 100644 --- a/docs/API.md +++ b/docs/API.md @@ -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/) diff --git a/docs/Home.md b/docs/Home.md index ba429c6..4141830 100644 --- a/docs/Home.md +++ b/docs/Home.md @@ -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) diff --git a/docs/Placeholders.md b/docs/Placeholders.md index 745da6d..534cc78 100644 --- a/docs/Placeholders.md +++ b/docs/Placeholders.md @@ -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. diff --git a/docs/Plugin-Message-API-Examples.md b/docs/Plugin-Message-API-Examples.md new file mode 100644 index 0000000..f3c4d73 --- /dev/null +++ b/docs/Plugin-Message-API-Examples.md @@ -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. +
+Example — Changing player's username in the TAB List + +```java +player.sendPluginMessage(plugin, "velocitab:update_custom_name", "Steve".getBytes()); +``` +
+ +### 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 + +
+Example — Changing player's team color + +```java +player.sendPluginMessage(plugin, "velocitab:update_team_color", "a".getBytes()); +``` +
\ No newline at end of file diff --git a/src/main/java/net/william278/velocitab/Velocitab.java b/src/main/java/net/william278/velocitab/Velocitab.java index fe41a2b..8eab81b 100644 --- a/src/main/java/net/william278/velocitab/Velocitab.java +++ b/src/main/java/net/william278/velocitab/Velocitab.java @@ -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() { diff --git a/src/main/java/net/william278/velocitab/api/PluginMessageAPI.java b/src/main/java/net/william278/velocitab/api/PluginMessageAPI.java new file mode 100644 index 0000000..8ea3f98 --- /dev/null +++ b/src/main/java/net/william278/velocitab/api/PluginMessageAPI.java @@ -0,0 +1,124 @@ +/* + * This file is part of Velocitab, licensed under the Apache License 2.0. + * + * Copyright (c) William278 + * 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 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 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 optionalTabPlayer = plugin.getTabList().getTabPlayer(player); + if (optionalTabPlayer.isEmpty()) { + return; + } + + final TabPlayer tabPlayer = optionalTabPlayer.get(); + if (!tabPlayer.isLoaded()) { + return; + } + + final Optional 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 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 get(@NotNull MinecraftChannelIdentifier channelIdentifier) { + return Arrays.stream(values()) + .filter(request -> request.name().equalsIgnoreCase(channelIdentifier.getName())) + .findFirst(); + } + } + +} diff --git a/src/main/java/net/william278/velocitab/config/Placeholder.java b/src/main/java/net/william278/velocitab/config/Placeholder.java index 98f39c0..6f76362 100644 --- a/src/main/java/net/william278/velocitab/config/Placeholder.java +++ b/src/main/java/net/william278/velocitab/config/Placeholder.java @@ -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())), diff --git a/src/main/java/net/william278/velocitab/config/Settings.java b/src/main/java/net/william278/velocitab/config/Settings.java index 3555492..ed3a6fb 100644 --- a/src/main/java/net/william278/velocitab/config/Settings.java +++ b/src/main/java/net/william278/velocitab/config/Settings.java @@ -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 * diff --git a/src/main/java/net/william278/velocitab/config/TabGroups.java b/src/main/java/net/william278/velocitab/config/TabGroups.java index 29c3d8a..d4d1284 100644 --- a/src/main/java/net/william278/velocitab/config/TabGroups.java +++ b/src/main/java/net/william278/velocitab/config/TabGroups.java @@ -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 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) { diff --git a/src/main/java/net/william278/velocitab/packet/ScoreboardManager.java b/src/main/java/net/william278/velocitab/packet/ScoreboardManager.java index fe7645e..0d1ac4a 100644 --- a/src/main/java/net/william278/velocitab/packet/ScoreboardManager.java +++ b/src/main/java/net/william278/velocitab/packet/ScoreboardManager.java @@ -195,6 +195,14 @@ public class ScoreboardManager { return; } + final Optional 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); } diff --git a/src/main/java/net/william278/velocitab/packet/UpdateTeamsPacket.java b/src/main/java/net/william278/velocitab/packet/UpdateTeamsPacket.java index f194607..93185eb 100644 --- a/src/main/java/net/william278/velocitab/packet/UpdateTeamsPacket.java +++ b/src/main/java/net/william278/velocitab/packet/UpdateTeamsPacket.java @@ -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; diff --git a/src/main/java/net/william278/velocitab/player/TabPlayer.java b/src/main/java/net/william278/velocitab/player/TabPlayer.java index 9ea6339..4719114 100644 --- a/src/main/java/net/william278/velocitab/player/TabPlayer.java +++ b/src/main/java/net/william278/velocitab/player/TabPlayer.java @@ -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 { private final Player player; @@ -49,6 +52,9 @@ public final class TabPlayer implements Comparable { 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 { 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 + - '}'; - } } diff --git a/src/main/java/net/william278/velocitab/tab/TabListListener.java b/src/main/java/net/william278/velocitab/tab/TabListListener.java index 8bd8982..5b19576 100644 --- a/src/main/java/net/william278/velocitab/tab/TabListListener.java +++ b/src/main/java/net/william278/velocitab/tab/TabListListener.java @@ -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 = tabList.getTabPlayer(joined); if (tabPlayer.isEmpty()) { return;