From 25389f223893d01590a2018e17a7e9a221a483d7 Mon Sep 17 00:00:00 2001 From: M_Viper Date: Sun, 24 May 2026 19:43:53 +0000 Subject: [PATCH] Delete src/main/java/net/viper/status/modules/tablist/TablistModule.java via Git Manager GUI --- .../status/modules/tablist/TablistModule.java | 1269 ----------------- 1 file changed, 1269 deletions(-) delete mode 100644 src/main/java/net/viper/status/modules/tablist/TablistModule.java diff --git a/src/main/java/net/viper/status/modules/tablist/TablistModule.java b/src/main/java/net/viper/status/modules/tablist/TablistModule.java deleted file mode 100644 index eb53c2b..0000000 --- a/src/main/java/net/viper/status/modules/tablist/TablistModule.java +++ /dev/null @@ -1,1269 +0,0 @@ -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.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.*; -import java.lang.reflect.Method; -import java.nio.charset.StandardCharsets; -import java.text.SimpleDateFormat; -import java.util.*; -import java.awt.Color; -import java.util.concurrent.ConcurrentHashMap; -import java.util.concurrent.TimeUnit; - -public class TablistModule implements Module, Listener { - - private static final String CONFIG_FILE = "tablist.properties"; - - // Leerer Skin (grauer Kopf) für Platzhalter-Slots - private static final net.md_5.bungee.protocol.data.Property[] EMPTY_SKIN = { - new net.md_5.bungee.protocol.data.Property( - "textures", - "ewogICJ0aW1lc3RhbXAiIDogMTY0NDcwNTExNjQ2OCwKICAicHJvZmlsZUlkIiA6ICJmZDQ3Y2I4YjgzNjQ0YmY3YWIyYmUxODZkYjI1ZmMwZCIsCiAgInByb2ZpbGVOYW1lIiA6ICJDVUNGTDEyIiwKICAic2lnbmF0dXJlUmVxdWlyZWQiIDogdHJ1ZSwKICAidGV4dHVyZXMiIDogewogICAgIlNLSU4iIDogewogICAgICAidXJsIiA6ICJodHRwOi8vdGV4dHVyZXMubWluZWNyYWZ0Lm5ldC90ZXh0dXJlL2ZmOWJiOWU1NjEyNWM4MjI3Yjk0YmJkYTlmNmUwZjg2MjkzMWMyMjkyNTViYThmMTIwNWQxM2M0NGMxYmI1NjEiCiAgICB9CiAgfQp9", - "D24yzbg+aBETxe5e+acQR8xJwBkhf8+CdkNYi1ufu3NgXk6YK67dIij8o3QtMx/y3rR6xupRq7bKHUGGgkw+joCC/mtG6yDdLbD32s//VAhA+VVDbIQq/CJrJ8oYarerElTjOF08zxQCw8n97cfI10gkoZvdTDouRfTfQYIIo6vvG9kTGyAJv7mIriTvxE/nwP3m6WlwRmtKWOqDhiMRNoWwo9btCp5JTZR9HVFaZdsNQvh6gUmjBqHoKtr/xWOVveEhQ5mc8WZh0dAiiC3Astfr0VIx7HW1+xNu+Z7xvRMgbZ+SbKuRwotW2KHCN+BDymTbiQ3GBljjXDjwFao0sBHQ24DjafWQcuEEWNsDnhDHtmG3tKdvGQbZ1bYhh97EjRYKXG+eZKMrFGG4jr9oCg0JD3JMBc88Z0mJWyKzPF9B+klFocmrFBF/UgkQnzkNShfkpC6RjUfCymrnAFAoV6XBcznbKQzyKKAMeNE3LPFZ3iS2Tygbrqo2Sjmq9zGpjva04RxWHJ1oeKzROQkge0z96AOO7ChTFTXqnNnAjdkfW2TjK7pSIwS0vMGsUgm1C/amzMpZdJuI0FXFEzz1jhFi5cdwHXSQY1gVpa4VTLNQvu1xgcnbOVJaV0Ty+AebI2s6CLt6OcpI3QKY+KPlITuwj5HydMiQvfYldhiHPjc=" - ) - }; - - private static final int ROWS = 20; - private int rows = ROWS, columns = 6, total = 120, tabSizeMax = 0; - private int configuredTabSize = 0; - private int configuredColumns = 0; - private UUID[] fakeUuids; - - private final ConcurrentHashMap skinCache = new ConcurrentHashMap<>(); - private final Map serverSymbols = new LinkedHashMap<>(); - - private String columnHeaderMode = "none"; - private String playerDisplayMode = "server"; - - private boolean enabled = true; - private int updateInterval = 5; - private String layoutMode = "compact"; - - 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 compactHeader1 = "&6&lViper Network &8• &2Hallo, &a%player%&7! &6Schön dass du da bist!"; - private String compactHeader2 = ""; - private String compactHeader3 = ""; - private boolean compactHeader1Spacer = false; - private boolean compactHeader2Spacer = false; - private boolean compactHeader3Spacer = false; - private String compactHeader4 = ""; - private boolean compactHeader4Spacer = false; - - private String compactFooter1 = ""; - private String compactFooter2 = "&7Zeit: &f%time% &8| &7Spieler: &f%online% &8| &7Ping: &f%ping%ms"; - private String compactFooter3 = "&7Kontostand: &a$%balance% &8| &7Server: &f%server% &8| &7Welt: &f%world%"; - private String compactFooter4 = ""; - private String compactFooter5 = ""; - private String compactFooter6 = ""; - private boolean compactFooter1Spacer = false; - private boolean compactFooter2Spacer = false; - private boolean compactFooter3Spacer = false; - private boolean compactFooter4Spacer = false; - private boolean compactFooter5Spacer = false; - private boolean compactFooter6Spacer = false; - - private String colorSrvHeader = "&6&l"; - private boolean showFooterServerList = true; - private String timeFormat = "HH:mm:ss / h:mm a"; - private String timeZone = "Europe/Berlin"; - private SimpleDateFormat sdf; - private List serverOrder = new ArrayList<>(); - private Set hiddenServers = new HashSet<>(); - private List rankOrder = new ArrayList<>(); - - private static class InfoEntry { - String label, type, value; boolean enabled; - InfoEntry(String l, String t, String v, boolean e) { label=l; type=t; value=v; enabled=e; } - } - private List infoEntries = new ArrayList<>(); - - private Plugin plugin; - private ScheduledTask updateTask; - private Method sendPacketQueuedMethod; - private java.lang.reflect.Field tabListHandlerField; - - @Override public String getName() { return "TablistModule"; } - - @Override - public void onEnable(Plugin plugin) { - this.plugin = plugin; - plugin.getLogger().info("[TablistModule] Starte..."); - ensureConfigExists(); - loadConfig(); - plugin.getLogger().info("[TablistModule] Config geladen. Layout=" + layoutMode + " enabled=" + enabled); - if (!enabled) { plugin.getLogger().info("[TablistModule] Deaktiviert."); return; } - try { - initGridSize(); - } catch (Exception e) { - plugin.getLogger().warning("[TablistModule] initGridSize Fehler: " + e.getMessage()); - int fbSize = configuredTabSize > 0 ? configuredTabSize : 120; - int maxCols = fbSize / ROWS; - tabSizeMax = fbSize; rows = ROWS; - columns = configuredColumns > 0 ? configuredColumns : Math.max(6, maxCols); - total = ROWS * columns; - } - initUuids(); - try { - Class uc = Class.forName("net.md_5.bungee.UserConnection"); - sendPacketQueuedMethod = uc.getMethod("sendPacketQueued", net.md_5.bungee.protocol.DefinedPacket.class); - sendPacketQueuedMethod.setAccessible(true); - // BungeeCords internen tabListHandler-Field finden und nullbar machen - try { - tabListHandlerField = uc.getDeclaredField("tabListHandler"); - tabListHandlerField.setAccessible(true); - } catch (Exception ignored) {} - plugin.getLogger().info("[TablistModule] sendPacketQueued gefunden."); - } 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); - ProxyServer.getInstance().getScheduler().schedule(plugin, () -> { - plugin.getLogger().info("[TablistModule] Alle BungeeCord-Server: " + new ArrayList<>(ProxyServer.getInstance().getServers().keySet())); - plugin.getLogger().info("[TablistModule] Tablist-Spalten: " + getServerOrder()); - recalculateGrid(); - }, 3L, TimeUnit.SECONDS); - plugin.getLogger().info("[TablistModule] Aktiviert. Grid=" + columns + "x" + rows + " layout=" + layoutMode - + " column_header=" + columnHeaderMode + " symbols=" + serverSymbols.size()); - } - - @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) {} - } - } - - private void initGridSize() { - // configuredTabSize aus loadConfig() hat immer Vorrang. - if (configuredTabSize > 0) { - tabSizeMax = configuredTabSize; - } else { - tabSizeMax = 400; // Sicherer Fallback - } - rows = ROWS; - - int serverCount = getServerOrder().size(); - - // SIMPEL & SICHER: configuredColumns direkt nehmen, kein komplexes Berechnen - if (configuredColumns > 0) { - columns = configuredColumns; - } else { - // Kein Wert in config → automatisch: 1 Spalte pro Server, mindestens 6 - columns = Math.max(6, serverCount); - } - - total = ROWS * columns; - plugin.getLogger().info("[TablistModule] Grid: " + columns + " Spalten × " + ROWS + " = " + total + " Slots | " + serverCount + " Server | tabSizeMax=" + tabSizeMax); - } - - private void initUuids() { - fakeUuids = new UUID[total]; - for (int i = 0; i < total; i++) fakeUuids[i] = new UUID(0xFFFEDEAD00000000L, (long) i); - } - - // ── Events ───────────────────────────────────────────────────────────────── - - @EventHandler - public void onLogin(PostLoginEvent e) { - if (!enabled) return; - ProxiedPlayer p = e.getPlayer(); - net.md_5.bungee.protocol.data.Property[] skin = fetchSkin(p); - if (skin != null && skin.length > 0) skinCache.put(p.getUniqueId(), skin); - disableBungeeTabHandler(p); - // BungeeCord resettet tabListHandler nach PostLoginEvent intern nochmals. - // Deshalb: 0.5s, 1s, 2s nacheinander überschreiben. - long[] delays = {500L, 1000L, 2000L}; - for (long delayMs : delays) { - ProxyServer.getInstance().getScheduler().schedule(plugin, () -> { - disableBungeeTabHandler(p); - updateTablist(p); - }, delayMs, TimeUnit.MILLISECONDS); - } - ProxyServer.getInstance().getScheduler().schedule(plugin, this::updateAll, 3L, TimeUnit.SECONDS); - } - - @EventHandler - public void onSwitch(ServerSwitchEvent e) { - if (!enabled) return; - ProxiedPlayer switched = e.getPlayer(); - - net.md_5.bungee.protocol.data.Property[] skin = fetchSkin(switched); - if (skin != null && skin.length > 0) skinCache.put(switched.getUniqueId(), skin); - // Sofort deaktivieren - disableBungeeTabHandler(switched); - - // BungeeCord setzt den tabListHandler nach ServerSwitch intern mehrfach zurück. - // Deshalb: 0.5s, 1s, 2s, 3s nacheinander überschreiben + tablist neu senden. - long[] delays = {500L, 1000L, 2000L, 3000L}; - for (long delayMs : delays) { - ProxyServer.getInstance().getScheduler().schedule(plugin, () -> { - disableBungeeTabHandler(switched); - net.md_5.bungee.protocol.data.Property[] freshSkin = fetchSkin(switched); - if (freshSkin != null && freshSkin.length > 0) skinCache.put(switched.getUniqueId(), freshSkin); - for (ProxiedPlayer viewer : ProxyServer.getInstance().getPlayers()) { - try { removeFakeSlots(viewer); } catch (Exception ignored) {} - } - updateAll(); - }, delayMs, TimeUnit.MILLISECONDS); - } - } - - @EventHandler - public void onDisconnect(PlayerDisconnectEvent e) { - if (!enabled) return; - skinCache.remove(e.getPlayer().getUniqueId()); - ProxyServer.getInstance().getScheduler().schedule(plugin, () -> { - for (ProxiedPlayer viewer : ProxyServer.getInstance().getPlayers()) { - try { removeFakeSlots(viewer); } catch (Exception ignored) {} - } - ProxyServer.getInstance().getScheduler().schedule(plugin, - this::updateAll, 1L, TimeUnit.SECONDS); - }, 1L, TimeUnit.SECONDS); - } - - // ── Core ─────────────────────────────────────────────────────────────────── - - private void updateAll() { - recalculateGrid(); - for (ProxiedPlayer p : ProxyServer.getInstance().getPlayers()) { - if (!skinCache.containsKey(p.getUniqueId())) { - net.md_5.bungee.protocol.data.Property[] skin = fetchSkin(p); - if (skin != null && skin.length > 0) skinCache.put(p.getUniqueId(), skin); - } - } - for (ProxiedPlayer p : ProxyServer.getInstance().getPlayers()) updateTablist(p); - } - - private void recalculateGrid() { - // tabSizeMax synchronisieren - if (configuredTabSize > 0) tabSizeMax = configuredTabSize; - - int serverCount = getServerOrder().size(); - int newColumns = configuredColumns > 0 ? configuredColumns : Math.max(6, serverCount); - int newTotal = ROWS * newColumns; - - if (newColumns == columns && newTotal == total && fakeUuids != null && fakeUuids.length == newTotal) return; - for (ProxiedPlayer p : ProxyServer.getInstance().getPlayers()) { - try { removeFakeSlots(p); } catch (Exception ignored) {} - } - rows = ROWS; - columns = newColumns; - total = newTotal; - initUuids(); - plugin.getLogger().info("[TablistModule] Grid: " + columns + "x" + rows + "=" + total + " (" + serverCount + " Server)"); - } - - private void updateTablist(ProxiedPlayer viewer) { - if (viewer == null || !viewer.isConnected()) return; - try { - // DEBUG: zeige aktuelle Grid-Werte im Chat (wird nach erstem Fix entfernt) - String srv = viewer.getServer() != null ? capitalize(viewer.getServer().getInfo().getName()) : "\u2014"; - String world = net.viper.status.StatusAPI.playerWorlds.getOrDefault(viewer.getUniqueId(), "world"); - String rank = getRank(viewer); - String time = sdf.format(new Date()); - String balance = getBalance(viewer); - int online = ProxyServer.getInstance().getOnlineCount(); - - String header, footer; - if ("compact".equalsIgnoreCase(layoutMode)) { - header = buildCompactHeader(viewer, srv, world, rank, time, balance, online); - footer = buildCompactFooter(viewer, srv, world, rank, time, balance, online); - } else { - header = c(headerLine1) + "\n" + c(headerLine2) + "\n" + c(headerLine3); - footer = c(footerLine1) + "\n" + c(footerLine2) + "\n" + c(footerLine3); - } - // fromLegacyText parst §x§R§R§G§G§B§B Hex-Sequenzen korrekt - net.md_5.bungee.api.chat.BaseComponent[] hComps = net.md_5.bungee.api.chat.TextComponent.fromLegacyText(header); - net.md_5.bungee.api.chat.BaseComponent[] fComps = net.md_5.bungee.api.chat.TextComponent.fromLegacyText(footer); - viewer.setTabHeader( - new net.md_5.bungee.api.chat.TextComponent(hComps), - new net.md_5.bungee.api.chat.TextComponent(fComps)); - // Erst Slots senden (ADD_PLAYER), DANN echte Spieler verstecken (UPDATE_LISTED=false). - // Umgekehrte Reihenfolge führt zu "Ignoring player info update for unknown player" - // weil der Client UPDATE_LISTED für unbekannte UUIDs ignoriert. - sendSlots(viewer, buildItems(viewer)); - hideRealPlayers(viewer); - } catch (Exception ex) { - plugin.getLogger().warning("[TablistModule] " + viewer.getName() + ": " + ex.getMessage()); - } - } - - // ── Header / Footer ──────────────────────────────────────────────────────── - - private String buildCompactHeader(ProxiedPlayer viewer, String srv, String world, String rank, String time, String balance, int online) { - StringBuilder sb = new StringBuilder(); - appendLine(sb, compactHeader1, compactHeader1Spacer, viewer, srv, world, rank, time, balance, online); - appendLine(sb, compactHeader2, compactHeader2Spacer, viewer, srv, world, rank, time, balance, online); - appendLine(sb, compactHeader3, compactHeader3Spacer, viewer, srv, world, rank, time, balance, online); - appendLine(sb, compactHeader4, compactHeader4Spacer, viewer, srv, world, rank, time, balance, online); - return sb.toString(); - } - - private String buildCompactFooter(ProxiedPlayer viewer, String srv, String world, String rank, String time, String balance, int online) { - StringBuilder sb = new StringBuilder(); - appendLine(sb, compactFooter1, compactFooter1Spacer, viewer, srv, world, rank, time, balance, online); - List servers = getServerOrder(); - if (showFooterServerList && !servers.isEmpty()) { - StringBuilder sLine = new StringBuilder(); - for (String sName : servers) { - ServerInfo si = ProxyServer.getInstance().getServerInfo(sName); - int cnt = si != null ? si.getPlayers().size() : 0; - if (sLine.length() > 0) sLine.append(" &8| "); - sLine.append(c(colorSrvHeader)).append(capitalize(sName)).append(" &8\u25cf &7").append(cnt); - } - if (sb.length() > 0) sb.append("\n"); - sb.append(c(sLine.toString())); - } - appendLine(sb, compactFooter2, compactFooter2Spacer, viewer, srv, world, rank, time, balance, online); - appendLine(sb, compactFooter3, compactFooter3Spacer, viewer, srv, world, rank, time, balance, online); - appendLine(sb, compactFooter4, compactFooter4Spacer, viewer, srv, world, rank, time, balance, online); - appendLine(sb, compactFooter5, compactFooter5Spacer, viewer, srv, world, rank, time, balance, online); - appendLine(sb, compactFooter6, compactFooter6Spacer, viewer, srv, world, rank, time, balance, online); - return sb.toString(); - } - - private void appendLine(StringBuilder sb, String line, boolean spacer, ProxiedPlayer viewer, String srv, String world, String rank, String time, String balance, int online) { - boolean empty = line == null || line.trim().isEmpty(); - if (empty && !spacer) return; - if (sb.length() > 0) sb.append("\n"); - sb.append(empty ? "\u00A0" : c(replacePlaceholders(line, viewer, srv, world, rank, time, balance, online))); - } - - // ── Items ────────────────────────────────────────────────────────────────── - - 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] = EMPTY_SKIN; pings[i] = 0; } - - boolean compact = "compact".equalsIgnoreCase(layoutMode); - // Ob der Spalten-Header einen Slot belegt: - // "full" = explizit aktiviert, "none"/"small" = früher deaktiviert. - // FIX: Im Server-Modus immer den Servernamen in Zeile 0 schreiben, - // sonst weiß der Spieler nicht welche Spalte welcher Server ist. - boolean useSlotHeader = !"none".equalsIgnoreCase(columnHeaderMode); - - // Info-Spalte (nur classic) - if (!compact) { - int base = 0, row = 0; - String srv = viewer.getServer() != null ? capitalize(viewer.getServer().getInfo().getName()) : "\u2014"; - String world = net.viper.status.StatusAPI.playerWorlds.getOrDefault(viewer.getUniqueId(), "world"); - String rank = getRank(viewer); String time = sdf.format(new Date()); - String balance = getBalance(viewer); int online = ProxyServer.getInstance().getOnlineCount(); - for (InfoEntry entry : infoEntries) { - if (!entry.enabled || row + 1 >= rows) continue; - if (entry.label != null && !entry.label.isEmpty()) - row = set(texts, base, row, c(replacePlaceholders(entry.label, viewer, srv, world, rank, time, balance, online))); - String val; - switch (entry.type) { - case "name": val = "&f" + viewer.getName(); break; - case "rank": val = "&f" + rank; break; - case "server": val = "&f" + srv; break; - case "world": val = "&f" + world; break; - case "time": val = "&f[" + time + "]"; break; - case "balance": val = "&f" + balance; break; - case "online": val = "&f" + online; break; - default: val = replacePlaceholders(entry.value, viewer, srv, world, rank, time, balance, online); break; - } - row = set(texts, base, row, c(val)); - row = set(texts, base, row, " "); - } - } - - if ("custom".equalsIgnoreCase(playerDisplayMode)) { - // ── Custom-Modus: alle Spieler zusammen, nach Rang sortiert ────────── - // Minecraft Tab-Grid ist spaltenweise aufgebaut (Spalte 1 = Slots 0-19, Spalte 2 = Slots 20-39) - // "Links nach rechts" = Zeile 0 über alle Spalten, dann Zeile 1 usw. - // Spieler 0 → Spalte 0 Zeile 0, Spieler 1 → Spalte 1 Zeile 0, Spieler 2 → Spalte 2 Zeile 0 - // Spieler 3 → Spalte 0 Zeile 1, Spieler 4 → Spalte 1 Zeile 1 usw. - List allPlayers = new ArrayList<>(ProxyServer.getInstance().getPlayers()); - allPlayers = sortPlayersByRank(allPlayers); - int startCol = compact ? 0 : 1; - int usedCols = columns - startCol; - int maxSlots = usedCols * rows; - int playerIdx = 0; - // Zeile für Zeile iterieren, innerhalb jeder Zeile alle Spalten - outer: - for (int row = 0; row < rows; row++) { - for (int col = startCol; col < columns; col++) { - if (playerIdx >= allPlayers.size()) break outer; - ProxiedPlayer p = allPlayers.get(playerIdx++); - int base = col * rows; - String prefix = getLuckPermsPrefix(p); - String symbol = getServerSymbol(p); - String nameStr = p.getName() + (symbol.isEmpty() ? "" : " " + symbol); - set(texts, base, row, prefix.isEmpty() - ? c("&7" + nameStr) - : c(prefix + "&r " + nameStr)); - net.md_5.bungee.protocol.data.Property[] skin = skinCache.get(p.getUniqueId()); - skins[base + row] = (skin != null && skin.length > 0) ? skin : EMPTY_SKIN; - pings[base + row] = p.getPing() < 0 ? 1 : p.getPing(); - } - } - } else { - // ── Server-Modus: pro Spalte ein Server (default) ────────────────── - List servers = getServerOrder(); - int startCol = compact ? 0 : 1; - for (int col = startCol; col < columns && (col - startCol) < servers.size(); col++) { - int base = col * rows; - int row = 0; - String sName = servers.get(col - startCol); - if (useSlotHeader) { - row = set(texts, base, row, c(colorSrvHeader + capitalize(sName))); - } - ServerInfo si = ProxyServer.getInstance().getServerInfo(sName); - if (si != null) { - for (ProxiedPlayer p : sortPlayersByRank(new ArrayList<>(si.getPlayers()))) { - if (row >= rows) break; - String prefix = getLuckPermsPrefix(p); - String symbol = getServerSymbol(p); - String nameStr = p.getName() + (symbol.isEmpty() ? "" : " " + symbol); - set(texts, base, row, prefix.isEmpty() - ? c("&7" + nameStr) - : c(prefix + "&r " + nameStr)); - net.md_5.bungee.protocol.data.Property[] skin = skinCache.get(p.getUniqueId()); - skins[base + row] = (skin != null && skin.length > 0) ? skin : EMPTY_SKIN; - pings[base + row] = p.getPing() < 0 ? 1 : p.getPing(); - row++; - } - } - } - } - - 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 dn = texts[i] == null || texts[i].isEmpty() ? " " : texts[i]; - item.setDisplayName(new net.md_5.bungee.api.chat.TextComponent( - net.md_5.bungee.api.chat.TextComponent.fromLegacyText(dn))); - items[i] = item; - } - return items; - } - - // ── Pakete ───────────────────────────────────────────────────────────────── - - @SuppressWarnings("unchecked") - private void hideRealPlayers(ProxiedPlayer viewer) { - if (sendPacketQueuedMethod == null) return; - try { - // Echte Spieler: nur listed=false setzen (KEIN Remove, sonst geht der Skin verloren!) - // Der Client kennt sie bereits per ADD_PLAYER von BungeeCord → UPDATE_LISTED reicht. - Collection online = ProxyServer.getInstance().getPlayers(); - if (!online.isEmpty()) { - PlayerListItemUpdate playerPkt = new PlayerListItemUpdate(); - playerPkt.setActions(EnumSet.of(PlayerListItemUpdate.Action.UPDATE_LISTED)); - Item[] playerItems = new Item[online.size()]; - int i = 0; - for (ProxiedPlayer p : online) { - Item it = new Item(); it.setUuid(p.getUniqueId()); it.setListed(false); - playerItems[i++] = it; - } - playerPkt.setItems(playerItems); - sendPacketQueuedMethod.invoke(viewer, playerPkt); - } - - // Server-UUIDs (BungeeCord schreibt pro Server 2 Einträge in die Tablist): - // Diese per PlayerListItemRemove entfernen – das funktioniert auch für - // UUIDs die der Client noch nicht kennt, ohne Skin-Schaden. - List serverUuids = new ArrayList<>(); - for (String srvName : ProxyServer.getInstance().getServers().keySet()) { - try { - serverUuids.add(UUID.nameUUIDFromBytes(("OfflinePlayer:" + srvName).getBytes(StandardCharsets.UTF_8))); - serverUuids.add(UUID.nameUUIDFromBytes(srvName.getBytes(StandardCharsets.UTF_8))); - } catch (Exception ignored) {} - } - if (!serverUuids.isEmpty()) { - try { - Class removeClass = Class.forName("net.md_5.bungee.protocol.packet.PlayerListItemRemove"); - Object removePkt = removeClass.getDeclaredConstructor().newInstance(); - removeClass.getMethod("setUuids", UUID[].class).invoke(removePkt, - (Object) serverUuids.toArray(new UUID[0])); - sendPacketQueuedMethod.invoke(viewer, removePkt); - } catch (Exception ignored) { - // Fallback: UPDATE_LISTED=false (Warnungen im Client-Log sind harmlos) - PlayerListItemUpdate srvPkt = new PlayerListItemUpdate(); - srvPkt.setActions(EnumSet.of(PlayerListItemUpdate.Action.UPDATE_LISTED)); - Item[] srvItems = new Item[serverUuids.size()]; - for (int j = 0; j < serverUuids.size(); j++) { - Item it = new Item(); it.setUuid(serverUuids.get(j)); it.setListed(false); - srvItems[j] = it; - } - srvPkt.setItems(srvItems); - sendPacketQueuedMethod.invoke(viewer, srvPkt); - } - } - } catch (Exception e) { plugin.getLogger().warning("[TablistModule] hideRealPlayers: " + e.getMessage()); } - } - - @SuppressWarnings("unchecked") - private void sendSlots(ProxiedPlayer viewer, Item[] items) { - if (sendPacketQueuedMethod == null) return; - try { - // Paket 1: ADD_PLAYER + UPDATE_DISPLAY_NAME + UPDATE_LISTED + UPDATE_LATENCY - PlayerListItemUpdate pkt = new PlayerListItemUpdate(); - pkt.setActions(EnumSet.of( - PlayerListItemUpdate.Action.ADD_PLAYER, - PlayerListItemUpdate.Action.UPDATE_DISPLAY_NAME, - PlayerListItemUpdate.Action.UPDATE_LISTED, - PlayerListItemUpdate.Action.UPDATE_LATENCY)); - pkt.setItems(items); - sendPacketQueuedMethod.invoke(viewer, pkt); - - // Paket 2: Explizit UPDATE_LISTED=true für alle Fake-Slots nochmal senden. - // BungeeCord sendet nach ServerSwitch ein eigenes Tab-Paket das einige Slots - // auf listed=false setzt – dieses zweite Paket überschreibt das wieder. - PlayerListItemUpdate listedPkt = new PlayerListItemUpdate(); - listedPkt.setActions(EnumSet.of(PlayerListItemUpdate.Action.UPDATE_LISTED)); - listedPkt.setItems(items); // items haben alle listed=true gesetzt - sendPacketQueuedMethod.invoke(viewer, listedPkt); - } catch (Exception e) { plugin.getLogger().warning("[TablistModule] sendSlots: " + 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 ──────────────────────────────────────────────────────────────── - - /** - * Ersetzt BungeeCords internen tabListHandler durch einen No-Op Proxy. - * null setzen crasht BungeeCord (NPE in DownstreamBridge). - * Ein leerer Proxy ignoriert alle Aufrufe lautlos. - */ - private void disableBungeeTabHandler(ProxiedPlayer player) { - if (tabListHandlerField == null) return; - try { - Class tabListClass = Class.forName("net.md_5.bungee.tab.TabList"); - // IMMER neu setzen – BungeeCord resettet den tabListHandler nach jedem - // ServerSwitch intern, daher reicht ein einmaliges Setzen nicht aus. - Object noopProxy = java.lang.reflect.Proxy.newProxyInstance( - tabListClass.getClassLoader(), - new Class[]{ tabListClass }, - (proxy, method, args) -> { - Class ret = method.getReturnType(); - if (ret == boolean.class || ret == Boolean.class) return false; - if (ret == int.class || ret == Integer.class) return 0; - if (ret == long.class || ret == Long.class) return 0L; - return null; - } - ); - tabListHandlerField.set(player, noopProxy); - } catch (Exception ignored) {} - } - - /** - * ── ULTIMATE: Gibt das konfigurierte Server-Symbol für den Spieler zurück. - * Leer wenn kein Symbol für den aktuellen Server definiert ist. - */ - private String getServerSymbol(ProxiedPlayer player) { - if (serverSymbols.isEmpty() || player.getServer() == null) return ""; - String srvKey = player.getServer().getInfo().getName().toLowerCase(); - String raw = serverSymbols.get(srvKey); - if (raw == null || raw.isEmpty()) return ""; - return c(raw); // Farb-Codes und Hex-Farben auflösen - } - - private net.md_5.bungee.protocol.data.Property[] fetchSkin(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 && profile.getProperties().length > 0) - return profile.getProperties(); - } catch (Exception ignored) {} - return new net.md_5.bungee.protocol.data.Property[0]; - } - - private List getServerOrder() { - List list; - if (!serverOrder.isEmpty()) { - list = new ArrayList<>(serverOrder); - list.removeIf(s -> hiddenServers.contains(s.toLowerCase())); - } else { - 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 List sortPlayersByRank(List players) { - if (rankOrder.isEmpty()) return players; - players.sort((a, b) -> { int ia = getRankIndex(a), ib = getRankIndex(b); - return ia != ib ? Integer.compare(ia, ib) : a.getName().compareToIgnoreCase(b.getName()); }); - return players; - } - - private int getRankIndex(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) { - Object pg = usr.getClass().getMethod("getPrimaryGroup").invoke(usr); - if (pg != null) { String g = pg.toString().toLowerCase(); - for (int i = 0; i < rankOrder.size(); i++) if (rankOrder.get(i).equalsIgnoreCase(g)) return i; } - } - } catch (Exception ignored) {} - return rankOrder.size(); - } - - 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); - // ── HEX-Farben auch im Prefix auflösen ─────────────────────── - if (pfx != null) return c(pfx.toString()); - } - } catch (Exception ignored) {} - return ""; - } - - private String getBalance(ProxiedPlayer player) { - try { - Map balances = (Map) net.viper.status.StatusAPI.class.getField("playerBalances").get(null); - Object val = balances.get(player.getUniqueId()); - if (val != null) return String.format("%,.2f", ((Number) val).doubleValue()); - } catch (Exception ignored) {} - return "0.00"; - } - - private String replacePlaceholders(String text, ProxiedPlayer viewer, String srv, String world, String rank, String time, String balance, int online) { - if (text == null) return ""; - String result = resolvePapiPlaceholders(text, viewer.getUniqueId()); - result = result.replace("%player%", viewer.getName()).replace("%rank%", rank) - .replace("%server%", srv).replace("%world%", world).replace("%time%", time) - .replace("%balance%", balance).replace("%ping%", String.valueOf(viewer.getPing())) - .replace("%online%", String.valueOf(online)); - return result; - } - - private static String resolvePapiPlaceholders(String text, UUID uuid) { - if (text == null || !text.contains("%")) return text; - Map papiMap = net.viper.status.StatusAPI.playerPapi.get(uuid); - if (papiMap == null || papiMap.isEmpty()) return text; - StringBuilder sb = new StringBuilder(); - int i = 0; - while (i < text.length()) { - int start = text.indexOf('%', i); - if (start < 0) { sb.append(text.substring(i)); break; } - int end = text.indexOf('%', start + 1); - if (end < 0) { sb.append(text.substring(i)); break; } - String token = text.substring(start + 1, end); - if (papiMap.containsKey(token)) { - sb.append(text, i, start); - sb.append(papiMap.get(token)); - i = end + 1; - } else { - sb.append(text, i, end + 1); - i = end + 1; - } - } - return sb.toString(); - } - - private int set(String[] arr, int base, int row, String text) { - if (base + row < total) arr[base + row] = text == null ? " " : text; return row + 1; - } - - // ── Farb-Auflösung ───────────────────────────────────────────────────────── - - // ══════════════════════════════════════════════════════════════════════════ - // Farb-Parser: Birdflop-kompatibel - // Unterstützte Formate (alle gleichzeitig nutzbar): - // - // &#RRGGBB → Pro-Zeichen Hex (Birdflop Standard-Output) - // {#RRGGBB} → Bracket-Format - // <#RRGGBB> → MiniMessage Kurzform - // → MiniMessage color-Tag - // → Farbverlauf (beliebig viele Farb-Stopps) - // → Text in Schattenfarbe - // → Formatierungen - // &l &o &n &m &k &r → Standard-Formatierungen - // ══════════════════════════════════════════════════════════════════════════ - - private static String c(String s) { - if (s == null) return " "; - s = parseMiniMessage(s); // MiniMessage-Tags (, , <#>, , usw.) - s = parseHexAmpersand(s); // &#RRGGBB und {#RRGGBB} - return net.md_5.bungee.api.ChatColor.translateAlternateColorCodes('&', s); - } - - private static String stripColors(String s) { - return s == null ? "" : net.md_5.bungee.api.ChatColor.stripColor(c(s)); - } - - // ── MiniMessage Haupt-Dispatcher ───────────────────────────────────────── - - private static String parseMiniMessage(String text) { - if (text == null || !text.contains("<")) return text == null ? "" : text; - // gradient-Tags als erstes, weil sie anderen Text enthalten können - text = parseGradientTags(text); - // shadow-Tags - text = parseShadowTags(text); - // Einfache Tags: , <#>, , , , , , - text = parseSimpleTags(text); - return text; - } - - // ── ────────────────────────────────────────── - - private static String parseGradientTags(String text) { - if (!text.contains(" suchen (mit Tiefenzähler für verschachtelte <...>) - int end = findClosingAngle(text, start + 1); - if (end < 0) { result.append(text, i, text.length()); break; } - String inner = text.substring(start + 1, end); // "gradient:#C1:#C2:TEXT" - result.append(applyGradientTag(inner)); - i = end + 1; - } - return result.toString(); - } - - /** - * Parst "gradient:#C1:#C2:#C3:TEXT" → eingefärbten Text. - * TEXT darf selbst wieder §-Codes oder &-Codes enthalten (z.B. &l für Bold). - */ - private static String applyGradientTag(String inner) { - // inner = "gradient:COLOR:COLOR:...:TEXT" - // Farben beginnen mit # oder mit & gefolgt von einem Hex-Code - java.util.List colors = new java.util.ArrayList<>(); - // Trenne am ersten Doppelpunkt nach "gradient" - int firstColon = inner.indexOf(':'); // nach "gradient" - if (firstColon < 0) return inner; - String rest = inner.substring(firstColon + 1); - - // Lese Farb-Stopps (jeder Teil beginnt mit #) - // TEXT ist alles ab dem ersten Teil der NICHT mit # beginnt - StringBuilder textSb = new StringBuilder(); - boolean inText = false; - String[] parts = rest.split(":", -1); - for (int p = 0; p < parts.length; p++) { - String part = parts[p]; - if (!inText && part.startsWith("#") && part.length() == 7) { - colors.add(part); - } else { - // Ab hier Text (inkl. Doppelpunkte wieder zusammensetzen) - inText = true; - if (textSb.length() > 0) textSb.append(":"); - textSb.append(part); - } - } - if (colors.size() < 2) return textSb.toString(); - - // Shadow-Tags im Text zuerst auflösen (können im Gradient-Text stecken) - String rawText = parseShadowTags(textSb.toString()); - return applyGradient(rawText, colors); - } - - private static String applyGradient(String text, java.util.List colorStops) { - if (text == null || text.isEmpty()) return text; - // §-Codes und &-Codes aus Text herausfiltern für Längenberechnung - String plain = text - .replaceAll("\u00A7[0-9a-fk-orx]", "") - .replaceAll("&[0-9a-fA-Fk-orK-OR]", "") - .replaceAll("\u00A7x(\u00A7[0-9a-fA-F]){6}", ""); // §x§R§R§G§G§B§B - int len = plain.length(); - if (len == 0) return text; - if (len == 1) return resolveColorToSection(colorStops.get(0)) + text; - - int[][] rgbStops = new int[colorStops.size()][3]; - for (int s = 0; s < colorStops.size(); s++) rgbStops[s] = hexToRgb(colorStops.get(s)); - - StringBuilder result = new StringBuilder(); - int charIdx = 0; - int ci = 0; - while (ci < text.length()) { - char ch = text.charAt(ci); - - // §x§R§R§G§G§B§B durchreichen (bereits aufgelöste Hex-Farbe z.B. von shadow) - if (ch == '\u00A7' && ci + 1 < text.length() && text.charAt(ci + 1) == 'x') { - // Lese die 12 folgenden Zeichen (§x + 6x §digit) - if (ci + 13 < text.length() + 1) { - result.append(text, ci, Math.min(ci + 14, text.length())); - ci = Math.min(ci + 14, text.length()); - } else { - result.append(ch); ci++; - } - continue; - } - // §-Formatcode durchreichen - if (ch == '\u00A7' && ci + 1 < text.length()) { - result.append(ch).append(text.charAt(ci + 1)); ci += 2; continue; - } - // &-Formatcode durchreichen - if (ch == '&' && ci + 1 < text.length() && "&0123456789abcdefABCDEFklmnorKLMNOR".indexOf(text.charAt(ci+1)) >= 0) { - result.append(ch).append(text.charAt(ci + 1)); ci += 2; continue; - } - - // Normales Zeichen → Farbe interpolieren - float t = len <= 1 ? 0f : (float) charIdx / (len - 1); - int segments = colorStops.size() - 1; - float scaled = t * segments; - int seg = Math.min((int) scaled, segments - 1); - float segT = scaled - seg; - int[] c1 = rgbStops[seg], c2 = rgbStops[seg + 1]; - int r = clamp((int)(c1[0] + (c2[0] - c1[0]) * segT)); - int g = clamp((int)(c1[1] + (c2[1] - c1[1]) * segT)); - int b = clamp((int)(c1[2] + (c2[2] - c1[2]) * segT)); - String hex = String.format("%02X%02X%02X", r, g, b); - appendHexSection(result, hex); - result.append(ch); - charIdx++; - ci++; - } - return result.toString(); - } - - // ── ───────────────────────────────────────────────── - - private static String parseShadowTags(String text) { - if (text == null || !text.contains("= 0 ? inner.indexOf(':', firstColon + 1) : -1; - if (firstColon < 0 || secondColon < 0) { result.append(text, i, end + 1); i = end + 1; continue; } - String colorPart = inner.substring(firstColon + 1, secondColon).trim(); - String content = inner.substring(secondColon + 1); - result.append(resolveColorToSection(colorPart)).append(content); - i = end + 1; - } - return result.toString(); - } - - // ── Einfache MiniMessage-Tags ───────────────────────────────────────────── - - private static String parseSimpleTags(String text) { - if (text == null || !text.contains("<")) return text == null ? "" : text; - // Ersetzungstabelle - text = text.replace("", "&l").replace("", "&r"); - text = text.replace("", "&o").replace("", "&r"); - text = text.replace("", "&n").replace("", "&r"); - text = text.replace("", "&m").replace("", "&r"); - text = text.replace("", "&k").replace("", "&r"); - text = text.replace("", "&r").replace("", ""); - // Closing-Tags entfernen (werden nach Verarbeitung nicht mehr benötigt) - text = text.replaceAll("", ""); - text = text.replaceAll("", ""); - text = text.replaceAll("", ""); - // und <#RRGGBB> - StringBuilder result = new StringBuilder(); - int i = 0; - while (i < text.length()) { - char ch = text.charAt(i); - if (ch != '<') { result.append(ch); i++; continue; } - // - if (text.startsWith("', i); - if (end > 0) { - String hex = text.substring(i + 7, end).trim(); - if (hex.startsWith("#") && hex.length() == 7 && hex.substring(1).matches("[0-9a-fA-F]{6}")) { - appendHexSection(result, hex.substring(1)); - i = end + 1; continue; - } - } - } - // <#RRGGBB> - if (text.startsWith("<#", i) && i + 9 <= text.length()) { - int end = text.indexOf('>', i); - if (end == i + 8) { - String hex = text.substring(i + 2, end); - if (hex.matches("[0-9a-fA-F]{6}")) { - appendHexSection(result, hex); - i = end + 1; continue; - } - } - } - result.append(ch); i++; - } - return result.toString(); - } - - // ── &#RRGGBB und {#RRGGBB} ─────────────────────────────────────────────── - - private static String parseHexAmpersand(String text) { - if (text == null) return ""; - if (!text.contains("&#") && !text.contains("{#")) return text; - StringBuilder sb = new StringBuilder(); - int i = 0; - while (i < text.length()) { - // &#RRGGBB - if (i + 7 < text.length() + 1 && i + 8 <= text.length() - && text.charAt(i) == '&' && text.charAt(i+1) == '#') { - String hex = text.substring(i+2, i+8); - if (hex.matches("[0-9a-fA-F]{6}")) { - appendHexSection(sb, hex); i += 8; continue; - } - } - // {#RRGGBB} - if (i + 8 < text.length() && text.charAt(i) == '{' && text.charAt(i+1) == '#') { - int end = text.indexOf('}', i+2); - if (end == i + 8) { - String hex = text.substring(i+2, i+8); - if (hex.matches("[0-9a-fA-F]{6}")) { - appendHexSection(sb, hex); i += 9; continue; - } - } - } - sb.append(text.charAt(i++)); - } - return sb.toString(); - } - - // ── Hilfsmethoden ───────────────────────────────────────────────────────── - - private static void appendHexSection(StringBuilder sb, String hex) { - sb.append('\u00A7').append('x'); - for (char ch : hex.toCharArray()) sb.append('\u00A7').append(ch); - } - - private static String resolveColorToSection(String color) { - if (color == null) return ""; - color = color.trim(); - if (color.startsWith("#") && color.length() == 7 - && color.substring(1).matches("[0-9a-fA-F]{6}")) { - StringBuilder sb = new StringBuilder(); - appendHexSection(sb, color.substring(1)); - return sb.toString(); - } - if (color.startsWith("&") && color.length() == 2) return "\u00A7" + color.charAt(1); - return color; - } - - private static int[] hexToRgb(String color) { - String hex = color == null ? "" : color.trim(); - if (hex.startsWith("#")) hex = hex.substring(1); - if (hex.length() != 6) return new int[]{255, 255, 255}; - try { - return new int[]{ - Integer.parseInt(hex.substring(0,2), 16), - Integer.parseInt(hex.substring(2,4), 16), - Integer.parseInt(hex.substring(4,6), 16) - }; - } catch (Exception e) { return new int[]{255,255,255}; } - } - - private static int clamp(int v) { return Math.max(0, Math.min(255, v)); } - - /** - * Findet das schließende '>' für ein Tag das bei fromIndex beginnt. - * Berücksichtigt verschachtelte <...>. - */ - private static int findClosingAngle(String text, int fromIndex) { - int depth = 0; - for (int i = fromIndex; i < text.length(); i++) { - char ch = text.charAt(i); - if (ch == '<') depth++; - else if (ch == '>') { if (depth == 0) return i; depth--; } - } - return -1; - } - - - private static String fakeName(int i) { return String.format("~vt%03d", i); } - 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=&FarbCode Symbol\n" + - "# Farben: & + Code (z.B. &6 = Gold) oder &#RRGGBB / {#RRGGBB} / <#RRGGBB>\n" + - "# Emojis und Unicode-Symbole werden unterstützt.\n" + - "# Der Symbol-Text erscheint hinter dem Spielernamen in der Tablist.\n" + - "tablist.symbol.lobby=&f\uD83C\uDFE0\n" + - "tablist.symbol.sv1=&6\u26CF\uFE0F\n" + - "# tablist.symbol.farmwelt=&a\uD83C\uDF3F\n" + - "# tablist.symbol.spielerwelt=&e\u2728\n" + - "# tablist.symbol.game=&d\uD83C\uDFAE\n"; - try (OutputStream out = new FileOutputStream(f)) { out.write(content.getBytes(StandardCharsets.UTF_8)); } - catch (Exception e) { plugin.getLogger().warning("[TablistModule] Config: " + e.getMessage()); } - } - - private void loadConfig() { - File file = new File(plugin.getDataFolder(), CONFIG_FILE); - Map map = new LinkedHashMap<>(); - if (file.exists()) { - try (BufferedReader br = new BufferedReader(new InputStreamReader(new FileInputStream(file), StandardCharsets.UTF_8))) { - String line; - while ((line = br.readLine()) != null) { - line = line.trim(); - if (line.isEmpty() || line.startsWith("#")) continue; - int eq = line.indexOf('='); - if (eq < 1) continue; - map.put(line.substring(0, eq).trim(), line.substring(eq + 1)); - } - } catch (Exception e) { plugin.getLogger().warning("[TablistModule] Ladefehler: " + e.getMessage()); } - } - java.util.function.BiFunction get = (k,d) -> map.getOrDefault(k,d); - - configuredTabSize = parseInt(get.apply("tablist.tab_size", "0"), 0); - configuredColumns = parseInt(get.apply("tablist.columns", "0"), 0); - enabled = Boolean.parseBoolean(get.apply("tablist.enabled", "true")); - updateInterval = parseInt(get.apply("tablist.update_interval", "5"), 5); - layoutMode = get.apply("tablist.layout", "compact").trim().toLowerCase(); - // ── UPGRADE: column_header Modus ────────────────────────────────────── - columnHeaderMode = get.apply("tablist.column_header", "none").trim().toLowerCase(); - playerDisplayMode = get.apply("tablist.player_display", "server").trim().toLowerCase(); - - headerLine1 = get.apply("tablist.header.line1", headerLine1); - headerLine2 = get.apply("tablist.header.line2", headerLine2); - headerLine3 = get.apply("tablist.header.line3", headerLine3); - footerLine1 = get.apply("tablist.footer.line1", footerLine1); - footerLine2 = get.apply("tablist.footer.line2", footerLine2); - footerLine3 = get.apply("tablist.footer.line3", footerLine3); - compactHeader1 = get.apply("tablist.compact.header.line1", compactHeader1); - compactHeader2 = get.apply("tablist.compact.header.line2", compactHeader2); - compactHeader3 = get.apply("tablist.compact.header.line3", compactHeader3); - compactHeader1Spacer = Boolean.parseBoolean(get.apply("tablist.compact.header.line1.spacer", "false")); - compactHeader2Spacer = Boolean.parseBoolean(get.apply("tablist.compact.header.line2.spacer", "false")); - compactHeader3Spacer = Boolean.parseBoolean(get.apply("tablist.compact.header.line3.spacer", "false")); - compactHeader4 = get.apply("tablist.compact.header.line4", compactHeader4); - compactHeader4Spacer = Boolean.parseBoolean(get.apply("tablist.compact.header.line4.spacer", "false")); - compactFooter1 = get.apply("tablist.compact.footer.line1", compactFooter1); - compactFooter2 = get.apply("tablist.compact.footer.line2", compactFooter2); - compactFooter3 = get.apply("tablist.compact.footer.line3", compactFooter3); - compactFooter4 = get.apply("tablist.compact.footer.line4", compactFooter4); - compactFooter5 = get.apply("tablist.compact.footer.line5", compactFooter5); - compactFooter6 = get.apply("tablist.compact.footer.line6", compactFooter6); - compactFooter1Spacer = Boolean.parseBoolean(get.apply("tablist.compact.footer.line1.spacer", "false")); - compactFooter2Spacer = Boolean.parseBoolean(get.apply("tablist.compact.footer.line2.spacer", "false")); - compactFooter3Spacer = Boolean.parseBoolean(get.apply("tablist.compact.footer.line3.spacer", "false")); - compactFooter4Spacer = Boolean.parseBoolean(get.apply("tablist.compact.footer.line4.spacer", "false")); - compactFooter5Spacer = Boolean.parseBoolean(get.apply("tablist.compact.footer.line5.spacer", "false")); - compactFooter6Spacer = Boolean.parseBoolean(get.apply("tablist.compact.footer.line6.spacer", "false")); - colorSrvHeader = get.apply("tablist.color.server_header", colorSrvHeader); - showFooterServerList = Boolean.parseBoolean(get.apply("tablist.compact.footer.serverlist", "true")); - timeFormat = get.apply("tablist.time_format", timeFormat); - timeZone = get.apply("tablist.timezone", timeZone); - try { sdf = new SimpleDateFormat(timeFormat); sdf.setTimeZone(java.util.TimeZone.getTimeZone(timeZone)); } - catch (Exception e) { sdf = new SimpleDateFormat("HH:mm:ss / h:mm a"); } - - rankOrder.clear(); - String rankRaw = get.apply("tablist.rank_order", "").trim(); - if (!rankRaw.isEmpty()) for (String s : rankRaw.split(",")) { String t=s.trim(); if(!t.isEmpty()) rankOrder.add(t.toLowerCase()); } - - serverOrder.clear(); - String raw = get.apply("tablist.server_order", "").trim(); - if (!raw.isEmpty()) for (String s : raw.split(",")) { String t=s.trim(); if(!t.isEmpty()) serverOrder.add(t); } - - hiddenServers.clear(); - String hRaw = get.apply("tablist.hidden_servers", "").trim(); - if (!hRaw.isEmpty()) for (String s : hRaw.split(",")) { String t=s.trim().toLowerCase(); if(!t.isEmpty()) hiddenServers.add(t); } - - infoEntries.clear(); - String orderRaw = get.apply("tablist.info.order", "website,name,rank,server,world,time,teamspeak").trim(); - for (String id : orderRaw.split(",")) { - id = id.trim(); if (id.isEmpty()) continue; - boolean en = Boolean.parseBoolean(get.apply("tablist.info." + id + ".enabled", "true")); - String label = get.apply("tablist.info." + id + ".label", ""); - String type = get.apply("tablist.info." + id + ".type", "custom"); - String value = get.apply("tablist.info." + id + ".value", ""); - infoEntries.add(new InfoEntry(label, type, value, en)); - } - if (infoEntries.isEmpty()) { - infoEntries.add(new InfoEntry("&b&lWebsite:", "website", "&fviper-network.de", true)); - infoEntries.add(new InfoEntry("&b&lName:", "name", "", true)); - infoEntries.add(new InfoEntry("&b&lRank:", "rank", "", true)); - infoEntries.add(new InfoEntry("&b&lServer:", "server", "", true)); - infoEntries.add(new InfoEntry("&b&lWorld:", "world", "", true)); - infoEntries.add(new InfoEntry("&b&lTime:", "time", "", true)); - infoEntries.add(new InfoEntry("&b&lTeamspeak:", "teamspeak", "&fts.viper-network.de", true)); - } - - // ── Server-Symbole aus tablist.properties ───────────────────────────── - // Format: tablist.symbol.=&FarbCode Symbol - // Beispiel: tablist.symbol.lobby=&f🏠 - // tablist.symbol.sv1=&6⛏️ - serverSymbols.clear(); - for (Map.Entry entry : map.entrySet()) { - String key = entry.getKey(); - if (key.startsWith("tablist.symbol.")) { - String srvName = key.substring("tablist.symbol.".length()).trim().toLowerCase(); - String symbol = entry.getValue().trim(); - if (!srvName.isEmpty() && !symbol.isEmpty()) { - serverSymbols.put(srvName, symbol); - plugin.getLogger().info("[TablistModule] Symbol: " + srvName + " → " + symbol); - } - } - } - } - -} \ No newline at end of file