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
[](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;