From b98dd65203c1ce67296687296bd824656ff334af Mon Sep 17 00:00:00 2001 From: akastijn Date: Sat, 22 Aug 2026 21:08:31 +0200 Subject: [PATCH] Refactor `PlayerListener` and add delayed server state updates for web integration. --- .../web/handler_class/PlayerListState.java | 15 ++ galaxy/build.gradle.kts | 2 + .../main/java/com/alttd/chat/ChatPlugin.java | 7 +- .../handlers/WebUpdatePlayerList.java | 189 ++++++++++++++++++ .../alttd/chat/listeners/PlayerListener.java | 40 ++-- galaxy/src/main/resources/plugin.yml | 11 +- 6 files changed, 234 insertions(+), 30 deletions(-) create mode 100644 api/src/main/java/com/alttd/chat/web/handler_class/PlayerListState.java create mode 100644 galaxy/src/main/java/com/alttd/chat/chat_web/handlers/WebUpdatePlayerList.java diff --git a/api/src/main/java/com/alttd/chat/web/handler_class/PlayerListState.java b/api/src/main/java/com/alttd/chat/web/handler_class/PlayerListState.java new file mode 100644 index 0000000..e372eb9 --- /dev/null +++ b/api/src/main/java/com/alttd/chat/web/handler_class/PlayerListState.java @@ -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 activePlayers; + +} diff --git a/galaxy/build.gradle.kts b/galaxy/build.gradle.kts index dc7026f..c6fcb98 100644 --- a/galaxy/build.gradle.kts +++ b/galaxy/build.gradle.kts @@ -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 { diff --git a/galaxy/src/main/java/com/alttd/chat/ChatPlugin.java b/galaxy/src/main/java/com/alttd/chat/ChatPlugin.java index 4a23231..2d50ae6 100755 --- a/galaxy/src/main/java/com/alttd/chat/ChatPlugin.java +++ b/galaxy/src/main/java/com/alttd/chat/ChatPlugin.java @@ -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 diff --git a/galaxy/src/main/java/com/alttd/chat/chat_web/handlers/WebUpdatePlayerList.java b/galaxy/src/main/java/com/alttd/chat/chat_web/handlers/WebUpdatePlayerList.java new file mode 100644 index 0000000..6b49932 --- /dev/null +++ b/galaxy/src/main/java/com/alttd/chat/chat_web/handlers/WebUpdatePlayerList.java @@ -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 { + private final ProtocolManager protocolManager = ProtocolLibrary.getProtocolManager(); + private final Map registeredPlayers = new HashMap<>(); + + @Override + public Class type() { + return PlayerListState.class; + } + + @Override + public synchronized void handle(PlayerListState playerListState) { + Set bukkitOnline = new HashSet<>(); + for (Player player : Bukkit.getOnlinePlayers()) { + bukkitOnline.add(player.getUniqueId()); + } + + Map desiredPlayers = createDesiredPlayers(playerListState, bukkitOnline); + + removeInactiveOrOnlinePlayers(desiredPlayers); + + createNewPlayers(desiredPlayers); + + updateTab(desiredPlayers); + } + + private static void updateTab(Map 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 createDesiredPlayers(PlayerListState playerListState, Set bukkitOnline) { + Map 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 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 desiredPlayers) { + Set 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 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 uuids, Collection 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 data) { + StructureModifier> 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 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) { + } +} diff --git a/galaxy/src/main/java/com/alttd/chat/listeners/PlayerListener.java b/galaxy/src/main/java/com/alttd/chat/listeners/PlayerListener.java index 1d20931..42930d6 100755 --- a/galaxy/src/main/java/com/alttd/chat/listeners/PlayerListener.java +++ b/galaxy/src/main/java/com/alttd/chat/listeners/PlayerListener.java @@ -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 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 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 diff --git a/galaxy/src/main/resources/plugin.yml b/galaxy/src/main/resources/plugin.yml index c89083d..04b103c 100755 --- a/galaxy/src/main/resources/plugin.yml +++ b/galaxy/src/main/resources/plugin.yml @@ -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 \ No newline at end of file + permission: chat.command.nick