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("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 compileOnly("net.luckperms:api:5.5") // Luckperms
implementation("com.alttd.inventory_gui:InventoryGUI:1.1.5-SNAPSHOT") 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 { tasks {
@@ -3,6 +3,7 @@ package com.alttd.chat;
import com.alttd.chat.chat_web.ChatMessageSender; import com.alttd.chat.chat_web.ChatMessageSender;
import com.alttd.chat.chat_web.handlers.WebChannelChatHandler; import com.alttd.chat.chat_web.handlers.WebChannelChatHandler;
import com.alttd.chat.chat_web.handlers.WebChatHandler; 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.commands.*;
import com.alttd.chat.config.Config; import com.alttd.chat.config.Config;
import com.alttd.chat.config.ServerConfig; 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.util.Utility;
import com.alttd.chat.web.SseSubscribeClient; import com.alttd.chat.web.SseSubscribeClient;
import lombok.Getter; import lombok.Getter;
import lombok.extern.slf4j.Slf4j;
import org.bukkit.Bukkit; import org.bukkit.Bukkit;
import org.bukkit.command.CommandExecutor; import org.bukkit.command.CommandExecutor;
import org.bukkit.event.Listener; import org.bukkit.event.Listener;
@@ -27,6 +29,7 @@ import org.bukkit.plugin.java.JavaPlugin;
import java.util.Objects; import java.util.Objects;
@Slf4j
public class ChatPlugin extends JavaPlugin { public class ChatPlugin extends JavaPlugin {
@Getter @Getter
@@ -50,7 +53,7 @@ public class ChatPlugin extends JavaPlugin {
DatabaseConnection.initialize(); DatabaseConnection.initialize();
serverConfig = new ServerConfig(ServerName.getServerName()); serverConfig = new ServerConfig(ServerName.getServerName());
ChatMessageSender chatMessageSender = new ChatMessageSender(chatLogHandler, chatAPI.getLuckPerms()); ChatMessageSender chatMessageSender = new ChatMessageSender(chatLogHandler, chatAPI.getLuckPerms());
registerListener(new PlayerListener(webHandler, serverConfig), registerListener(new PlayerListener(serverConfig, webHandler, this),
new ChatListener(chatMessageSender), new ChatListener(chatMessageSender),
new BookListener(), new BookListener(),
new ShutdownListener(chatLogHandler, this) new ShutdownListener(chatLogHandler, this)
@@ -103,6 +106,8 @@ public class ChatPlugin extends JavaPlugin {
private void registerWebHandlers(ChatMessageSender chatMessageSender, SseSubscribeClient sseSubscribeClient) { private void registerWebHandlers(ChatMessageSender chatMessageSender, SseSubscribeClient sseSubscribeClient) {
sseSubscribeClient.register("web_chat", new WebChatHandler(chatMessageSender)); 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 @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; package com.alttd.chat.listeners;
import com.alttd.chat.ChatPlugin;
import com.alttd.chat.config.Config; import com.alttd.chat.config.Config;
import com.alttd.chat.config.ServerConfig; import com.alttd.chat.config.ServerConfig;
import com.alttd.chat.database.Queries; 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.objects.chat_log.WebHandler;
import com.alttd.chat.util.GalaxyUtility; import com.alttd.chat.util.GalaxyUtility;
import com.alttd.chat.util.Utility; import com.alttd.chat.util.Utility;
import lombok.RequiredArgsConstructor;
import net.kyori.adventure.text.Component; import net.kyori.adventure.text.Component;
import net.kyori.adventure.text.TextReplacementConfig; import net.kyori.adventure.text.TextReplacementConfig;
import net.kyori.adventure.text.format.Style; import net.kyori.adventure.text.format.Style;
@@ -37,15 +39,12 @@ import java.util.Stack;
import java.util.UUID; import java.util.UUID;
import java.util.concurrent.TimeUnit; import java.util.concurrent.TimeUnit;
@RequiredArgsConstructor
public class PlayerListener implements Listener { public class PlayerListener implements Listener {
private final ServerConfig serverConfig; private final ServerConfig serverConfig;
private final WebHandler webHandler; private final WebHandler webHandler;
private final ChatPlugin chatPlugin;
public PlayerListener(WebHandler webHandler, ServerConfig serverConfig) {
this.webHandler = webHandler;
this.serverConfig = serverConfig;
}
@EventHandler @EventHandler
private void onPlayerLogin(PlayerJoinEvent event) { private void onPlayerLogin(PlayerJoinEvent event) {
@@ -62,7 +61,7 @@ public class PlayerListener implements Listener {
ChatUser user = ChatUserManager.getChatUser(uuid); ChatUser user = ChatUserManager.getChatUser(uuid);
if (user != null) { if (user != null) {
updateServerState(event.getPlayer(), user, true); updateServerState(user);
return; return;
} }
@@ -70,7 +69,7 @@ public class PlayerListener implements Listener {
ChatUser chatUser = new ChatUser(uuid, -1, null); ChatUser chatUser = new ChatUser(uuid, -1, null);
ChatUserManager.addUser(chatUser); ChatUserManager.addUser(chatUser);
Queries.saveUser(chatUser); Queries.saveUser(chatUser);
updateServerState(event.getPlayer(), chatUser, true); updateServerState(chatUser);
//TODO load player on other servers with plugin message? //TODO load player on other servers with plugin message?
} }
@@ -79,25 +78,18 @@ public class PlayerListener implements Listener {
UUID uuid = event.getPlayer().getUniqueId(); UUID uuid = event.getPlayer().getUniqueId();
ChatUser user = ChatUserManager.getChatUser(uuid); ChatUser user = ChatUserManager.getChatUser(uuid);
ChatUserManager.removeUser(user); ChatUserManager.removeUser(user);
updateServerState(event.getPlayer(), user, false); updateServerState(user);
} }
private void updateServerState(Player triggerPlayer, ChatUser chatUser, boolean add) { private void updateServerState(ChatUser chatUser) {
ArrayList<Player> playerList = new ArrayList<>(Bukkit.getOnlinePlayers()); Bukkit.getScheduler().runTaskLaterAsynchronously(chatPlugin, () -> {
if (add) { ArrayList<Player> playerList = new ArrayList<>(Bukkit.getOnlinePlayers());
if (playerList.stream() String name = Bukkit.getServer().getName();
.filter(player -> player.getUniqueId().equals(triggerPlayer.getUniqueId())) webHandler.updateServerState(name,
.findFirst() playerList.stream().map(player -> WebPlayerMapper.fromPlayer(player, chatUser)).toList()
.isEmpty()) { );
playerList.add(triggerPlayer); }, 20
} );//20 ticks aka 1 second delay
} 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()
);
} }
@EventHandler(ignoreCancelled = true) // untested @EventHandler(ignoreCancelled = true) // untested
+5 -4
View File
@@ -2,8 +2,9 @@ name: ChatPlugin
version: 2.0.0-SNAPSHOT version: 2.0.0-SNAPSHOT
main: com.alttd.chat.ChatPlugin main: com.alttd.chat.ChatPlugin
api-version: 1.19 api-version: 1.19
authors: [Destro, Teriuihi] authors: [ Destro, Teriuihi ]
depend: [LuckPerms] depend: [ LuckPerms, ProtocolLib ]
softdepend: [ TAB ]
loadbefore: loadbefore:
- mcMMO - mcMMO
- GriefPrevention - GriefPrevention
@@ -18,7 +19,7 @@ commands:
aliases: togglegc aliases: togglegc
message: message:
permission: command.chat.message permission: command.chat.message
aliases: [msg, tell] aliases: [ msg, tell ]
reply: reply:
permission: command.chat.message permission: command.chat.message
aliases: r aliases: r
@@ -39,6 +40,6 @@ commands:
permission: chat.command.clear-chat permission: chat.command.clear-chat
emoteslist: emoteslist:
permission: chat.command.emoteslist permission: chat.command.emoteslist
aliases: [emotes] aliases: [ emotes ]
nick: nick:
permission: chat.command.nick permission: chat.command.nick