diff --git a/_trash/2026-05-07T19-39-23-130Z/src/main/java/net/viper/status/modules/tablist/TablistModule.java b/_trash/2026-05-07T19-39-23-130Z/src/main/java/net/viper/status/modules/tablist/TablistModule.java new file mode 100644 index 0000000..9d3b96d --- /dev/null +++ b/_trash/2026-05-07T19-39-23-130Z/src/main/java/net/viper/status/modules/tablist/TablistModule.java @@ -0,0 +1,579 @@ +package net.viper.status.modules.tablist; + +import net.md_5.bungee.api.ChatColor; +import net.md_5.bungee.api.ProxyServer; +import net.md_5.bungee.api.chat.TextComponent; +import net.md_5.bungee.api.config.ListenerInfo; +import net.md_5.bungee.api.config.ServerInfo; +import net.md_5.bungee.api.connection.ProxiedPlayer; +import net.md_5.bungee.api.event.PlayerDisconnectEvent; +import net.md_5.bungee.api.event.PostLoginEvent; +import net.md_5.bungee.api.event.ServerSwitchEvent; +import net.md_5.bungee.api.plugin.Listener; +import net.md_5.bungee.api.plugin.Plugin; +import net.md_5.bungee.api.scheduler.ScheduledTask; +import net.md_5.bungee.event.EventHandler; +import net.md_5.bungee.protocol.packet.PlayerListItem; +import net.md_5.bungee.protocol.packet.PlayerListItem.Item; +import net.md_5.bungee.protocol.packet.PlayerListItemUpdate; +import net.viper.status.module.Module; + +import java.io.File; +import java.io.FileInputStream; +import java.io.FileOutputStream; +import java.io.InputStreamReader; +import java.io.OutputStream; +import java.lang.reflect.Method; +import java.nio.charset.StandardCharsets; +import java.text.SimpleDateFormat; +import java.util.ArrayList; +import java.util.Date; +import java.util.EnumSet; +import java.util.HashSet; +import java.util.List; +import java.util.Properties; +import java.util.Set; +import java.util.UUID; +import java.util.concurrent.TimeUnit; + +/** + * TablistModule fuer StatusAPI (BungeeCord 1.19.3+). + * + * Liest die tab_size aus der BungeeCord-Konfiguration und berechnet + * ROWS/COLUMNS dynamisch: + * tab_size=60 -> 3 Spalten x 20 Zeilen + * tab_size=80 -> 4 Spalten x 20 Zeilen + * + * Layout: + * Spalte 0 = Info (Website / Name / Rank / Server / World / Time / TS) + * Spalte 1 = Spieler auf Server 1 (Lobby) + * Spalte 2 = Spieler auf Server 2 + * Spalte 3 = Spieler auf Server 3 (nur bei tab_size=80) + */ +public class TablistModule implements Module, Listener { + + private static final String CONFIG_FILE = "tablist.properties"; + + // Wird beim Start dynamisch aus BungeeCord tab_size ermittelt + private int rows = 20; + private int columns = 4; + private int total = rows * columns; + + // Fake-UUIDs – werden nach Bestimmung von total initialisiert + private UUID[] fakeUuids; + + // ── Config ───────────────────────────────────────────────────────────────── + private boolean enabled = true; + private int updateInterval = 5; + + private String headerLine1 = "&8&m" + rep('\u2501', 53); + private String headerLine2 = " &6&lViper Network"; + private String headerLine3 = "&8&m" + rep('\u2501', 53); + private String footerLine1 = "&8&m" + rep('\u2501', 53); + private String footerLine2 = " &7Discord: &ediscord.viper-network.de &8| &7Shop: &eviper-network.de/shop"; + private String footerLine3 = "&8&m" + rep('\u2501', 53); + + private String labelWebsite = "&b&lWebsite:"; + private String valueWebsite = "&fviper-network.de"; + private String labelName = "&b&lName:"; + private String labelRank = "&b&lRank:"; + private String labelServer = "&b&lServer:"; + private String labelWorld = "&b&lWorld:"; + private String labelTime = "&b&lTime:"; + private String labelTeamspeak = "&b&lTeamspeak:"; + private String valueTeamspeak = "&fts.viper-network.de"; + private String colorSrvHeader = "&6&l"; + + private String timeFormat = "HH:mm:ss / h:mm a"; + private SimpleDateFormat sdf; + private List serverOrder = new ArrayList<>(); + private Set hiddenServers = new HashSet<>(); + + // ── State ────────────────────────────────────────────────────────────────── + private Plugin plugin; + private ScheduledTask updateTask; + private Method sendPacketQueuedMethod; + private int tabSizeMax = 80; // maximale Slots laut BungeeCord tab_size + + // ══════════════════════════════════════════════════════════════════════════ + + @Override public String getName() { return "TablistModule"; } + + @Override + public void onEnable(Plugin plugin) { + this.plugin = plugin; + ensureConfigExists(); + loadConfig(); + if (!enabled) { plugin.getLogger().info("[TablistModule] Deaktiviert."); return; } + + // Tab-Size aus BungeeCord auslesen und ROWS/COLUMNS berechnen + initGridSize(); + + // Fake-UUIDs initialisieren + fakeUuids = new UUID[total]; + for (int i = 0; i < total; i++) { + fakeUuids[i] = new UUID(0xFFFEDEAD00000000L, (long) i); + } + + // Reflection-Cache fuer sendPacketQueued + try { + Class ucClass = Class.forName("net.md_5.bungee.UserConnection"); + sendPacketQueuedMethod = ucClass.getMethod("sendPacketQueued", + net.md_5.bungee.protocol.DefinedPacket.class); + sendPacketQueuedMethod.setAccessible(true); + } catch (Exception e) { + plugin.getLogger().severe("[TablistModule] sendPacketQueued nicht gefunden: " + e.getMessage()); + return; + } + + ProxyServer.getInstance().getPluginManager().registerListener(plugin, this); + updateTask = ProxyServer.getInstance().getScheduler().schedule( + plugin, this::updateAll, 2L, Math.max(1, updateInterval), TimeUnit.SECONDS); + + // Server-Erkennung loggen + ProxyServer.getInstance().getScheduler().schedule(plugin, () -> { + List all = new ArrayList<>(ProxyServer.getInstance().getServers().keySet()); + plugin.getLogger().info("[TablistModule] Alle BungeeCord-Server: " + all); + plugin.getLogger().info("[TablistModule] Tablist-Spalten (" + columns + "x" + rows + "): " + getServerOrder()); + }, 3L, TimeUnit.SECONDS); + + plugin.getLogger().info("[TablistModule] Aktiviert. Grid=" + columns + "x" + rows + + " (total=" + total + "), Interval=" + updateInterval + "s"); + } + + @Override + public void onDisable(Plugin plugin) { + if (updateTask != null) { updateTask.cancel(); updateTask = null; } + for (ProxiedPlayer p : ProxyServer.getInstance().getPlayers()) { + try { + removeFakeSlots(p); + p.setTabHeader(new TextComponent(""), new TextComponent("")); + } catch (Exception ignored) {} + } + } + + /** + * Liest tab_size aus dem ersten BungeeCord-Listener und berechnet ROWS/COLUMNS. + * Minecraft zeigt immer 20 Zeilen, die Spaltenanzahl ergibt sich aus tab_size/20. + * Fallback: 4 Spalten x 20 Zeilen = 80. + */ + private void initGridSize() { + int tabSize = 80; + try { + for (ListenerInfo li : ProxyServer.getInstance().getConfig().getListeners()) { + // getTabSize() nicht im API-Interface, daher Reflection + try { + java.lang.reflect.Method m = li.getClass().getMethod("getTabSize"); + Object val = m.invoke(li); + if (val instanceof Number && ((Number) val).intValue() > 0) { + tabSize = ((Number) val).intValue(); + break; + } + } catch (Exception ignored) {} + } + } catch (Exception e) { + plugin.getLogger().warning("[TablistModule] Konnte tab_size nicht lesen, nutze 80."); + } + rows = 20; + tabSizeMax = tabSize; + // Beim Start noch keine Server bekannt → maxColumns als Startwert, recalculateGrid korrigiert später + columns = Math.max(2, tabSize / rows); + total = rows * columns; + + // Sofort korrekt berechnen falls Server bereits bekannt + int serverCount = getServerOrder().size(); + if (serverCount > 0) { + int needed = 1 + serverCount; + columns = Math.max(2, Math.min(needed, tabSize / rows)); + total = rows * columns; + } + plugin.getLogger().info("[TablistModule] BungeeCord tab_size=" + tabSize + + " -> " + columns + " Spalten x " + rows + " Zeilen = " + total + " Slots"); + } + + // ══════════════════════════════════════════════════════════════════════════ + // Events + // ══════════════════════════════════════════════════════════════════════════ + + @EventHandler public void onLogin(PostLoginEvent e) { + if (!enabled) return; + ProxyServer.getInstance().getScheduler().schedule(plugin, + () -> updateTablist(e.getPlayer()), 3L, TimeUnit.SECONDS); + } + + @EventHandler public void onSwitch(ServerSwitchEvent e) { + if (!enabled) return; + ProxyServer.getInstance().getScheduler().schedule(plugin, + () -> updateTablist(e.getPlayer()), 1L, TimeUnit.SECONDS); + } + + @EventHandler public void onDisconnect(PlayerDisconnectEvent e) { + if (!enabled) return; + ProxyServer.getInstance().getScheduler().schedule(plugin, this::updateAll, 1L, TimeUnit.SECONDS); + } + + // ══════════════════════════════════════════════════════════════════════════ + // Core + // ══════════════════════════════════════════════════════════════════════════ + + private void updateAll() { + recalculateGrid(); + for (ProxiedPlayer p : ProxyServer.getInstance().getPlayers()) updateTablist(p); + } + + /** + * Berechnet Spaltenanzahl dynamisch anhand der aktuell sichtbaren Server. + * Spalte 0 = Info, Spalten 1..n = Server + * Minimum: 2 Spalten (Info + 1 Server) + * Maximum: durch tab_size begrenzt + */ + private void recalculateGrid() { + int serverCount = getServerOrder().size(); + int needed = 1 + serverCount; // Info-Spalte + Server-Spalten + int maxColumns = tabSizeMax / rows; + int newColumns = Math.max(2, Math.min(needed, maxColumns)); + int newTotal = rows * newColumns; + + if (newColumns == columns && newTotal == total) return; + + // Grid hat sich geändert – alte Fake-Slots bei allen Spielern entfernen + for (ProxiedPlayer p : ProxyServer.getInstance().getPlayers()) { + try { removeFakeSlots(p); } catch (Exception ignored) {} + } + columns = newColumns; + total = newTotal; + + // Fake-UUIDs neu initialisieren + fakeUuids = new UUID[total]; + for (int i = 0; i < total; i++) { + fakeUuids[i] = new UUID(0xFFFEDEAD00000000L, (long) i); + } + + plugin.getLogger().info("[TablistModule] Grid: " + + columns + " Spalten x " + rows + " Zeilen = " + total + + " Slots (" + serverCount + " Server)"); + } + + private void updateTablist(ProxiedPlayer viewer) { + if (viewer == null || !viewer.isConnected()) return; + try { + String header = c(headerLine1) + "\n" + c(headerLine2) + "\n" + c(headerLine3); + String footer = c(footerLine1) + "\n" + c(footerLine2) + "\n" + c(footerLine3); + viewer.setTabHeader(new TextComponent(header), new TextComponent(footer)); + hideRealPlayers(viewer); + sendSlots(viewer, buildItems(viewer)); + } catch (Exception ex) { + plugin.getLogger().warning("[TablistModule] Fehler fuer " + viewer.getName() + ": " + ex.getMessage()); + } + } + + /** + * Setzt alle echten Spieler-Slots auf listed=false damit sie in der Tablist + * unsichtbar werden und unsere Fake-Slots nicht verschieben. + */ + @SuppressWarnings("unchecked") + private void hideRealPlayers(ProxiedPlayer viewer) { + if (sendPacketQueuedMethod == null) return; + try { + java.util.Collection online = ProxyServer.getInstance().getPlayers(); + if (online.isEmpty()) return; + + PlayerListItemUpdate pkt = new PlayerListItemUpdate(); + pkt.setActions(EnumSet.of(PlayerListItemUpdate.Action.UPDATE_LISTED)); + + Item[] items = new Item[online.size()]; + int idx = 0; + for (ProxiedPlayer p : online) { + Item item = new Item(); + item.setUuid(p.getUniqueId()); + item.setListed(false); + items[idx++] = item; + } + pkt.setItems(items); + sendPacketQueuedMethod.invoke(viewer, pkt); + } catch (Exception e) { + plugin.getLogger().warning("[TablistModule] hideRealPlayers: " + e.getMessage()); + } + } + + // ══════════════════════════════════════════════════════════════════════════ + // Item-Matrix aufbauen + // ══════════════════════════════════════════════════════════════════════════ + + private Item[] buildItems(ProxiedPlayer viewer) { + String[] texts = new String[total]; + net.md_5.bungee.protocol.data.Property[][] skins = new net.md_5.bungee.protocol.data.Property[total][]; + int[] pings = new int[total]; + for (int i = 0; i < total; i++) { + texts[i] = " "; + skins[i] = new net.md_5.bungee.protocol.data.Property[0]; + pings[i] = 0; // Fake-Ping fuer leere Slots + } + + // ── Spalte 0: Info ──────────────────────────────────────────────────── + int base = 0, row = 0; + row = set(texts, base, row, c(labelWebsite)); + row = set(texts, base, row, c(valueWebsite)); + row = set(texts, base, row, " "); + row = set(texts, base, row, c(labelName)); + row = set(texts, base, row, c("&f" + viewer.getName())); + row = set(texts, base, row, " "); + row = set(texts, base, row, c(labelRank)); + row = set(texts, base, row, c("&f" + getRank(viewer))); + row = set(texts, base, row, " "); + String srv = viewer.getServer() != null ? capitalize(viewer.getServer().getInfo().getName()) : "\u2014"; + row = set(texts, base, row, c(labelServer)); + row = set(texts, base, row, c("&f" + srv)); + row = set(texts, base, row, " "); + row = set(texts, base, row, c(labelWorld)); + String world = net.viper.status.StatusAPI.playerWorlds.getOrDefault(viewer.getUniqueId(), "world"); + row = set(texts, base, row, c("&f" + world)); + row = set(texts, base, row, " "); + row = set(texts, base, row, c(labelTime)); + row = set(texts, base, row, c("&f[" + sdf.format(new Date()) + "]")); + row = set(texts, base, row, " "); + row = set(texts, base, row, c(labelTeamspeak)); + row = set(texts, base, row, c(valueTeamspeak)); + + // ── Spalten 1 bis (columns-1): Server-Spieler ───────────────────────── + List servers = getServerOrder(); + for (int col = 1; col < columns && (col - 1) < servers.size(); col++) { + base = col * rows; + row = 0; + String sName = servers.get(col - 1); + row = set(texts, base, row, c(colorSrvHeader + capitalize(sName))); + + ServerInfo info = ProxyServer.getInstance().getServerInfo(sName); + if (info != null) { + for (ProxiedPlayer p : info.getPlayers()) { + if (row >= rows) break; + String prefix = getLuckPermsPrefix(p); + String display = prefix.isEmpty() + ? c("&7" + p.getName()) + : c(prefix + "&r " + p.getName()); + set(texts, base, row, display); + skins[base + row] = getPlayerSkin(p); + pings[base + row] = Math.max(0, p.getPing()); + row++; + } + } + } + + // Items zusammenbauen + Item[] items = new Item[total]; + for (int i = 0; i < total; i++) { + Item item = new Item(); + item.setUuid(fakeUuids[i]); + item.setUsername(fakeName(i)); + item.setProperties(skins[i]); + item.setGamemode(0); + item.setPing(pings[i]); + item.setListed(true); + String text = texts[i]; + item.setDisplayName(new TextComponent(text == null || text.isEmpty() ? " " : text)); + items[i] = item; + } + return items; + } + + private int set(String[] arr, int base, int row, String text) { + if (base + row < total) arr[base + row] = text == null ? " " : text; + return row + 1; + } + + private net.md_5.bungee.protocol.data.Property[] getPlayerSkin(ProxiedPlayer player) { + try { + Object pending = player.getPendingConnection(); + net.md_5.bungee.connection.LoginResult profile = + (net.md_5.bungee.connection.LoginResult) + pending.getClass().getMethod("getLoginProfile").invoke(pending); + if (profile != null && profile.getProperties() != null) { + return profile.getProperties(); + } + } catch (Exception ignored) {} + return new net.md_5.bungee.protocol.data.Property[0]; + } + + // ══════════════════════════════════════════════════════════════════════════ + // Pakete senden + // ══════════════════════════════════════════════════════════════════════════ + + @SuppressWarnings("unchecked") + private void sendSlots(ProxiedPlayer viewer, Item[] items) { + if (sendPacketQueuedMethod == null) return; + PlayerListItemUpdate pkt = new PlayerListItemUpdate(); + EnumSet actions = EnumSet.of( + PlayerListItemUpdate.Action.ADD_PLAYER, + PlayerListItemUpdate.Action.UPDATE_DISPLAY_NAME, + PlayerListItemUpdate.Action.UPDATE_LISTED, + PlayerListItemUpdate.Action.UPDATE_LATENCY + ); + pkt.setActions(actions); + pkt.setItems(items); + try { sendPacketQueuedMethod.invoke(viewer, pkt); } + catch (Exception e) { plugin.getLogger().warning("[TablistModule] sendPacketQueued: " + e.getMessage()); } + } + + private void removeFakeSlots(ProxiedPlayer viewer) { + if (sendPacketQueuedMethod == null || fakeUuids == null) return; + try { + Class cls = Class.forName("net.md_5.bungee.protocol.packet.PlayerListItemRemove"); + Object pkt = cls.getDeclaredConstructor().newInstance(); + cls.getMethod("setUuids", UUID[].class).invoke(pkt, (Object) fakeUuids.clone()); + sendPacketQueuedMethod.invoke(viewer, pkt); + } catch (Exception e) { + try { + PlayerListItem rem = new PlayerListItem(); + rem.setAction(PlayerListItem.Action.REMOVE_PLAYER); + Item[] items = new Item[total]; + for (int i = 0; i < total; i++) { Item it = new Item(); it.setUuid(fakeUuids[i]); items[i] = it; } + rem.setItems(items); + sendPacketQueuedMethod.invoke(viewer, rem); + } catch (Exception ignored) {} + } + } + + // ══════════════════════════════════════════════════════════════════════════ + // Helpers + // ══════════════════════════════════════════════════════════════════════════ + + private List getServerOrder() { + if (!serverOrder.isEmpty()) return new ArrayList<>(serverOrder); + List list = new ArrayList<>(); + final String[] lobbyKey = { null }; + for (String key : ProxyServer.getInstance().getServers().keySet()) { + if (key.equalsIgnoreCase("lobby")) { lobbyKey[0] = key; break; } + } + if (lobbyKey[0] != null) list.add(lobbyKey[0]); + ProxyServer.getInstance().getServers().keySet().stream() + .filter(s -> lobbyKey[0] == null || !s.equalsIgnoreCase(lobbyKey[0])) + .filter(s -> !hiddenServers.contains(s.toLowerCase())) + .sorted(String.CASE_INSENSITIVE_ORDER) + .forEach(list::add); + return list; + } + + private String getRank(ProxiedPlayer player) { + try { + Class prov = Class.forName("net.luckperms.api.LuckPermsProvider"); + Object api = prov.getMethod("get").invoke(null); + Object um = api.getClass().getMethod("getUserManager").invoke(api); + Object usr = um.getClass().getMethod("getUser", UUID.class).invoke(um, player.getUniqueId()); + if (usr != null) { + Class qo = Class.forName("net.luckperms.api.query.QueryOptions"); + Object opts = qo.getMethod("defaultContextualOptions").invoke(null); + Object cache = usr.getClass().getMethod("getCachedData").invoke(usr); + Object meta = cache.getClass().getMethod("getMetaData", qo).invoke(cache, opts); + Object pfx = meta.getClass().getMethod("getPrefix").invoke(meta); + if (pfx != null && !pfx.toString().isEmpty()) return pfx.toString(); + Object pg = usr.getClass().getMethod("getPrimaryGroup").invoke(usr); + if (pg != null && !pg.toString().isEmpty()) return "[" + pg.toString().toUpperCase() + "]"; + } + } catch (Exception ignored) {} + return "NONE"; + } + + private String getLuckPermsPrefix(ProxiedPlayer player) { + try { + Class prov = Class.forName("net.luckperms.api.LuckPermsProvider"); + Object api = prov.getMethod("get").invoke(null); + Object um = api.getClass().getMethod("getUserManager").invoke(api); + Object usr = um.getClass().getMethod("getUser", UUID.class).invoke(um, player.getUniqueId()); + if (usr != null) { + Class qo = Class.forName("net.luckperms.api.query.QueryOptions"); + Object opts = qo.getMethod("defaultContextualOptions").invoke(null); + Object cache = usr.getClass().getMethod("getCachedData").invoke(usr); + Object meta = cache.getClass().getMethod("getMetaData", qo).invoke(cache, opts); + Object pfx = meta.getClass().getMethod("getPrefix").invoke(meta); + if (pfx != null) return ChatColor.translateAlternateColorCodes('&', pfx.toString()); + } + } catch (Exception ignored) {} + return ""; + } + + private static String fakeName(int i) { return String.format("~vt%03d", i); } + private static String c(String s) { return ChatColor.translateAlternateColorCodes('&', s == null ? "" : s); } + private static String capitalize(String s) { return s == null || s.isEmpty() ? s : Character.toUpperCase(s.charAt(0)) + s.substring(1); } + private static String rep(char ch, int n) { StringBuilder sb = new StringBuilder(n); for (int i=0;i