Refactor PlayerListener and add delayed server state updates for web integration.

This commit is contained in:
2026-08-22 21:08:31 +02:00
parent 9e647f83a8
commit b98dd65203
6 changed files with 234 additions and 30 deletions
@@ -0,0 +1,15 @@
package com.alttd.chat.web.handler_class;
import lombok.Data;
import lombok.NoArgsConstructor;
import java.util.List;
import java.util.UUID;
@Data
@NoArgsConstructor
public class PlayerListState {
private List<UUID> activePlayers;
}
+2
View File
@@ -15,6 +15,8 @@ dependencies {
compileOnly("org.apache.commons:commons-lang3:3.17.0") // needs an alternative, already removed from upstream api and will be removed in server
compileOnly("net.luckperms:api:5.5") // Luckperms
implementation("com.alttd.inventory_gui:InventoryGUI:1.1.5-SNAPSHOT")
compileOnly("com.github.NEZNAMY", "TAB-API", "6.1.2") // TAB
compileOnly("net.dmulloy2:ProtocolLib:5.4.0") // ProtocolLib
}
tasks {
@@ -3,6 +3,7 @@ package com.alttd.chat;
import com.alttd.chat.chat_web.ChatMessageSender;
import com.alttd.chat.chat_web.handlers.WebChannelChatHandler;
import com.alttd.chat.chat_web.handlers.WebChatHandler;
import com.alttd.chat.chat_web.handlers.WebUpdatePlayerList;
import com.alttd.chat.commands.*;
import com.alttd.chat.config.Config;
import com.alttd.chat.config.ServerConfig;
@@ -20,6 +21,7 @@ import com.alttd.chat.util.ServerName;
import com.alttd.chat.util.Utility;
import com.alttd.chat.web.SseSubscribeClient;
import lombok.Getter;
import lombok.extern.slf4j.Slf4j;
import org.bukkit.Bukkit;
import org.bukkit.command.CommandExecutor;
import org.bukkit.event.Listener;
@@ -27,6 +29,7 @@ import org.bukkit.plugin.java.JavaPlugin;
import java.util.Objects;
@Slf4j
public class ChatPlugin extends JavaPlugin {
@Getter
@@ -50,7 +53,7 @@ public class ChatPlugin extends JavaPlugin {
DatabaseConnection.initialize();
serverConfig = new ServerConfig(ServerName.getServerName());
ChatMessageSender chatMessageSender = new ChatMessageSender(chatLogHandler, chatAPI.getLuckPerms());
registerListener(new PlayerListener(webHandler, serverConfig),
registerListener(new PlayerListener(serverConfig, webHandler, this),
new ChatListener(chatMessageSender),
new BookListener(),
new ShutdownListener(chatLogHandler, this)
@@ -103,6 +106,8 @@ public class ChatPlugin extends JavaPlugin {
private void registerWebHandlers(ChatMessageSender chatMessageSender, SseSubscribeClient sseSubscribeClient) {
sseSubscribeClient.register("web_chat", new WebChatHandler(chatMessageSender));
//TODO [Stijn] [2026-08-22]: send to velocity as well so it can add the players to the tab command
sseSubscribeClient.register("player_state", new WebUpdatePlayerList());
}
@Override
@@ -0,0 +1,189 @@
package com.alttd.chat.chat_web.handlers;
import com.alttd.chat.managers.ChatUserManager;
import com.alttd.chat.objects.ChatUser;
import com.alttd.chat.web.WebHandler;
import com.alttd.chat.web.handler_class.PlayerListState;
import com.comphenix.protocol.PacketType;
import com.comphenix.protocol.ProtocolLibrary;
import com.comphenix.protocol.ProtocolManager;
import com.comphenix.protocol.events.PacketContainer;
import com.comphenix.protocol.reflect.StructureModifier;
import com.comphenix.protocol.wrappers.EnumWrappers;
import com.comphenix.protocol.wrappers.PlayerInfoData;
import com.comphenix.protocol.wrappers.WrappedChatComponent;
import com.comphenix.protocol.wrappers.WrappedGameProfile;
import lombok.extern.slf4j.Slf4j;
import me.neznamy.tab.api.TabAPI;
import me.neznamy.tab.api.TabPlayer;
import org.bukkit.Bukkit;
import org.bukkit.OfflinePlayer;
import org.bukkit.entity.Player;
import org.jspecify.annotations.NonNull;
import java.util.*;
@Slf4j
public final class WebUpdatePlayerList implements WebHandler<PlayerListState> {
private final ProtocolManager protocolManager = ProtocolLibrary.getProtocolManager();
private final Map<UUID, FakeTabPlayer> registeredPlayers = new HashMap<>();
@Override
public Class<PlayerListState> type() {
return PlayerListState.class;
}
@Override
public synchronized void handle(PlayerListState playerListState) {
Set<UUID> bukkitOnline = new HashSet<>();
for (Player player : Bukkit.getOnlinePlayers()) {
bukkitOnline.add(player.getUniqueId());
}
Map<UUID, FakeTabPlayer> desiredPlayers = createDesiredPlayers(playerListState, bukkitOnline);
removeInactiveOrOnlinePlayers(desiredPlayers);
createNewPlayers(desiredPlayers);
updateTab(desiredPlayers);
}
private static void updateTab(Map<UUID, FakeTabPlayer> desiredPlayers) {
TabAPI tabAPI;
try {
tabAPI = TabAPI.getInstance();
} catch (IllegalStateException e) {
log.error("Failed to get TabAPI instance", e);
return;
}
for (TabPlayer onlinePlayer : tabAPI.getOnlinePlayers()) {
if (!desiredPlayers.containsKey(onlinePlayer.getUniqueId())) {
if (onlinePlayer.getGroup().equals("web")) {
onlinePlayer.setTemporaryGroup(null);
}
continue;
}
if (onlinePlayer.getGroup().equals("web")) {
continue;
}
onlinePlayer.setTemporaryGroup("web");
}
}
private static @NonNull Map<UUID, FakeTabPlayer> createDesiredPlayers(PlayerListState playerListState, Set<UUID> bukkitOnline) {
Map<UUID, FakeTabPlayer> desiredPlayers = new LinkedHashMap<>();
for (ChatUser chatUser : playerListState.getActivePlayers()
.stream()
.map(ChatUserManager::getChatUser)
.toList()) {
if (chatUser == null || chatUser.getUuid() == null) {
log.warn("PlayerListState contained a null ChatUser or ChatUser with null UUID");
continue;
}
UUID uuid = chatUser.getUuid();
if (bukkitOnline.contains(uuid)) {
continue;
}
desiredPlayers.putIfAbsent(
uuid,
new FakeTabPlayer(uuid, getOfflinePlayerName(uuid))
);
}
return desiredPlayers;
}
private void createNewPlayers(Map<UUID, FakeTabPlayer> desiredPlayers) {
for (FakeTabPlayer desired : desiredPlayers.values()) {
FakeTabPlayer existing = registeredPlayers.get(desired.uuid());
if (existing == null) {
addFakePlayer(desired, Bukkit.getOnlinePlayers());
registeredPlayers.put(desired.uuid(), desired);
continue;
}
removeFakePlayers(Set.of(desired.uuid()), Bukkit.getOnlinePlayers());
addFakePlayer(desired, Bukkit.getOnlinePlayers());
registeredPlayers.put(desired.uuid(), desired);
}
}
private void removeInactiveOrOnlinePlayers(Map<UUID, FakeTabPlayer> desiredPlayers) {
Set<UUID> toRemove = new HashSet<>(registeredPlayers.keySet());
toRemove.removeAll(desiredPlayers.keySet());
if (!toRemove.isEmpty()) {
removeFakePlayers(toRemove, Bukkit.getOnlinePlayers());
toRemove.forEach(registeredPlayers::remove);
}
}
private void addFakePlayer(FakeTabPlayer fakePlayer, Collection<? extends Player> viewers) {
PacketContainer packet = protocolManager.createPacket(PacketType.Play.Server.PLAYER_INFO);
packet.getPlayerInfoActions().write(
0,
EnumSet.of(
EnumWrappers.PlayerInfoAction.ADD_PLAYER,
EnumWrappers.PlayerInfoAction.UPDATE_LISTED,
EnumWrappers.PlayerInfoAction.UPDATE_DISPLAY_NAME,
EnumWrappers.PlayerInfoAction.UPDATE_GAME_MODE,
EnumWrappers.PlayerInfoAction.UPDATE_LATENCY
)
);
WrappedGameProfile profile = new WrappedGameProfile(
fakePlayer.uuid(),
fakePlayer.name()
);
PlayerInfoData data = new PlayerInfoData(
fakePlayer.uuid(),
0,
true,
EnumWrappers.NativeGameMode.SURVIVAL,
profile,
WrappedChatComponent.fromText(fakePlayer.name())
);
writePlayerInfoData(packet, List.of(data));
send(packet, viewers);
}
private void removeFakePlayers(Set<UUID> uuids, Collection<? extends Player> viewers) {
if (uuids.isEmpty() || viewers.isEmpty()) {
return;
}
PacketContainer packet = protocolManager.createPacket(PacketType.Play.Server.PLAYER_INFO_REMOVE);
packet.getUUIDLists().write(0, new ArrayList<>(uuids));
send(packet, viewers);
}
private static void writePlayerInfoData(PacketContainer packet, List<PlayerInfoData> data) {
StructureModifier<List<PlayerInfoData>> modifier = packet.getPlayerInfoDataLists();
if (modifier.size() == 0) {
throw new IllegalStateException("PLAYER_INFO packet has no PlayerInfoData list");
}
modifier.write(modifier.size() - 1, data);
}
private void send(PacketContainer packet, Collection<? extends Player> viewers) {
for (Player viewer : viewers) {
protocolManager.sendServerPacket(viewer, packet);
}
}
private static String getOfflinePlayerName(UUID uuid) {
OfflinePlayer offlinePlayer = Bukkit.getOfflinePlayer(uuid);
return offlinePlayer.getName() == null ? offlinePlayer.getUniqueId().toString() : offlinePlayer.getName();
}
private record FakeTabPlayer(UUID uuid, String name) {
}
}
@@ -1,5 +1,6 @@
package com.alttd.chat.listeners;
import com.alttd.chat.ChatPlugin;
import com.alttd.chat.config.Config;
import com.alttd.chat.config.ServerConfig;
import com.alttd.chat.database.Queries;
@@ -11,6 +12,7 @@ import com.alttd.chat.objects.Toggleable;
import com.alttd.chat.objects.chat_log.WebHandler;
import com.alttd.chat.util.GalaxyUtility;
import com.alttd.chat.util.Utility;
import lombok.RequiredArgsConstructor;
import net.kyori.adventure.text.Component;
import net.kyori.adventure.text.TextReplacementConfig;
import net.kyori.adventure.text.format.Style;
@@ -37,15 +39,12 @@ import java.util.Stack;
import java.util.UUID;
import java.util.concurrent.TimeUnit;
@RequiredArgsConstructor
public class PlayerListener implements Listener {
private final ServerConfig serverConfig;
private final WebHandler webHandler;
public PlayerListener(WebHandler webHandler, ServerConfig serverConfig) {
this.webHandler = webHandler;
this.serverConfig = serverConfig;
}
private final ChatPlugin chatPlugin;
@EventHandler
private void onPlayerLogin(PlayerJoinEvent event) {
@@ -62,7 +61,7 @@ public class PlayerListener implements Listener {
ChatUser user = ChatUserManager.getChatUser(uuid);
if (user != null) {
updateServerState(event.getPlayer(), user, true);
updateServerState(user);
return;
}
@@ -70,7 +69,7 @@ public class PlayerListener implements Listener {
ChatUser chatUser = new ChatUser(uuid, -1, null);
ChatUserManager.addUser(chatUser);
Queries.saveUser(chatUser);
updateServerState(event.getPlayer(), chatUser, true);
updateServerState(chatUser);
//TODO load player on other servers with plugin message?
}
@@ -79,25 +78,18 @@ public class PlayerListener implements Listener {
UUID uuid = event.getPlayer().getUniqueId();
ChatUser user = ChatUserManager.getChatUser(uuid);
ChatUserManager.removeUser(user);
updateServerState(event.getPlayer(), user, false);
updateServerState(user);
}
private void updateServerState(Player triggerPlayer, ChatUser chatUser, boolean add) {
ArrayList<Player> playerList = new ArrayList<>(Bukkit.getOnlinePlayers());
if (add) {
if (playerList.stream()
.filter(player -> player.getUniqueId().equals(triggerPlayer.getUniqueId()))
.findFirst()
.isEmpty()) {
playerList.add(triggerPlayer);
}
} else {
playerList.removeIf(player -> player.getUniqueId().equals(triggerPlayer.getUniqueId()));
}
String name = Bukkit.getServer().getName();
webHandler.updateServerState(name,
playerList.stream().map(player -> WebPlayerMapper.fromPlayer(player, chatUser)).toList()
);
private void updateServerState(ChatUser chatUser) {
Bukkit.getScheduler().runTaskLaterAsynchronously(chatPlugin, () -> {
ArrayList<Player> playerList = new ArrayList<>(Bukkit.getOnlinePlayers());
String name = Bukkit.getServer().getName();
webHandler.updateServerState(name,
playerList.stream().map(player -> WebPlayerMapper.fromPlayer(player, chatUser)).toList()
);
}, 20
);//20 ticks aka 1 second delay
}
@EventHandler(ignoreCancelled = true) // untested
+6 -5
View File
@@ -2,8 +2,9 @@ name: ChatPlugin
version: 2.0.0-SNAPSHOT
main: com.alttd.chat.ChatPlugin
api-version: 1.19
authors: [Destro, Teriuihi]
depend: [LuckPerms]
authors: [ Destro, Teriuihi ]
depend: [ LuckPerms, ProtocolLib ]
softdepend: [ TAB ]
loadbefore:
- mcMMO
- GriefPrevention
@@ -18,7 +19,7 @@ commands:
aliases: togglegc
message:
permission: command.chat.message
aliases: [msg, tell]
aliases: [ msg, tell ]
reply:
permission: command.chat.message
aliases: r
@@ -39,6 +40,6 @@ commands:
permission: chat.command.clear-chat
emoteslist:
permission: chat.command.emoteslist
aliases: [emotes]
aliases: [ emotes ]
nick:
permission: chat.command.nick
permission: chat.command.nick