diff --git a/StatusAPI/src/main/java/net/viper/status/modules/scoreboard/ScoreboardModule.java b/StatusAPI/src/main/java/net/viper/status/modules/scoreboard/ScoreboardModule.java index a41d53b..a410f75 100644 --- a/StatusAPI/src/main/java/net/viper/status/modules/scoreboard/ScoreboardModule.java +++ b/StatusAPI/src/main/java/net/viper/status/modules/scoreboard/ScoreboardModule.java @@ -7,6 +7,7 @@ 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.event.TabCompleteEvent; import net.md_5.bungee.api.plugin.Command; import net.md_5.bungee.api.plugin.Listener; import net.md_5.bungee.api.plugin.Plugin; @@ -1217,22 +1218,256 @@ public class ScoreboardModule implements Module, Listener { // ── Farb-Hilfsmethoden ──────────────────────────────────────────────────── + // ══════════════════════════════════════════════════════════════════════════ + // 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 " "; - return ChatColor.translateAlternateColorCodes('&', hexToSection(s)); + s = parseMiniMessage(s); // MiniMessage-Tags (, , <#>, , usw.) + s = parseHexAmpersand(s); // &#RRGGBB und {#RRGGBB} + return net.md_5.bungee.api.ChatColor.translateAlternateColorCodes('&', s); } - private static String hexToSection(String text) { - if (text == null || !text.contains("&#")) return text == null ? "" : text; + 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()) { - if (i + 7 <= text.length() && text.charAt(i) == '&' && text.charAt(i+1) == '#') { + // &#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}")) { - sb.append('\u00A7').append('x'); - for (char ch : hex.toCharArray()) sb.append('\u00A7').append(ch); - i += 8; continue; + 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++)); @@ -1240,17 +1475,65 @@ public class ScoreboardModule implements Module, Listener { return sb.toString(); } - private static String stripColors(String s) { - return s == null ? "" : ChatColor.stripColor(c(s)); + // ── 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; + } + + + + // ── Config ─────────────────────────────────────────────────────────────── private void ensureConfigExists() { File f = new File(plugin.getDataFolder(), CONFIG_FILE); if (f.exists()) return; if (!plugin.getDataFolder().exists()) plugin.getDataFolder().mkdirs(); - String content = + String content = "# ScoreboardModule Konfiguration\n" + "# Platzhalter Spieler: %player% %rank% %money% %server% %compass% %health% %hearts% %date%\n" + "# %ping% %online% %maxplayers% %time% %playtime% %news%\n" + @@ -1258,59 +1541,59 @@ public class ScoreboardModule implements Module, Listener { "# Platzhalter Admin: %tps% %ram% %proxymem% %uptime% %servers%\n" + "# Gradient: %gradient:FARBE1:FARBE2:TEXT%\n" + "# Sonstiges: %line%\n" + - "# Farben: &-Codes und Hex &#FF6600\n\n" + + "# Farben: &-Codes und Hex &#FF6600\n" + + "\n" + "scoreboard.enabled=true\n" + "scoreboard.update_interval=500\n" + "scoreboard.title=&lViper Network\n" + "scoreboard.admin_title=&l[Admin] Panel\n" + - "scoreboard.supporter_title=&l[Support] Panel\n\n" + + "scoreboard.supporter_title=&l[Support] Panel\n" + + "\n" + "scoreboard.ticker.text=\n" + "scoreboard.ticker.width=26\n" + - "scoreboard.ticker.speed=1\n\n" + + "scoreboard.ticker.speed=1\n" + + "\n" + "scoreboard.rainbow.enabled=true\n" + "# wave=fließende Welle, chars=Regenbogen pro Buchstabe, line=eine Farbe\n" + "scoreboard.rainbow.mode=wave\n" + "# Wellengeschwindigkeit: 1=sehr langsam, 10=normal, 50=schnell\n" + "scoreboard.rainbow.speed=10\n" + "# Leer = voller HSB-Regenbogen\n" + - "scoreboard.rainbow.colors=#FF0000,#FF6600,#FFFF00,#00FF00,#00FFFF,#0000FF,#FF00FF\n\n" + + "scoreboard.rainbow.colors=#FF0000,#FF6600,#FFFF00,#00FF00,#00FFFF,#0000FF,#FF00FF\n" + + "\n" + "scoreboard.admin_permission=statusapi.scoreboard.admin\n" + - "scoreboard.supporter_permission=statusapi.scoreboard.supporter\n\n" + + "scoreboard.supporter_permission=statusapi.scoreboard.supporter\n" + + "\n" + "scoreboard.time_format=HH:mm\n" + "scoreboard.date_format=dd.MM.yyyy\n" + "scoreboard.timezone=Europe/Berlin\n" + "scoreboard.money_format=#,##0.00\n" + - "scoreboard.money_decimal_separator=,\n\n" + + "scoreboard.money_decimal_separator=,\n" + + "\n" + "# SEPARATOR – wird als %line% Placeholder genutzt\n" + - "# scoreboard.separator=&8&m-------------------- (Standard)\n" + - "# scoreboard.separator=&8&m==================== (Doppelt)\n" + - "# scoreboard.separator=&8&m~~~~~~~~~~~~~~~~~~~~ (Wellig)\n" + - "# scoreboard.separator=&8&m──────────────────── (Duenn)\n" + - "# scoreboard.separator=&8&m════════════════════ (Dick)\n" + - "# scoreboard.separator=%gradient:&8:&7:────────────────────% (Gradient)\n" + - "# scoreboard.separator= (Leer)\n" + "scoreboard.separator=&8&m--------------------\n" + "# News-Ticker (erscheint als %news% Placeholder)\n" + "scoreboard.news.text=&eWillkommen auf Viper Network!\n" + "scoreboard.news.prefix=&8[&6News&8] &r\n" + "scoreboard.news.width=20\n" + - "scoreboard.news.speed=1\n\n" + + "scoreboard.news.speed=1\n" + + "\n" + "scoreboard.rotation_interval=4\n" + "# ===================================================\n" + "# ZEILEN - max 15 sichtbar\n" + "# ===================================================\n" + "scoreboard.lines.1=%line%\n" + - "scoreboard.lines.2=%gradient:&b:&f:&b:&l> Player Info:%\n" + + "scoreboard.lines.2=%gradient:&6:&f:&6:&l> Player Info:%\n" + "scoreboard.lines.3=&7%rank% &f%player%\n" + "scoreboard.lines.4=\n" + "scoreboard.lines.5=&7Spielzeit: &f%playtime%\n" + "scoreboard.lines.5.2=&7Leben: &c%health%\n" + "scoreboard.lines.5.3=&7Hunger: B4513%foodsym%\n" + "scoreboard.lines.6=\n" + - "scoreboard.lines.7=%gradient:&b:&f:&b:&l> Money:%\n" + + "scoreboard.lines.7=%gradient:&6:&f:&6:&l> Money:%\n" + "scoreboard.lines.8=&a$%money%\n" + "scoreboard.lines.9=\n" + - "scoreboard.lines.10=%gradient:&b:&f:&b:&l> Server Info:%\n" + + "scoreboard.lines.10=%gradient:&6:&f:&6:&l> Server Info:%\n" + "scoreboard.lines.11=&f%server%\n" + "scoreboard.lines.11.2=&7Ping: &f%ping%ms &8| &7Online: &f%online%\n" + "scoreboard.lines.12=\n" + @@ -1321,13 +1604,13 @@ public class ScoreboardModule implements Module, Listener { "# ADMIN-ZEILEN\n" + "# ===================================================\n" + "scoreboard.admin_lines.1=%line%\n" + - "scoreboard.admin_lines.2=%gradient:&b:&f:&b:&l> Player Info:%\n" + + "scoreboard.admin_lines.2=%gradient:&6:&f:&6:&l> Player Info:%\n" + "scoreboard.admin_lines.3=&7%rank% &f%player%\n" + "scoreboard.admin_lines.4=&7Gamemode: &f%gamemode%\n" + "scoreboard.admin_lines.5=&7Leben: &c%health%\n" + "scoreboard.admin_lines.5.2=&7Hunger: B4513%foodsym%\n" + "scoreboard.admin_lines.6=\n" + - "scoreboard.admin_lines.7=%gradient:&b:&f:&b:&l> Server Info:%\n" + + "scoreboard.admin_lines.7=%gradient:&6:&f:&6:&l> Server Info:%\n" + "scoreboard.admin_lines.8=&f%server% &8| &7RAM: &e%ram%\n" + "scoreboard.admin_lines.8.2=&7Proxy: &f%uptime%\n" + "scoreboard.admin_lines.9=\n" + @@ -1355,8 +1638,9 @@ public class ScoreboardModule implements Module, Listener { "scoreboard.supporter_lines.12=&7Zeit: &f%time%\n" + "scoreboard.supporter_lines.13=\n" + "scoreboard.supporter_lines.14=%line%\n" + - "scoreboard.supporter_lines.15=&7%compass%\n"; - try (OutputStream out = new FileOutputStream(f)) { + "scoreboard.supporter_lines.15=&7%compass%\n" + + ""; + try (OutputStream out = new FileOutputStream(f)) { out.write(content.getBytes(StandardCharsets.UTF_8)); } catch (Exception e) { plugin.getLogger().warning("[ScoreboardModule] Config: " + e.getMessage()); @@ -1554,6 +1838,35 @@ public class ScoreboardModule implements Module, Listener { * * Aliase: /sb, /togglesb */ + private static final List SB_SUBS = Arrays.asList("hide", "show", "player", "admin", "supporter"); + + /** Tab-Completion für /scoreboard via TabCompleteEvent */ + @EventHandler + public void onTabComplete(TabCompleteEvent event) { + if (!(event.getSender() instanceof ProxiedPlayer)) return; + String cursor = event.getCursor(); + if (cursor == null) return; + String lower = cursor.toLowerCase(); + boolean match = lower.startsWith("/scoreboard ") || lower.startsWith("/sb ") + || lower.startsWith("/togglesb "); + if (!match) return; + + ProxiedPlayer p = (ProxiedPlayer) event.getSender(); + int spaceIdx = cursor.indexOf(' '); + String typed = spaceIdx >= 0 ? cursor.substring(spaceIdx + 1).toLowerCase() : ""; + + List suggestions = new ArrayList<>(); + for (String sub : SB_SUBS) { + // Supporter und Admin nur anzeigen wenn Berechtigung vorhanden + if (sub.equals("admin") && !p.hasPermission(adminPermission)) continue; + if (sub.equals("supporter") && !p.hasPermission(supporterPermission) + && !p.hasPermission(adminPermission)) continue; + if (sub.startsWith(typed)) suggestions.add(sub); + } + event.getSuggestions().clear(); + event.getSuggestions().addAll(suggestions); + } + private class ScoreboardToggleCommand extends Command { ScoreboardToggleCommand() { @@ -1653,4 +1966,5 @@ public class ScoreboardModule implements Module, Listener { } } + } \ No newline at end of file diff --git a/StatusAPI/src/main/java/net/viper/status/modules/tablist/TablistModule.java b/StatusAPI/src/main/java/net/viper/status/modules/tablist/TablistModule.java index 90189a2..a18f754 100644 --- a/StatusAPI/src/main/java/net/viper/status/modules/tablist/TablistModule.java +++ b/StatusAPI/src/main/java/net/viper/status/modules/tablist/TablistModule.java @@ -23,12 +23,14 @@ 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"; + private static final String CONFIG_FILE = "tablist.properties"; + // ── NEU: Server-Symbol-Config ────────────────────────────────────────────── // Leerer Skin (grauer Kopf) für Platzhalter-Slots private static final net.md_5.bungee.protocol.data.Property[] EMPTY_SKIN = { @@ -48,6 +50,18 @@ public class TablistModule implements Module, Listener { // Skin-Cache (pro Spieler) private final ConcurrentHashMap skinCache = new ConcurrentHashMap<>(); + // ── NEU: Server-Symbol-Map (serverName lowercase → colored symbol string) ─ + private final Map serverSymbols = new LinkedHashMap<>(); + + // ── NEU: Spalten-Header-Modus ────────────────────────────────────────────── + // "full" → bisheriges Verhalten: Spalten-Header belegt Zeile 0 (große Markierung) + // "none" → kein Header; Zeile 0 ist frei für Spieler oder bleibt leer + // "small" → kein Slot-Header, aber der Spalten-Name erscheint im Tab-Header/Footer + // (empfohlen für MuckiDEE: kleine Markierungen bleiben, große weg) + private String columnHeaderMode = "none"; // default: keine großen Markierungen + // player_display: "server" = Server-basiert (default) | "custom" = alle zusammen, links→rechts nach Rang + private String playerDisplayMode = "server"; + // Config private boolean enabled = true; private int updateInterval = 5; @@ -63,19 +77,24 @@ public class TablistModule implements Module, Listener { private String compactHeader1 = "&6&lViper Network &8• &7%online% Spieler online"; private String compactHeader2 = ""; private String compactHeader3 = ""; + private boolean compactHeader1Spacer = false; private boolean compactHeader2Spacer = false; private boolean compactHeader3Spacer = 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 columnHeaderMode = "none"; - private final Map serverSymbols = new LinkedHashMap<>(); + private boolean showFooterServerList = true; // tablist.compact.footer.serverlist=true/false private String timeFormat = "HH:mm:ss / h:mm a"; private String timeZone = "Europe/Berlin"; private SimpleDateFormat sdf; @@ -129,7 +148,8 @@ public class TablistModule implements Module, Listener { plugin.getLogger().info("[TablistModule] Tablist-Spalten: " + getServerOrder()); recalculateGrid(); }, 3L, TimeUnit.SECONDS); - plugin.getLogger().info("[TablistModule] Aktiviert. Grid=" + columns + "x" + rows + " layout=" + layoutMode); + plugin.getLogger().info("[TablistModule] Aktiviert. Grid=" + columns + "x" + rows + " layout=" + layoutMode + + " column_header=" + columnHeaderMode + " symbols=" + serverSymbols.size()); } @Override @@ -150,13 +170,12 @@ public class TablistModule implements Module, Listener { } catch (Exception ignored) {} } } catch (Exception ignored) {} - if (configuredTabSize > 0) tabSize = configuredTabSize; // manuell gesetzt in tablist.properties + if (configuredTabSize > 0) tabSize = configuredTabSize; tabSizeMax = tabSize; - rows = ROWS; // immer 20 – Minecraft-Client-Pflicht + rows = ROWS; boolean hasInfo = !"compact".equalsIgnoreCase(layoutMode); int serverCount = getServerOrder().size(); int needed = (hasInfo ? 1 : 0) + Math.max(1, serverCount); - // Spalten = benötigte Spalten, aber max was tab-size erlaubt (tab-size/20) columns = Math.max(hasInfo ? 2 : 1, Math.min(needed, tabSize / ROWS)); total = ROWS * columns; if (needed > tabSize / ROWS) { @@ -182,7 +201,6 @@ public class TablistModule implements Module, Listener { if (skin != null && skin.length > 0) skinCache.put(p.getUniqueId(), skin); ProxyServer.getInstance().getScheduler().schedule(plugin, () -> { updateTablist(p); - // Nach 2s nochmals für alle damit der neue Spieler mit Kopf erscheint ProxyServer.getInstance().getScheduler().schedule(plugin, this::updateAll, 2L, TimeUnit.SECONDS); }, 2L, TimeUnit.SECONDS); } @@ -192,25 +210,18 @@ public class TablistModule implements Module, Listener { if (!enabled) return; ProxiedPlayer switched = e.getPlayer(); - // Skin sofort cachen (noch auf dem alten Server, LoginProfile noch verfügbar) net.md_5.bungee.protocol.data.Property[] skin = fetchSkin(switched); if (skin != null && skin.length > 0) skinCache.put(switched.getUniqueId(), skin); - // Nach 1s: alle Fake-Slots bei allen Viewern entfernen → erzwingt frisches ADD_PLAYER mit neuem Skin ProxyServer.getInstance().getScheduler().schedule(plugin, () -> { - // Skin nochmals versuchen (jetzt auf neuem Server) net.md_5.bungee.protocol.data.Property[] freshSkin = fetchSkin(switched); if (freshSkin != null && freshSkin.length > 0) skinCache.put(switched.getUniqueId(), freshSkin); - // Alle Slots bei allen Viewern entfernen for (ProxiedPlayer viewer : ProxyServer.getInstance().getPlayers()) { try { removeFakeSlots(viewer); } catch (Exception ignored) {} } - - // Sofort neu aufbauen (kein weiterer Delay nötig da removeFakeSlots synchron ist) updateAll(); - // Nochmal nach 2s als Sicherheit ProxyServer.getInstance().getScheduler().schedule(plugin, () -> { net.md_5.bungee.protocol.data.Property[] s2 = fetchSkin(switched); if (s2 != null && s2.length > 0) skinCache.put(switched.getUniqueId(), s2); @@ -227,8 +238,6 @@ public class TablistModule implements Module, Listener { public void onDisconnect(PlayerDisconnectEvent e) { if (!enabled) return; skinCache.remove(e.getPlayer().getUniqueId()); - // Erst alle Fake-Slots entfernen, dann nach kurzer Pause neu aufbauen - // So verschwindet der Kopf des Spielers zuverlässig ProxyServer.getInstance().getScheduler().schedule(plugin, () -> { for (ProxiedPlayer viewer : ProxyServer.getInstance().getPlayers()) { try { removeFakeSlots(viewer); } catch (Exception ignored) {} @@ -242,7 +251,6 @@ public class TablistModule implements Module, Listener { private void updateAll() { recalculateGrid(); - // Fehlende Skins nachladen for (ProxiedPlayer p : ProxyServer.getInstance().getPlayers()) { if (!skinCache.containsKey(p.getUniqueId())) { net.md_5.bungee.protocol.data.Property[] skin = fetchSkin(p); @@ -254,10 +262,15 @@ public class TablistModule implements Module, Listener { private void recalculateGrid() { boolean hasInfo = !"compact".equalsIgnoreCase(layoutMode); - int serverCount = getServerOrder().size(); - int needed = (hasInfo ? 1 : 0) + Math.max(1, serverCount); - // Spalten = benötigte Spalten, aber max was tab-size erlaubt (tab-size/20) - int newColumns = Math.max(hasInfo ? 2 : 1, Math.min(needed, tabSizeMax / ROWS)); + int newColumns; + if ("custom".equalsIgnoreCase(playerDisplayMode)) { + // Custom-Modus: immer 3 Spalten (konfigurierbar via tab_size) + newColumns = Math.min(3, tabSizeMax / ROWS); + } else { + int serverCount = getServerOrder().size(); + int needed = (hasInfo ? 1 : 0) + Math.max(1, serverCount); + newColumns = Math.max(hasInfo ? 2 : 1, Math.min(needed, tabSizeMax / ROWS)); + } int newTotal = ROWS * newColumns; if (newColumns == columns && newTotal == total) return; for (ProxiedPlayer p : ProxyServer.getInstance().getPlayers()) { @@ -267,7 +280,7 @@ public class TablistModule implements Module, Listener { columns = newColumns; total = newTotal; initUuids(); - plugin.getLogger().info("[TablistModule] Grid: " + columns + "x" + rows + "=" + total + " (" + serverCount + " Server)"); + plugin.getLogger().info("[TablistModule] Grid: " + columns + "x" + rows + "=" + total + " (" + getServerOrder().size() + " Server, Modus: " + playerDisplayMode + ")"); } private void updateTablist(ProxiedPlayer viewer) { @@ -288,7 +301,12 @@ public class TablistModule implements Module, Listener { header = c(headerLine1) + "\n" + c(headerLine2) + "\n" + c(headerLine3); footer = c(footerLine1) + "\n" + c(footerLine2) + "\n" + c(footerLine3); } - viewer.setTabHeader(new TextComponent(header), new TextComponent(footer)); + // 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)); hideRealPlayers(viewer); sendSlots(viewer, buildItems(viewer)); } catch (Exception ex) { @@ -300,7 +318,7 @@ public class TablistModule implements Module, Listener { private String buildCompactHeader(ProxiedPlayer viewer, String srv, String world, String rank, String time, String balance, int online) { StringBuilder sb = new StringBuilder(); - appendLine(sb, compactHeader1, false, viewer, srv, world, rank, time, balance, online); + 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); return sb.toString(); @@ -321,9 +339,11 @@ public class TablistModule implements Module, Listener { if (sb.length() > 0) sb.append("\n"); sb.append(c(sLine.toString())); } - appendLine(sb, compactFooter2, false, viewer, srv, world, rank, time, balance, online); - appendLine(sb, compactFooter3, false, viewer, srv, world, rank, time, balance, online); + 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(); } @@ -331,7 +351,7 @@ public class TablistModule implements Module, Listener { boolean empty = line == null || line.trim().isEmpty(); if (empty && !spacer) return; if (sb.length() > 0) sb.append("\n"); - sb.append(empty ? " " : c(replacePlaceholders(line, viewer, srv, world, rank, time, balance, online))); + sb.append(empty ? "\u00A0" : c(replacePlaceholders(line, viewer, srv, world, rank, time, balance, online))); } // ── Items ────────────────────────────────────────────────────────────────── @@ -343,6 +363,7 @@ public class TablistModule implements Module, Listener { 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") oder nicht (= "none"/"small") boolean useSlotHeader = "full".equalsIgnoreCase(columnHeaderMode); // Info-Spalte (nur classic) @@ -372,26 +393,62 @@ public class TablistModule implements Module, Listener { } } - // Server-Spalten - 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); + 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)); + 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++; + } + } + } 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++; + } } } } @@ -405,7 +462,9 @@ public class TablistModule implements Module, Listener { item.setGamemode(0); item.setPing(pings[i]); item.setListed(true); - item.setDisplayName(new TextComponent(texts[i] == null || texts[i].isEmpty() ? " " : texts[i])); + 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; @@ -418,20 +477,33 @@ public class TablistModule implements Module, Listener { if (sendPacketQueuedMethod == null) return; try { Collection online = ProxyServer.getInstance().getPlayers(); + + // Sammle alle UUIDs die versteckt werden sollen: + // 1. Echte Spieler + // 2. BungeeCord-interne Server-Eintraege (werden von BC selbst in die Tablist geschrieben) List toHide = new ArrayList<>(); for (ProxiedPlayer p : online) toHide.add(p.getUniqueId()); + + // BungeeCord schreibt fuer jeden Server einen eigenen Eintrag mit einer + // deterministischen UUID (nameUUIDFromBytes des Server-Namens). + // Diese auch auf listed=false setzen damit die grossen Spalten-Header verschwinden. for (String srvName : ProxyServer.getInstance().getServers().keySet()) { try { - toHide.add(UUID.nameUUIDFromBytes(("OfflinePlayer:" + srvName).getBytes(StandardCharsets.UTF_8))); - toHide.add(UUID.nameUUIDFromBytes(srvName.getBytes(StandardCharsets.UTF_8))); + UUID srvUuid = UUID.nameUUIDFromBytes(("OfflinePlayer:" + srvName).getBytes(StandardCharsets.UTF_8)); + toHide.add(srvUuid); + UUID srvUuid2 = UUID.nameUUIDFromBytes(srvName.getBytes(StandardCharsets.UTF_8)); + toHide.add(srvUuid2); } catch (Exception ignored) {} } + if (toHide.isEmpty()) return; PlayerListItemUpdate pkt = new PlayerListItemUpdate(); pkt.setActions(EnumSet.of(PlayerListItemUpdate.Action.UPDATE_LISTED)); Item[] items = new Item[toHide.size()]; int idx = 0; - for (UUID uuid : toHide) { Item it = new Item(); it.setUuid(uuid); it.setListed(false); items[idx++] = it; } + for (UUID uuid : toHide) { + Item it = new Item(); it.setUuid(uuid); it.setListed(false); items[idx++] = it; + } pkt.setItems(items); sendPacketQueuedMethod.invoke(viewer, pkt); } catch (Exception e) { plugin.getLogger().warning("[TablistModule] hideRealPlayers: " + e.getMessage()); } @@ -440,7 +512,6 @@ public class TablistModule implements Module, Listener { @SuppressWarnings("unchecked") private void sendSlots(ProxiedPlayer viewer, Item[] items) { if (sendPacketQueuedMethod == null) return; - // Immer vollständiges ADD_PLAYER – einfach und zuverlässig PlayerListItemUpdate pkt = new PlayerListItemUpdate(); pkt.setActions(EnumSet.of( PlayerListItemUpdate.Action.ADD_PLAYER, @@ -473,11 +544,16 @@ public class TablistModule implements Module, Listener { // ── Helpers ──────────────────────────────────────────────────────────────── + /** + * ── 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 raw = serverSymbols.get(player.getServer().getInfo().getName().toLowerCase()); + String srvKey = player.getServer().getInfo().getName().toLowerCase(); + String raw = serverSymbols.get(srvKey); if (raw == null || raw.isEmpty()) return ""; - return c(raw); + return c(raw); // Farb-Codes und Hex-Farben auflösen } private net.md_5.bungee.protocol.data.Property[] fetchSkin(ProxiedPlayer player) { @@ -495,7 +571,6 @@ public class TablistModule implements Module, Listener { List list; if (!serverOrder.isEmpty()) { list = new ArrayList<>(serverOrder); - // Versteckte Server auch aus manueller Liste entfernen list.removeIf(s -> hiddenServers.contains(s.toLowerCase())); } else { list = new ArrayList<>(); @@ -565,6 +640,7 @@ public class TablistModule implements Module, Listener { 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) {} @@ -582,7 +658,6 @@ public class TablistModule implements Module, Listener { private String replacePlaceholders(String text, ProxiedPlayer viewer, String srv, String world, String rank, String time, String balance, int online) { if (text == null) return ""; - // PAPI zuerst, native Tokens danach (überschreiben PAPI-Werte falls gleicher Name) String result = resolvePapiPlaceholders(text, viewer.getUniqueId()); result = result.replace("%player%", viewer.getName()).replace("%rank%", rank) .replace("%server%", srv).replace("%world%", world).replace("%time%", time) @@ -593,7 +668,7 @@ public class TablistModule implements Module, Listener { private static String resolvePapiPlaceholders(String text, UUID uuid) { if (text == null || !text.contains("%")) return text; - java.util.Map papiMap = net.viper.status.StatusAPI.playerPapi.get(uuid); + Map papiMap = net.viper.status.StatusAPI.playerPapi.get(uuid); if (papiMap == null || papiMap.isEmpty()) return text; StringBuilder sb = new StringBuilder(); int i = 0; @@ -619,54 +694,315 @@ public class TablistModule implements Module, Listener { 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 = replaceHexColors(s); - return ChatColor.translateAlternateColorCodes('&', 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 replaceHexColors(String text) { - if (text == null) return null; - if (!text.contains("&#") && !text.contains("{#") && !text.contains("<#")) return text; + 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()) { - if (i + 7 <= text.length() && text.charAt(i) == '&' && text.charAt(i+1) == '#') { + // &#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}")) { - sb.append('\u00A7').append('x'); - for (char ch : hex.toCharArray()) sb.append('\u00A7').append(ch); - i += 8; continue; + 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) { + if (end == i + 8) { String hex = text.substring(i+2, i+8); if (hex.matches("[0-9a-fA-F]{6}")) { - sb.append('\u00A7').append('x'); - for (char ch : hex.toCharArray()) sb.append('\u00A7').append(ch); - i += 9; continue; + appendHexSection(sb, hex); i += 9; continue; } } } - // Format 3: <#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}")) { - sb.append('\u00A7').append('x'); - for (char ch : hex.toCharArray()) sb.append('\u00A7').append(ch); - i += 9; continue; - } - } - } - sb.append(text.charAt(i)); i++; + 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.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()); } } @@ -772,6 +1122,10 @@ public class TablistModule implements Module, Listener { 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); @@ -781,17 +1135,23 @@ public class TablistModule implements Module, Listener { 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")); 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")); - columnHeaderMode = get.apply("tablist.column_header", "none").trim().toLowerCase(); 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)); } @@ -829,16 +1189,22 @@ public class TablistModule implements Module, Listener { infoEntries.add(new InfoEntry("&b&lTeamspeak:", "teamspeak", "&fts.viper-network.de", true)); } - // Server-Symbole + // ── 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()) + if (!srvName.isEmpty() && !symbol.isEmpty()) { serverSymbols.put(srvName, symbol); + plugin.getLogger().info("[TablistModule] Symbol: " + srvName + " → " + symbol); + } } } } -} \ No newline at end of file + +} diff --git a/StatusAPI/src/main/resources/plugin.yml b/StatusAPI/src/main/resources/plugin.yml index 5d98cf2..c4d4267 100644 --- a/StatusAPI/src/main/resources/plugin.yml +++ b/StatusAPI/src/main/resources/plugin.yml @@ -1,6 +1,6 @@ name: StatusAPI main: net.viper.status.StatusAPI -version: 4.1.1 +version: 4.1.2 author: M_Viper description: StatusAPI für BungeeCord inkl. Update-Checker, Modul-System und ChatModule # Mindestanforderung: Minecraft 1.20 / BungeeCord mit PlayerChatEvent-Unterstützung