Upload folder via GUI - src

This commit is contained in:
Git Manager GUI
2026-05-21 10:10:02 +02:00
parent 627559356b
commit abcbd0bbae
3 changed files with 803 additions and 123 deletions

View File

@@ -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.PlayerDisconnectEvent;
import net.md_5.bungee.api.event.PostLoginEvent; import net.md_5.bungee.api.event.PostLoginEvent;
import net.md_5.bungee.api.event.ServerSwitchEvent; 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.Command;
import net.md_5.bungee.api.plugin.Listener; import net.md_5.bungee.api.plugin.Listener;
import net.md_5.bungee.api.plugin.Plugin; import net.md_5.bungee.api.plugin.Plugin;
@@ -1217,22 +1218,256 @@ public class ScoreboardModule implements Module, Listener {
// ── Farb-Hilfsmethoden ──────────────────────────────────────────────────── // ── 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
// <color:#RRGGBB> → MiniMessage color-Tag
// <gradient:#C1:#C2:Text> → Farbverlauf (beliebig viele Farb-Stopps)
// <shadow:#C:Text> → Text in Schattenfarbe
// <b> <i> <u> <st> <obf> → Formatierungen
// &l &o &n &m &k &r → Standard-Formatierungen
// ══════════════════════════════════════════════════════════════════════════
private static String c(String s) { private static String c(String s) {
if (s == null) return " "; if (s == null) return " ";
return ChatColor.translateAlternateColorCodes('&', hexToSection(s)); s = parseMiniMessage(s); // MiniMessage-Tags (<gradient:>, <shadow:>, <#>, <color:>, <b> usw.)
s = parseHexAmpersand(s); // &#RRGGBB und {#RRGGBB}
return net.md_5.bungee.api.ChatColor.translateAlternateColorCodes('&', s);
} }
private static String hexToSection(String text) { private static String stripColors(String s) {
if (text == null || !text.contains("&#")) return text == null ? "" : text; 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: <color:#>, <#>, <b>, <i>, <u>, <st>, <obf>, </...>
text = parseSimpleTags(text);
return text;
}
// ── <gradient:#C1:#C2:...:TEXT> ──────────────────────────────────────────
private static String parseGradientTags(String text) {
if (!text.contains("<gradient:")) return text;
StringBuilder result = new StringBuilder();
int i = 0;
while (i < text.length()) {
int start = text.indexOf("<gradient:", i);
if (start < 0) { result.append(text, i, text.length()); break; }
result.append(text, i, start);
// Schließendes > 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<String> 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<String> 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();
}
// ── <shadow:#RRGGBB:TEXT> ─────────────────────────────────────────────────
private static String parseShadowTags(String text) {
if (text == null || !text.contains("<shadow:")) return text == null ? "" : text;
StringBuilder result = new StringBuilder();
int i = 0;
while (i < text.length()) {
int start = text.indexOf("<shadow:", i);
if (start < 0) { result.append(text, i, text.length()); break; }
result.append(text, i, start);
int end = findClosingAngle(text, start + 1);
if (end < 0) { result.append(text, i, text.length()); break; }
String inner = text.substring(start + 1, end); // "shadow:#RRGGBB:TEXT"
// Format: shadow:COLOR:TEXT
int firstColon = inner.indexOf(':');
int secondColon = firstColon >= 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("<b>", "&l").replace("</b>", "&r");
text = text.replace("<i>", "&o").replace("</i>", "&r");
text = text.replace("<u>", "&n").replace("</u>", "&r");
text = text.replace("<st>", "&m").replace("</st>", "&r");
text = text.replace("<obf>", "&k").replace("</obf>", "&r");
text = text.replace("<reset>", "&r").replace("</reset>", "");
// Closing-Tags entfernen (werden nach Verarbeitung nicht mehr benötigt)
text = text.replaceAll("</gradient>", "");
text = text.replaceAll("</shadow>", "");
text = text.replaceAll("</color>", "");
// <color:#RRGGBB> 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; }
// <color:#RRGGBB>
if (text.startsWith("<color:#", i)) {
int end = text.indexOf('>', 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(); StringBuilder sb = new StringBuilder();
int i = 0; int i = 0;
while (i < text.length()) { 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); String hex = text.substring(i+2, i+8);
if (hex.matches("[0-9a-fA-F]{6}")) { if (hex.matches("[0-9a-fA-F]{6}")) {
sb.append('\u00A7').append('x'); appendHexSection(sb, hex); i += 8; continue;
for (char ch : hex.toCharArray()) sb.append('\u00A7').append(ch); }
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++)); sb.append(text.charAt(i++));
@@ -1240,17 +1475,65 @@ public class ScoreboardModule implements Module, Listener {
return sb.toString(); return sb.toString();
} }
private static String stripColors(String s) { // ── Hilfsmethoden ─────────────────────────────────────────────────────────
return s == null ? "" : ChatColor.stripColor(c(s));
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 ─────────────────────────────────────────────────────────────── // ── Config ───────────────────────────────────────────────────────────────
private void ensureConfigExists() { private void ensureConfigExists() {
File f = new File(plugin.getDataFolder(), CONFIG_FILE); File f = new File(plugin.getDataFolder(), CONFIG_FILE);
if (f.exists()) return; if (f.exists()) return;
if (!plugin.getDataFolder().exists()) plugin.getDataFolder().mkdirs(); if (!plugin.getDataFolder().exists()) plugin.getDataFolder().mkdirs();
String content = String content =
"# ScoreboardModule Konfiguration\n" + "# ScoreboardModule Konfiguration\n" +
"# Platzhalter Spieler: %player% %rank% %money% %server% %compass% %health% %hearts% %date%\n" + "# Platzhalter Spieler: %player% %rank% %money% %server% %compass% %health% %hearts% %date%\n" +
"# %ping% %online% %maxplayers% %time% %playtime% %news%\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" + "# Platzhalter Admin: %tps% %ram% %proxymem% %uptime% %servers%\n" +
"# Gradient: %gradient:FARBE1:FARBE2:TEXT%\n" + "# Gradient: %gradient:FARBE1:FARBE2:TEXT%\n" +
"# Sonstiges: %line%\n" + "# Sonstiges: %line%\n" +
"# Farben: &-Codes und Hex &#FF6600\n\n" + "# Farben: &-Codes und Hex &#FF6600\n" +
"\n" +
"scoreboard.enabled=true\n" + "scoreboard.enabled=true\n" +
"scoreboard.update_interval=500\n" + "scoreboard.update_interval=500\n" +
"scoreboard.title=&lViper Network\n" + "scoreboard.title=&lViper Network\n" +
"scoreboard.admin_title=&l[Admin] Panel\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.text=\n" +
"scoreboard.ticker.width=26\n" + "scoreboard.ticker.width=26\n" +
"scoreboard.ticker.speed=1\n\n" + "scoreboard.ticker.speed=1\n" +
"\n" +
"scoreboard.rainbow.enabled=true\n" + "scoreboard.rainbow.enabled=true\n" +
"# wave=fließende Welle, chars=Regenbogen pro Buchstabe, line=eine Farbe\n" + "# wave=fließende Welle, chars=Regenbogen pro Buchstabe, line=eine Farbe\n" +
"scoreboard.rainbow.mode=wave\n" + "scoreboard.rainbow.mode=wave\n" +
"# Wellengeschwindigkeit: 1=sehr langsam, 10=normal, 50=schnell\n" + "# Wellengeschwindigkeit: 1=sehr langsam, 10=normal, 50=schnell\n" +
"scoreboard.rainbow.speed=10\n" + "scoreboard.rainbow.speed=10\n" +
"# Leer = voller HSB-Regenbogen\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.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.time_format=HH:mm\n" +
"scoreboard.date_format=dd.MM.yyyy\n" + "scoreboard.date_format=dd.MM.yyyy\n" +
"scoreboard.timezone=Europe/Berlin\n" + "scoreboard.timezone=Europe/Berlin\n" +
"scoreboard.money_format=#,##0.00\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" + "# 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" + "scoreboard.separator=&8&m--------------------\n" +
"# News-Ticker (erscheint als %news% Placeholder)\n" + "# News-Ticker (erscheint als %news% Placeholder)\n" +
"scoreboard.news.text=&eWillkommen auf Viper Network!\n" + "scoreboard.news.text=&eWillkommen auf Viper Network!\n" +
"scoreboard.news.prefix=&8[&6News&8] &r\n" + "scoreboard.news.prefix=&8[&6News&8] &r\n" +
"scoreboard.news.width=20\n" + "scoreboard.news.width=20\n" +
"scoreboard.news.speed=1\n\n" + "scoreboard.news.speed=1\n" +
"\n" +
"scoreboard.rotation_interval=4\n" + "scoreboard.rotation_interval=4\n" +
"# ===================================================\n" + "# ===================================================\n" +
"# ZEILEN - max 15 sichtbar\n" + "# ZEILEN - max 15 sichtbar\n" +
"# ===================================================\n" + "# ===================================================\n" +
"scoreboard.lines.1=%line%\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.3=&7%rank% &f%player%\n" +
"scoreboard.lines.4=\n" + "scoreboard.lines.4=\n" +
"scoreboard.lines.5=&7Spielzeit: &f%playtime%\n" + "scoreboard.lines.5=&7Spielzeit: &f%playtime%\n" +
"scoreboard.lines.5.2=&7Leben: &c%health%\n" + "scoreboard.lines.5.2=&7Leben: &c%health%\n" +
"scoreboard.lines.5.3=&7Hunger: &#8B4513%foodsym%\n" + "scoreboard.lines.5.3=&7Hunger: &#8B4513%foodsym%\n" +
"scoreboard.lines.6=\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.8=&a$%money%\n" +
"scoreboard.lines.9=\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=&f%server%\n" +
"scoreboard.lines.11.2=&7Ping: &f%ping%ms &8| &7Online: &f%online%\n" + "scoreboard.lines.11.2=&7Ping: &f%ping%ms &8| &7Online: &f%online%\n" +
"scoreboard.lines.12=\n" + "scoreboard.lines.12=\n" +
@@ -1321,13 +1604,13 @@ public class ScoreboardModule implements Module, Listener {
"# ADMIN-ZEILEN\n" + "# ADMIN-ZEILEN\n" +
"# ===================================================\n" + "# ===================================================\n" +
"scoreboard.admin_lines.1=%line%\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.3=&7%rank% &f%player%\n" +
"scoreboard.admin_lines.4=&7Gamemode: &f%gamemode%\n" + "scoreboard.admin_lines.4=&7Gamemode: &f%gamemode%\n" +
"scoreboard.admin_lines.5=&7Leben: &c%health%\n" + "scoreboard.admin_lines.5=&7Leben: &c%health%\n" +
"scoreboard.admin_lines.5.2=&7Hunger: &#8B4513%foodsym%\n" + "scoreboard.admin_lines.5.2=&7Hunger: &#8B4513%foodsym%\n" +
"scoreboard.admin_lines.6=\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=&f%server% &8| &7RAM: &e%ram%\n" +
"scoreboard.admin_lines.8.2=&7Proxy: &f%uptime%\n" + "scoreboard.admin_lines.8.2=&7Proxy: &f%uptime%\n" +
"scoreboard.admin_lines.9=\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.12=&7Zeit: &f%time%\n" +
"scoreboard.supporter_lines.13=\n" + "scoreboard.supporter_lines.13=\n" +
"scoreboard.supporter_lines.14=%line%\n" + "scoreboard.supporter_lines.14=%line%\n" +
"scoreboard.supporter_lines.15=&7%compass%\n"; "scoreboard.supporter_lines.15=&7%compass%\n" +
try (OutputStream out = new FileOutputStream(f)) { "";
try (OutputStream out = new FileOutputStream(f)) {
out.write(content.getBytes(StandardCharsets.UTF_8)); out.write(content.getBytes(StandardCharsets.UTF_8));
} catch (Exception e) { } catch (Exception e) {
plugin.getLogger().warning("[ScoreboardModule] Config: " + e.getMessage()); plugin.getLogger().warning("[ScoreboardModule] Config: " + e.getMessage());
@@ -1554,6 +1838,35 @@ public class ScoreboardModule implements Module, Listener {
* *
* Aliase: /sb, /togglesb * Aliase: /sb, /togglesb
*/ */
private static final List<String> 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<String> 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 { private class ScoreboardToggleCommand extends Command {
ScoreboardToggleCommand() { ScoreboardToggleCommand() {
@@ -1653,4 +1966,5 @@ public class ScoreboardModule implements Module, Listener {
} }
} }
} }

View File

@@ -23,12 +23,14 @@ import java.lang.reflect.Method;
import java.nio.charset.StandardCharsets; import java.nio.charset.StandardCharsets;
import java.text.SimpleDateFormat; import java.text.SimpleDateFormat;
import java.util.*; import java.util.*;
import java.awt.Color;
import java.util.concurrent.ConcurrentHashMap; import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.TimeUnit; import java.util.concurrent.TimeUnit;
public class TablistModule implements Module, Listener { 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 // Leerer Skin (grauer Kopf) für Platzhalter-Slots
private static final net.md_5.bungee.protocol.data.Property[] EMPTY_SKIN = { 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) // Skin-Cache (pro Spieler)
private final ConcurrentHashMap<UUID, net.md_5.bungee.protocol.data.Property[]> skinCache = new ConcurrentHashMap<>(); private final ConcurrentHashMap<UUID, net.md_5.bungee.protocol.data.Property[]> skinCache = new ConcurrentHashMap<>();
// ── NEU: Server-Symbol-Map (serverName lowercase → colored symbol string) ─
private final Map<String, String> 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 // Config
private boolean enabled = true; private boolean enabled = true;
private int updateInterval = 5; 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 compactHeader1 = "&6&lViper Network &8• &7%online% Spieler online";
private String compactHeader2 = ""; private String compactHeader2 = "";
private String compactHeader3 = ""; private String compactHeader3 = "";
private boolean compactHeader1Spacer = false;
private boolean compactHeader2Spacer = false; private boolean compactHeader2Spacer = false;
private boolean compactHeader3Spacer = false; private boolean compactHeader3Spacer = false;
private String compactFooter1 = ""; private String compactFooter1 = "";
private String compactFooter2 = "&7Zeit: &f%time% &8| &7Spieler: &f%online% &8| &7Ping: &f%ping%ms"; 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 compactFooter3 = "&7Kontostand: &a$%balance% &8| &7Server: &f%server% &8| &7Welt: &f%world%";
private String compactFooter4 = ""; private String compactFooter4 = "";
private String compactFooter5 = "";
private String compactFooter6 = "";
private boolean compactFooter1Spacer = false; private boolean compactFooter1Spacer = false;
private boolean compactFooter2Spacer = false;
private boolean compactFooter3Spacer = false;
private boolean compactFooter4Spacer = false; private boolean compactFooter4Spacer = false;
private boolean compactFooter5Spacer = false;
private boolean compactFooter6Spacer = false;
private String colorSrvHeader = "&6&l"; private String colorSrvHeader = "&6&l";
private boolean showFooterServerList = true; private boolean showFooterServerList = true; // tablist.compact.footer.serverlist=true/false
private String columnHeaderMode = "none";
private final Map<String, String> serverSymbols = new LinkedHashMap<>();
private String timeFormat = "HH:mm:ss / h:mm a"; private String timeFormat = "HH:mm:ss / h:mm a";
private String timeZone = "Europe/Berlin"; private String timeZone = "Europe/Berlin";
private SimpleDateFormat sdf; private SimpleDateFormat sdf;
@@ -129,7 +148,8 @@ public class TablistModule implements Module, Listener {
plugin.getLogger().info("[TablistModule] Tablist-Spalten: " + getServerOrder()); plugin.getLogger().info("[TablistModule] Tablist-Spalten: " + getServerOrder());
recalculateGrid(); recalculateGrid();
}, 3L, TimeUnit.SECONDS); }, 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 @Override
@@ -150,13 +170,12 @@ public class TablistModule implements Module, Listener {
} catch (Exception ignored) {} } catch (Exception ignored) {}
} }
} catch (Exception ignored) {} } catch (Exception ignored) {}
if (configuredTabSize > 0) tabSize = configuredTabSize; // manuell gesetzt in tablist.properties if (configuredTabSize > 0) tabSize = configuredTabSize;
tabSizeMax = tabSize; tabSizeMax = tabSize;
rows = ROWS; // immer 20 Minecraft-Client-Pflicht rows = ROWS;
boolean hasInfo = !"compact".equalsIgnoreCase(layoutMode); boolean hasInfo = !"compact".equalsIgnoreCase(layoutMode);
int serverCount = getServerOrder().size(); int serverCount = getServerOrder().size();
int needed = (hasInfo ? 1 : 0) + Math.max(1, serverCount); 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)); columns = Math.max(hasInfo ? 2 : 1, Math.min(needed, tabSize / ROWS));
total = ROWS * columns; total = ROWS * columns;
if (needed > tabSize / ROWS) { 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); if (skin != null && skin.length > 0) skinCache.put(p.getUniqueId(), skin);
ProxyServer.getInstance().getScheduler().schedule(plugin, () -> { ProxyServer.getInstance().getScheduler().schedule(plugin, () -> {
updateTablist(p); 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); ProxyServer.getInstance().getScheduler().schedule(plugin, this::updateAll, 2L, TimeUnit.SECONDS);
}, 2L, TimeUnit.SECONDS); }, 2L, TimeUnit.SECONDS);
} }
@@ -192,25 +210,18 @@ public class TablistModule implements Module, Listener {
if (!enabled) return; if (!enabled) return;
ProxiedPlayer switched = e.getPlayer(); 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); net.md_5.bungee.protocol.data.Property[] skin = fetchSkin(switched);
if (skin != null && skin.length > 0) skinCache.put(switched.getUniqueId(), skin); 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, () -> { ProxyServer.getInstance().getScheduler().schedule(plugin, () -> {
// Skin nochmals versuchen (jetzt auf neuem Server)
net.md_5.bungee.protocol.data.Property[] freshSkin = fetchSkin(switched); net.md_5.bungee.protocol.data.Property[] freshSkin = fetchSkin(switched);
if (freshSkin != null && freshSkin.length > 0) skinCache.put(switched.getUniqueId(), freshSkin); if (freshSkin != null && freshSkin.length > 0) skinCache.put(switched.getUniqueId(), freshSkin);
// Alle Slots bei allen Viewern entfernen
for (ProxiedPlayer viewer : ProxyServer.getInstance().getPlayers()) { for (ProxiedPlayer viewer : ProxyServer.getInstance().getPlayers()) {
try { removeFakeSlots(viewer); } catch (Exception ignored) {} try { removeFakeSlots(viewer); } catch (Exception ignored) {}
} }
// Sofort neu aufbauen (kein weiterer Delay nötig da removeFakeSlots synchron ist)
updateAll(); updateAll();
// Nochmal nach 2s als Sicherheit
ProxyServer.getInstance().getScheduler().schedule(plugin, () -> { ProxyServer.getInstance().getScheduler().schedule(plugin, () -> {
net.md_5.bungee.protocol.data.Property[] s2 = fetchSkin(switched); net.md_5.bungee.protocol.data.Property[] s2 = fetchSkin(switched);
if (s2 != null && s2.length > 0) skinCache.put(switched.getUniqueId(), s2); 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) { public void onDisconnect(PlayerDisconnectEvent e) {
if (!enabled) return; if (!enabled) return;
skinCache.remove(e.getPlayer().getUniqueId()); 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, () -> { ProxyServer.getInstance().getScheduler().schedule(plugin, () -> {
for (ProxiedPlayer viewer : ProxyServer.getInstance().getPlayers()) { for (ProxiedPlayer viewer : ProxyServer.getInstance().getPlayers()) {
try { removeFakeSlots(viewer); } catch (Exception ignored) {} try { removeFakeSlots(viewer); } catch (Exception ignored) {}
@@ -242,7 +251,6 @@ public class TablistModule implements Module, Listener {
private void updateAll() { private void updateAll() {
recalculateGrid(); recalculateGrid();
// Fehlende Skins nachladen
for (ProxiedPlayer p : ProxyServer.getInstance().getPlayers()) { for (ProxiedPlayer p : ProxyServer.getInstance().getPlayers()) {
if (!skinCache.containsKey(p.getUniqueId())) { if (!skinCache.containsKey(p.getUniqueId())) {
net.md_5.bungee.protocol.data.Property[] skin = fetchSkin(p); net.md_5.bungee.protocol.data.Property[] skin = fetchSkin(p);
@@ -254,10 +262,15 @@ public class TablistModule implements Module, Listener {
private void recalculateGrid() { private void recalculateGrid() {
boolean hasInfo = !"compact".equalsIgnoreCase(layoutMode); boolean hasInfo = !"compact".equalsIgnoreCase(layoutMode);
int serverCount = getServerOrder().size(); int newColumns;
int needed = (hasInfo ? 1 : 0) + Math.max(1, serverCount); if ("custom".equalsIgnoreCase(playerDisplayMode)) {
// Spalten = benötigte Spalten, aber max was tab-size erlaubt (tab-size/20) // Custom-Modus: immer 3 Spalten (konfigurierbar via tab_size)
int newColumns = Math.max(hasInfo ? 2 : 1, Math.min(needed, tabSizeMax / ROWS)); 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; int newTotal = ROWS * newColumns;
if (newColumns == columns && newTotal == total) return; if (newColumns == columns && newTotal == total) return;
for (ProxiedPlayer p : ProxyServer.getInstance().getPlayers()) { for (ProxiedPlayer p : ProxyServer.getInstance().getPlayers()) {
@@ -267,7 +280,7 @@ public class TablistModule implements Module, Listener {
columns = newColumns; columns = newColumns;
total = newTotal; total = newTotal;
initUuids(); 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) { private void updateTablist(ProxiedPlayer viewer) {
@@ -288,7 +301,12 @@ public class TablistModule implements Module, Listener {
header = c(headerLine1) + "\n" + c(headerLine2) + "\n" + c(headerLine3); header = c(headerLine1) + "\n" + c(headerLine2) + "\n" + c(headerLine3);
footer = c(footerLine1) + "\n" + c(footerLine2) + "\n" + c(footerLine3); 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); hideRealPlayers(viewer);
sendSlots(viewer, buildItems(viewer)); sendSlots(viewer, buildItems(viewer));
} catch (Exception ex) { } 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) { private String buildCompactHeader(ProxiedPlayer viewer, String srv, String world, String rank, String time, String balance, int online) {
StringBuilder sb = new StringBuilder(); 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, compactHeader2, compactHeader2Spacer, viewer, srv, world, rank, time, balance, online);
appendLine(sb, compactHeader3, compactHeader3Spacer, viewer, srv, world, rank, time, balance, online); appendLine(sb, compactHeader3, compactHeader3Spacer, viewer, srv, world, rank, time, balance, online);
return sb.toString(); return sb.toString();
@@ -321,9 +339,11 @@ public class TablistModule implements Module, Listener {
if (sb.length() > 0) sb.append("\n"); if (sb.length() > 0) sb.append("\n");
sb.append(c(sLine.toString())); sb.append(c(sLine.toString()));
} }
appendLine(sb, compactFooter2, false, viewer, srv, world, rank, time, balance, online); appendLine(sb, compactFooter2, compactFooter2Spacer, viewer, srv, world, rank, time, balance, online);
appendLine(sb, compactFooter3, false, 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, 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(); return sb.toString();
} }
@@ -331,7 +351,7 @@ public class TablistModule implements Module, Listener {
boolean empty = line == null || line.trim().isEmpty(); boolean empty = line == null || line.trim().isEmpty();
if (empty && !spacer) return; if (empty && !spacer) return;
if (sb.length() > 0) sb.append("\n"); 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 ────────────────────────────────────────────────────────────────── // ── 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; } for (int i = 0; i < total; i++) { texts[i] = " "; skins[i] = EMPTY_SKIN; pings[i] = 0; }
boolean compact = "compact".equalsIgnoreCase(layoutMode); boolean compact = "compact".equalsIgnoreCase(layoutMode);
// Ob der Spalten-Header einen Slot belegt (= "full") oder nicht (= "none"/"small")
boolean useSlotHeader = "full".equalsIgnoreCase(columnHeaderMode); boolean useSlotHeader = "full".equalsIgnoreCase(columnHeaderMode);
// Info-Spalte (nur classic) // Info-Spalte (nur classic)
@@ -372,26 +393,62 @@ public class TablistModule implements Module, Listener {
} }
} }
// Server-Spalten if ("custom".equalsIgnoreCase(playerDisplayMode)) {
List<String> servers = getServerOrder(); // ── Custom-Modus: alle Spieler zusammen, nach Rang sortiert ──────────
int startCol = compact ? 0 : 1; // Minecraft Tab-Grid ist spaltenweise aufgebaut (Spalte 1 = Slots 0-19, Spalte 2 = Slots 20-39)
for (int col = startCol; col < columns && (col - startCol) < servers.size(); col++) { // "Links nach rechts" = Zeile 0 über alle Spalten, dann Zeile 1 usw.
int base = col * rows; // Spieler 0 → Spalte 0 Zeile 0, Spieler 1 → Spalte 1 Zeile 0, Spieler 2 → Spalte 2 Zeile 0
int row = 0; // Spieler 3 → Spalte 0 Zeile 1, Spieler 4 → Spalte 1 Zeile 1 usw.
String sName = servers.get(col - startCol); List<ProxiedPlayer> allPlayers = new ArrayList<>(ProxyServer.getInstance().getPlayers());
if (useSlotHeader) row = set(texts, base, row, c(colorSrvHeader + capitalize(sName))); allPlayers = sortPlayersByRank(allPlayers);
ServerInfo si = ProxyServer.getInstance().getServerInfo(sName); int startCol = compact ? 0 : 1;
if (si != null) { int usedCols = columns - startCol;
for (ProxiedPlayer p : sortPlayersByRank(new ArrayList<>(si.getPlayers()))) { int maxSlots = usedCols * rows;
if (row >= rows) break; int playerIdx = 0;
String prefix = getLuckPermsPrefix(p); // Zeile für Zeile iterieren, innerhalb jeder Zeile alle Spalten
String symbol = getServerSymbol(p); 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); 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()); net.md_5.bungee.protocol.data.Property[] skin = skinCache.get(p.getUniqueId());
skins[base + row] = (skin != null && skin.length > 0) ? skin : EMPTY_SKIN; skins[base + row] = (skin != null && skin.length > 0) ? skin : EMPTY_SKIN;
pings[base + row] = p.getPing() < 0 ? 1 : p.getPing(); pings[base + row] = p.getPing() < 0 ? 1 : p.getPing();
row++; }
}
} else {
// ── Server-Modus: pro Spalte ein Server (default) ──────────────────
List<String> 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.setGamemode(0);
item.setPing(pings[i]); item.setPing(pings[i]);
item.setListed(true); 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; items[i] = item;
} }
return items; return items;
@@ -418,20 +477,33 @@ public class TablistModule implements Module, Listener {
if (sendPacketQueuedMethod == null) return; if (sendPacketQueuedMethod == null) return;
try { try {
Collection<ProxiedPlayer> online = ProxyServer.getInstance().getPlayers(); Collection<ProxiedPlayer> 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<UUID> toHide = new ArrayList<>(); List<UUID> toHide = new ArrayList<>();
for (ProxiedPlayer p : online) toHide.add(p.getUniqueId()); 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()) { for (String srvName : ProxyServer.getInstance().getServers().keySet()) {
try { try {
toHide.add(UUID.nameUUIDFromBytes(("OfflinePlayer:" + srvName).getBytes(StandardCharsets.UTF_8))); UUID srvUuid = UUID.nameUUIDFromBytes(("OfflinePlayer:" + srvName).getBytes(StandardCharsets.UTF_8));
toHide.add(UUID.nameUUIDFromBytes(srvName.getBytes(StandardCharsets.UTF_8))); toHide.add(srvUuid);
UUID srvUuid2 = UUID.nameUUIDFromBytes(srvName.getBytes(StandardCharsets.UTF_8));
toHide.add(srvUuid2);
} catch (Exception ignored) {} } catch (Exception ignored) {}
} }
if (toHide.isEmpty()) return; if (toHide.isEmpty()) return;
PlayerListItemUpdate pkt = new PlayerListItemUpdate(); PlayerListItemUpdate pkt = new PlayerListItemUpdate();
pkt.setActions(EnumSet.of(PlayerListItemUpdate.Action.UPDATE_LISTED)); pkt.setActions(EnumSet.of(PlayerListItemUpdate.Action.UPDATE_LISTED));
Item[] items = new Item[toHide.size()]; Item[] items = new Item[toHide.size()];
int idx = 0; 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); pkt.setItems(items);
sendPacketQueuedMethod.invoke(viewer, pkt); sendPacketQueuedMethod.invoke(viewer, pkt);
} catch (Exception e) { plugin.getLogger().warning("[TablistModule] hideRealPlayers: " + e.getMessage()); } } catch (Exception e) { plugin.getLogger().warning("[TablistModule] hideRealPlayers: " + e.getMessage()); }
@@ -440,7 +512,6 @@ public class TablistModule implements Module, Listener {
@SuppressWarnings("unchecked") @SuppressWarnings("unchecked")
private void sendSlots(ProxiedPlayer viewer, Item[] items) { private void sendSlots(ProxiedPlayer viewer, Item[] items) {
if (sendPacketQueuedMethod == null) return; if (sendPacketQueuedMethod == null) return;
// Immer vollständiges ADD_PLAYER einfach und zuverlässig
PlayerListItemUpdate pkt = new PlayerListItemUpdate(); PlayerListItemUpdate pkt = new PlayerListItemUpdate();
pkt.setActions(EnumSet.of( pkt.setActions(EnumSet.of(
PlayerListItemUpdate.Action.ADD_PLAYER, PlayerListItemUpdate.Action.ADD_PLAYER,
@@ -473,11 +544,16 @@ public class TablistModule implements Module, Listener {
// ── Helpers ──────────────────────────────────────────────────────────────── // ── 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) { private String getServerSymbol(ProxiedPlayer player) {
if (serverSymbols.isEmpty() || player.getServer() == null) return ""; 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 ""; 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) { private net.md_5.bungee.protocol.data.Property[] fetchSkin(ProxiedPlayer player) {
@@ -495,7 +571,6 @@ public class TablistModule implements Module, Listener {
List<String> list; List<String> list;
if (!serverOrder.isEmpty()) { if (!serverOrder.isEmpty()) {
list = new ArrayList<>(serverOrder); list = new ArrayList<>(serverOrder);
// Versteckte Server auch aus manueller Liste entfernen
list.removeIf(s -> hiddenServers.contains(s.toLowerCase())); list.removeIf(s -> hiddenServers.contains(s.toLowerCase()));
} else { } else {
list = new ArrayList<>(); list = new ArrayList<>();
@@ -565,6 +640,7 @@ public class TablistModule implements Module, Listener {
Object cache = usr.getClass().getMethod("getCachedData").invoke(usr); Object cache = usr.getClass().getMethod("getCachedData").invoke(usr);
Object meta = cache.getClass().getMethod("getMetaData", qo).invoke(cache, opts); Object meta = cache.getClass().getMethod("getMetaData", qo).invoke(cache, opts);
Object pfx = meta.getClass().getMethod("getPrefix").invoke(meta); Object pfx = meta.getClass().getMethod("getPrefix").invoke(meta);
// ── HEX-Farben auch im Prefix auflösen ───────────────────────
if (pfx != null) return c(pfx.toString()); if (pfx != null) return c(pfx.toString());
} }
} catch (Exception ignored) {} } 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) { private String replacePlaceholders(String text, ProxiedPlayer viewer, String srv, String world, String rank, String time, String balance, int online) {
if (text == null) return ""; if (text == null) return "";
// PAPI zuerst, native Tokens danach (überschreiben PAPI-Werte falls gleicher Name)
String result = resolvePapiPlaceholders(text, viewer.getUniqueId()); String result = resolvePapiPlaceholders(text, viewer.getUniqueId());
result = result.replace("%player%", viewer.getName()).replace("%rank%", rank) result = result.replace("%player%", viewer.getName()).replace("%rank%", rank)
.replace("%server%", srv).replace("%world%", world).replace("%time%", time) .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) { private static String resolvePapiPlaceholders(String text, UUID uuid) {
if (text == null || !text.contains("%")) return text; if (text == null || !text.contains("%")) return text;
java.util.Map<String, String> papiMap = net.viper.status.StatusAPI.playerPapi.get(uuid); Map<String, String> papiMap = net.viper.status.StatusAPI.playerPapi.get(uuid);
if (papiMap == null || papiMap.isEmpty()) return text; if (papiMap == null || papiMap.isEmpty()) return text;
StringBuilder sb = new StringBuilder(); StringBuilder sb = new StringBuilder();
int i = 0; 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; 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
// <color:#RRGGBB> → MiniMessage color-Tag
// <gradient:#C1:#C2:Text> → Farbverlauf (beliebig viele Farb-Stopps)
// <shadow:#C:Text> → Text in Schattenfarbe
// <b> <i> <u> <st> <obf> → Formatierungen
// &l &o &n &m &k &r → Standard-Formatierungen
// ══════════════════════════════════════════════════════════════════════════
private static String c(String s) { private static String c(String s) {
if (s == null) return ""; if (s == null) return " ";
s = replaceHexColors(s); s = parseMiniMessage(s); // MiniMessage-Tags (<gradient:>, <shadow:>, <#>, <color:>, <b> usw.)
return ChatColor.translateAlternateColorCodes('&', s); s = parseHexAmpersand(s); // &#RRGGBB und {#RRGGBB}
return net.md_5.bungee.api.ChatColor.translateAlternateColorCodes('&', s);
} }
private static String replaceHexColors(String text) { private static String stripColors(String s) {
if (text == null) return null; return s == null ? "" : net.md_5.bungee.api.ChatColor.stripColor(c(s));
if (!text.contains("&#") && !text.contains("{#") && !text.contains("<#")) return text; }
// ── 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: <color:#>, <#>, <b>, <i>, <u>, <st>, <obf>, </...>
text = parseSimpleTags(text);
return text;
}
// ── <gradient:#C1:#C2:...:TEXT> ──────────────────────────────────────────
private static String parseGradientTags(String text) {
if (!text.contains("<gradient:")) return text;
StringBuilder result = new StringBuilder();
int i = 0;
while (i < text.length()) {
int start = text.indexOf("<gradient:", i);
if (start < 0) { result.append(text, i, text.length()); break; }
result.append(text, i, start);
// Schließendes > 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<String> 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<String> 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();
}
// ── <shadow:#RRGGBB:TEXT> ─────────────────────────────────────────────────
private static String parseShadowTags(String text) {
if (text == null || !text.contains("<shadow:")) return text == null ? "" : text;
StringBuilder result = new StringBuilder();
int i = 0;
while (i < text.length()) {
int start = text.indexOf("<shadow:", i);
if (start < 0) { result.append(text, i, text.length()); break; }
result.append(text, i, start);
int end = findClosingAngle(text, start + 1);
if (end < 0) { result.append(text, i, text.length()); break; }
String inner = text.substring(start + 1, end); // "shadow:#RRGGBB:TEXT"
// Format: shadow:COLOR:TEXT
int firstColon = inner.indexOf(':');
int secondColon = firstColon >= 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("<b>", "&l").replace("</b>", "&r");
text = text.replace("<i>", "&o").replace("</i>", "&r");
text = text.replace("<u>", "&n").replace("</u>", "&r");
text = text.replace("<st>", "&m").replace("</st>", "&r");
text = text.replace("<obf>", "&k").replace("</obf>", "&r");
text = text.replace("<reset>", "&r").replace("</reset>", "");
// Closing-Tags entfernen (werden nach Verarbeitung nicht mehr benötigt)
text = text.replaceAll("</gradient>", "");
text = text.replaceAll("</shadow>", "");
text = text.replaceAll("</color>", "");
// <color:#RRGGBB> 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; }
// <color:#RRGGBB>
if (text.startsWith("<color:#", i)) {
int end = text.indexOf('>', 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(); StringBuilder sb = new StringBuilder();
int i = 0; int i = 0;
while (i < text.length()) { 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); String hex = text.substring(i+2, i+8);
if (hex.matches("[0-9a-fA-F]{6}")) { if (hex.matches("[0-9a-fA-F]{6}")) {
sb.append('\u00A7').append('x'); appendHexSection(sb, hex); i += 8; continue;
for (char ch : hex.toCharArray()) sb.append('\u00A7').append(ch);
i += 8; continue;
} }
} }
// {#RRGGBB}
if (i + 8 < text.length() && text.charAt(i) == '{' && text.charAt(i+1) == '#') { if (i + 8 < text.length() && text.charAt(i) == '{' && text.charAt(i+1) == '#') {
int end = text.indexOf('}', i+2); int end = text.indexOf('}', i+2);
if (end == i+8) { if (end == i + 8) {
String hex = text.substring(i+2, i+8); String hex = text.substring(i+2, i+8);
if (hex.matches("[0-9a-fA-F]{6}")) { if (hex.matches("[0-9a-fA-F]{6}")) {
sb.append('\u00A7').append('x'); appendHexSection(sb, hex); i += 9; continue;
for (char ch : hex.toCharArray()) sb.append('\u00A7').append(ch);
i += 9; continue;
} }
} }
} }
// Format 3: <#RRGGBB> sb.append(text.charAt(i++));
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++;
} }
return sb.toString(); 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 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 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<n;i++) sb.append(ch); return sb.toString(); } private static String rep(char ch, int n) { StringBuilder sb=new StringBuilder(n); for(int i=0;i<n;i++) sb.append(ch); return sb.toString(); }
@@ -689,6 +1025,12 @@ public class TablistModule implements Module, Listener {
"tablist.server_order=\n" + "tablist.server_order=\n" +
"tablist.hidden_servers=\n" + "tablist.hidden_servers=\n" +
"tablist.rank_order=owner,mod,primo,vip,scout,bewohner\n\n" + "tablist.rank_order=owner,mod,primo,vip,scout,bewohner\n\n" +
"# column_header: full = großer Spalten-Header (alte Markierung)\n" +
"# none = kein Header, Zeile 0 ist für Spieler frei (MuckiDEE-Wunsch)\n" +
"# small = wie none, aber Server-Namen erscheinen im Tab-Footer\n" +
"tablist.column_header=none\n" +
"# player_display: server = Server-basiert (default) | custom = alle zusammen nach Rang sortiert\n" +
"tablist.player_display=server\n\n" +
"tablist.header.line1=&8&m" + sep + "\n" + "tablist.header.line1=&8&m" + sep + "\n" +
"tablist.header.line2= &6&lViper Network\n" + "tablist.header.line2= &6&lViper Network\n" +
"tablist.header.line3=&8&m" + sep + "\n\n" + "tablist.header.line3=&8&m" + sep + "\n\n" +
@@ -710,10 +1052,12 @@ public class TablistModule implements Module, Listener {
"tablist.compact.footer.line3=&7Kontostand: &a$%balance% &8| &7Server: &f%server% &8| &7Welt: &f%world%\n" + "tablist.compact.footer.line3=&7Kontostand: &a$%balance% &8| &7Server: &f%server% &8| &7Welt: &f%world%\n" +
"tablist.compact.footer.line3.spacer=false\n" + "tablist.compact.footer.line3.spacer=false\n" +
"tablist.compact.footer.line4=\n" + "tablist.compact.footer.line4=\n" +
"tablist.compact.footer.line4.spacer=false\n\n" + "tablist.compact.footer.line4.spacer=false\n" +
"tablist.compact.footer.line5=\n" +
"tablist.compact.footer.line5.spacer=false\n" +
"tablist.compact.footer.line6=\n" +
"tablist.compact.footer.line6.spacer=false\n\n" +
"tablist.color.server_header=&6&l\n" + "tablist.color.server_header=&6&l\n" +
"# column_header: full=großer Header | none=kein Header (Zeile 0 frei) | small=nur im Footer\n" +
"tablist.column_header=none\n" +
"# Server-Liste im Footer anzeigen (true/false)\n" + "# Server-Liste im Footer anzeigen (true/false)\n" +
"tablist.compact.footer.serverlist=true\n" + "tablist.compact.footer.serverlist=true\n" +
"tablist.time_format=HH:mm:ss / h:mm a\n" + "tablist.time_format=HH:mm:ss / h:mm a\n" +
@@ -743,10 +1087,16 @@ public class TablistModule implements Module, Listener {
"tablist.info.teamspeak.label=&b&lTeamspeak:\n" + "tablist.info.teamspeak.label=&b&lTeamspeak:\n" +
"tablist.info.teamspeak.type=teamspeak\n" + "tablist.info.teamspeak.type=teamspeak\n" +
"tablist.info.teamspeak.value=&fts.viper-network.de\n" + "tablist.info.teamspeak.value=&fts.viper-network.de\n" +
"\n# Server-Symbole hinter dem Spielernamen\n" + "\n# ── Server-Symbole ───────────────────────────────────────────────────\n" +
"# Format: tablist.symbol.<servername>=&FarbCode Symbol\n" + "# Format: tablist.symbol.<servername>=&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.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)); } try (OutputStream out = new FileOutputStream(f)) { out.write(content.getBytes(StandardCharsets.UTF_8)); }
catch (Exception e) { plugin.getLogger().warning("[TablistModule] Config: " + e.getMessage()); } 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")); enabled = Boolean.parseBoolean(get.apply("tablist.enabled", "true"));
updateInterval = parseInt(get.apply("tablist.update_interval", "5"), 5); updateInterval = parseInt(get.apply("tablist.update_interval", "5"), 5);
layoutMode = get.apply("tablist.layout", "compact").trim().toLowerCase(); 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); headerLine1 = get.apply("tablist.header.line1", headerLine1);
headerLine2 = get.apply("tablist.header.line2", headerLine2); headerLine2 = get.apply("tablist.header.line2", headerLine2);
headerLine3 = get.apply("tablist.header.line3", headerLine3); 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); compactHeader1 = get.apply("tablist.compact.header.line1", compactHeader1);
compactHeader2 = get.apply("tablist.compact.header.line2", compactHeader2); compactHeader2 = get.apply("tablist.compact.header.line2", compactHeader2);
compactHeader3 = get.apply("tablist.compact.header.line3", compactHeader3); 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")); compactHeader2Spacer = Boolean.parseBoolean(get.apply("tablist.compact.header.line2.spacer", "false"));
compactHeader3Spacer = Boolean.parseBoolean(get.apply("tablist.compact.header.line3.spacer", "false")); compactHeader3Spacer = Boolean.parseBoolean(get.apply("tablist.compact.header.line3.spacer", "false"));
compactFooter1 = get.apply("tablist.compact.footer.line1", compactFooter1); compactFooter1 = get.apply("tablist.compact.footer.line1", compactFooter1);
compactFooter2 = get.apply("tablist.compact.footer.line2", compactFooter2); compactFooter2 = get.apply("tablist.compact.footer.line2", compactFooter2);
compactFooter3 = get.apply("tablist.compact.footer.line3", compactFooter3); compactFooter3 = get.apply("tablist.compact.footer.line3", compactFooter3);
compactFooter4 = get.apply("tablist.compact.footer.line4", compactFooter4); 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")); 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")); 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); colorSrvHeader = get.apply("tablist.color.server_header", colorSrvHeader);
showFooterServerList = Boolean.parseBoolean(get.apply("tablist.compact.footer.serverlist", "true")); 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); timeFormat = get.apply("tablist.time_format", timeFormat);
timeZone = get.apply("tablist.timezone", timeZone); timeZone = get.apply("tablist.timezone", timeZone);
try { sdf = new SimpleDateFormat(timeFormat); sdf.setTimeZone(java.util.TimeZone.getTimeZone(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)); infoEntries.add(new InfoEntry("&b&lTeamspeak:", "teamspeak", "&fts.viper-network.de", true));
} }
// Server-Symbole // ── Server-Symbole aus tablist.properties ─────────────────────────────
// Format: tablist.symbol.<servername>=&FarbCode Symbol
// Beispiel: tablist.symbol.lobby=&f🏠
// tablist.symbol.sv1=&6⛏
serverSymbols.clear(); serverSymbols.clear();
for (Map.Entry<String, String> entry : map.entrySet()) { for (Map.Entry<String, String> entry : map.entrySet()) {
String key = entry.getKey(); String key = entry.getKey();
if (key.startsWith("tablist.symbol.")) { if (key.startsWith("tablist.symbol.")) {
String srvName = key.substring("tablist.symbol.".length()).trim().toLowerCase(); String srvName = key.substring("tablist.symbol.".length()).trim().toLowerCase();
String symbol = entry.getValue().trim(); String symbol = entry.getValue().trim();
if (!srvName.isEmpty() && !symbol.isEmpty()) if (!srvName.isEmpty() && !symbol.isEmpty()) {
serverSymbols.put(srvName, symbol); serverSymbols.put(srvName, symbol);
plugin.getLogger().info("[TablistModule] Symbol: " + srvName + "" + symbol);
}
} }
} }
} }
} }

View File

@@ -1,6 +1,6 @@
name: StatusAPI name: StatusAPI
main: net.viper.status.StatusAPI main: net.viper.status.StatusAPI
version: 4.1.1 version: 4.1.2
author: M_Viper author: M_Viper
description: StatusAPI für BungeeCord inkl. Update-Checker, Modul-System und ChatModule description: StatusAPI für BungeeCord inkl. Update-Checker, Modul-System und ChatModule
# Mindestanforderung: Minecraft 1.20 / BungeeCord mit PlayerChatEvent-Unterstützung # Mindestanforderung: Minecraft 1.20 / BungeeCord mit PlayerChatEvent-Unterstützung