diff --git a/StatusAPI/lib/BungeeCord.jar b/StatusAPI/lib/BungeeCord.jar index c146a5a..b44894a 100644 Binary files a/StatusAPI/lib/BungeeCord.jar and b/StatusAPI/lib/BungeeCord.jar differ diff --git a/StatusAPI/pom.xml b/StatusAPI/pom.xml index 785f063..8a22041 100644 --- a/StatusAPI/pom.xml +++ b/StatusAPI/pom.xml @@ -7,7 +7,7 @@ net.viper.bungee StatusAPI - 4.1.0 + 4.1.1 jar StatusAPI diff --git a/StatusAPI/src/main/java/net/viper/status/StatusAPI.java b/StatusAPI/src/main/java/net/viper/status/StatusAPI.java index 9622e01..0a9782a 100644 --- a/StatusAPI/src/main/java/net/viper/status/StatusAPI.java +++ b/StatusAPI/src/main/java/net/viper/status/StatusAPI.java @@ -54,6 +54,12 @@ public class StatusAPI extends Plugin implements Runnable { // Kontostand pro Spieler (UUID -> Balance), wird von StatusAPIBridge gepusht public static final ConcurrentHashMap playerBalances = new ConcurrentHashMap<>(); + // PlaceholderAPI-Werte pro Spieler (UUID -> (placeholder -> aufgelöster Wert)) + public static final ConcurrentHashMap> playerPapi = new ConcurrentHashMap<>(); + + /** Alle %token%-Tokens aus den Config-Dateien – als JSON-Array für GET /papi/tokens */ + public static volatile String papiTokensJson = "[]"; + // Debug-Modus (aus verify.properties) public static boolean DEBUG = false; @@ -124,6 +130,13 @@ public class StatusAPI extends Plugin implements Runnable { moduleManager.enableAll(this); + // PAPI-Tokens sofort scannen + nochmal nach 5s als Fallback (falls Configs erst beim Enable erstellt) + scanAndPublishPapiTokens(); + ProxyServer.getInstance().getScheduler().schedule(this, this::scanAndPublishPapiTokens, 5, TimeUnit.SECONDS); + + // /statusapi reload Befehl registrieren + ProxyServer.getInstance().getPluginManager().registerCommand(this, new StatusAPICommand(this)); + // FIX: ScoreboardModule mit NetworkInfoModule verbinden (TPS-Fallback) try { net.viper.status.modules.scoreboard.ScoreboardModule sbMod = @@ -836,6 +849,61 @@ public class StatusAPI extends Plugin implements Runnable { return; } + // GET /papi/tokens – liefert alle erkannten %token%-Placeholder als JSON-Array + if ("GET".equalsIgnoreCase(method) && "/papi/tokens".equalsIgnoreCase(pathOnly)) { + sendHttpResponse(out, papiTokensJson, 200); + return; + } + + // POST /player/papi – empfängt von StatusAPIBridge aufgelöste PAPI-Werte + if ("POST".equalsIgnoreCase(method) && "/player/papi".equalsIgnoreCase(pathOnly)) { + String body = readBody(in, headers); + String uuidStr = extractJsonString(body, "uuid"); + if (uuidStr != null && !uuidStr.isEmpty()) { + try { + UUID papiUuid = UUID.fromString(uuidStr.trim()); + Map map = playerPapi.computeIfAbsent(papiUuid, k -> new ConcurrentHashMap<>()); + // "placeholders"-Objekt manuell parsen + int start = body.indexOf("\"placeholders\""); + if (start >= 0) { + int brace = body.indexOf('{', start + 14); + if (brace >= 0) { + int i = brace + 1; + while (i < body.length()) { + while (i < body.length() && Character.isWhitespace(body.charAt(i))) i++; + if (i >= body.length() || body.charAt(i) == '}') break; + if (body.charAt(i) != '"') { i++; continue; } + i++; + StringBuilder key = new StringBuilder(); + while (i < body.length() && body.charAt(i) != '"') { + char ch = body.charAt(i++); + if (ch == '\\' && i < body.length()) i++; else key.append(ch); + } + i++; + while (i < body.length() && (body.charAt(i) == ':' || Character.isWhitespace(body.charAt(i)))) i++; + if (i < body.length() && body.charAt(i) == '"') { + i++; + StringBuilder val = new StringBuilder(); + boolean esc = false; + while (i < body.length()) { + char ch = body.charAt(i++); + if (esc) { val.append(ch == 'n' ? '\n' : ch == 't' ? '\t' : ch); esc = false; } + else if (ch == '\\') esc = true; + else if (ch == '"') break; + else val.append(ch); + } + if (key.length() > 0) map.put(key.toString(), val.toString()); + } + while (i < body.length() && (body.charAt(i) == ',' || Character.isWhitespace(body.charAt(i)))) i++; + } + } + } + } catch (Exception ignored) {} + } + sendHttpResponse(out, "{\"success\":true}", 200); + return; + } + // GET – Status-Endpunkt if (inputLine.startsWith("GET")) { Map data = new LinkedHashMap<>(); @@ -1202,4 +1270,165 @@ public class StatusAPI extends Plugin implements Runnable { String e = event.trim().toLowerCase(Locale.ROOT); return e.contains("ip_rate") || e.contains("vpn") || e.contains("learning_threshold_block") || e.contains("block"); } + + // ── PAPI-Token-Erkennung ────────────────────────────────────────────────── + + /** Alle Tokens die StatusAPI selbst auflöst – werden nicht an PAPI weitergegeben */ + private static final Set NATIVE_TOKENS = new HashSet<>(Arrays.asList( + "player", "rank", "money", "server", "compass", "health", "hearts", "ping", + "online", "maxplayers", "tps", "ram", "time", "playtime", "x", "y", "z", + "world", "gamemode", "exp", "food", "foodsym", "speed", "uptime", "servers", + "proxymem", "date", "news", "line", "balance", + "ticket_my_open", "ticket_open", "ticket_claimed", + "ticket_rating_good", "ticket_rating_bad", "ticket_rating_pct" + )); + + /** + * Scannt alle .properties-Dateien im Plugin-Ordner nach %token%-Mustern, + * filtert nativ unterstützte Tokens heraus und veröffentlicht den Rest + * als JSON-Array unter GET /papi/tokens für StatusAPIBridge. + */ + public void scanAndPublishPapiTokens() { + Set tokens = new LinkedHashSet<>(); + File folder = getDataFolder(); + if (folder.exists()) { + File[] files = folder.listFiles((dir, name) -> name.endsWith(".properties")); + if (files != null) { + for (File f : files) { + try (BufferedReader br = new BufferedReader( + new InputStreamReader(new FileInputStream(f), StandardCharsets.UTF_8))) { + String line; + while ((line = br.readLine()) != null) { + extractAllTokensFromText(line, tokens); + } + } catch (IOException ignored) {} + } + } + } + // Ressourcen-Defaults scannen (falls noch keine Dateien im Ordner) + for (String resource : new String[]{"scoreboard.properties"}) { + try (java.io.InputStream is = getResourceAsStream(resource)) { + if (is == null) continue; + BufferedReader br = new BufferedReader(new InputStreamReader(is, StandardCharsets.UTF_8)); + String line; + while ((line = br.readLine()) != null) { + extractAllTokensFromText(line, tokens); + } + } catch (IOException ignored) {} + } + tokens.removeAll(NATIVE_TOKENS); + // JSON-Array bauen + StringBuilder json = new StringBuilder("["); + boolean first = true; + for (String token : tokens) { + if (!first) json.append(","); + json.append("\"").append(token.replace("\\", "\\\\").replace("\"", "\\\"")).append("\""); + first = false; + } + json.append("]"); + papiTokensJson = json.toString(); + if (!tokens.isEmpty()) { + getLogger().info("[StatusAPI] " + tokens.size() + " PAPI-Token(s) erkannt: " + tokens); + } + } + + private static void extractAllTokensFromText(String text, Set result) { + if (text == null || text.startsWith("#") || !text.contains("%")) return; + int eq = text.indexOf('='); + String value = eq >= 0 ? text.substring(eq + 1) : text; + int i = 0; + while (i < value.length()) { + int start = value.indexOf('%', i); + if (start < 0) break; + int end = value.indexOf('%', start + 1); + if (end < 0) break; + String token = value.substring(start + 1, end); + if (!token.isEmpty() && !token.contains(" ") && token.matches("[a-zA-Z0-9_:]+")) { + result.add(token); + } + i = end + 1; + } + } + + // ── Reload ──────────────────────────────────────────────────────────────── + + /** + * Lädt Scoreboard und Tablist neu (Config + Tasks), ohne den HTTP-Server zu berühren. + * Alle anderen Module (Chat, AntiBot, etc.) bleiben unberührt. + */ + public void reloadModules() { + getLogger().info("[StatusAPI] Reload von Scoreboard und Tablist..."); + + net.viper.status.module.Module sbMod = moduleManager.getModule("ScoreboardModule"); + net.viper.status.module.Module tabMod = moduleManager.getModule("TablistModule"); + + if (sbMod != null) sbMod.onDisable(this); + if (tabMod != null) tabMod.onDisable(this); + + // Neue Instanzen erstellen und registrieren + net.viper.status.modules.scoreboard.ScoreboardModule newSb = new net.viper.status.modules.scoreboard.ScoreboardModule(); + net.viper.status.modules.tablist.TablistModule newTab = new net.viper.status.modules.tablist.TablistModule(); + + moduleManager.replaceModule("ScoreboardModule", newSb); + moduleManager.replaceModule("TablistModule", newTab); + + newSb.onEnable(this); + newTab.onEnable(this); + + // TPS-Fallback neu verbinden + try { + net.viper.status.modules.network.NetworkInfoModule nim = + (net.viper.status.modules.network.NetworkInfoModule) moduleManager.getModule("NetworkInfoModule"); + if (nim != null) newSb.setNetworkInfoModule(nim); + } catch (Exception ignored) {} + + scanAndPublishPapiTokens(); + getLogger().info("[StatusAPI] Reload abgeschlossen."); + } + + // ── /statusapi Befehl ───────────────────────────────────────────────────── + + private static class StatusAPICommand extends net.md_5.bungee.api.plugin.Command { + + private final StatusAPI plugin; + + StatusAPICommand(StatusAPI plugin) { + super("statusapi", "statusapi.admin", "sapi"); + this.plugin = plugin; + } + + @Override + public void execute(net.md_5.bungee.api.CommandSender sender, String[] args) { + if (args.length == 0 || args[0].equalsIgnoreCase("help")) { + send(sender, "&8&m──────────────────────────────────────────"); + send(sender, "&6&lStatusAPI &7| Befehle"); + send(sender, "&e/statusapi reload &7– Scoreboard & Tablist neu laden"); + send(sender, "&8&m──────────────────────────────────────────"); + return; + } + + if (!sender.hasPermission("statusapi.admin")) { + send(sender, "&cKeine Berechtigung."); + return; + } + + switch (args[0].toLowerCase()) { + case "reload": + send(sender, "&7Lade &6Scoreboard &7und &6Tablist &7neu..."); + plugin.reloadModules(); + send(sender, "&aScoreboard &7und &aTablist &7wurden neu geladen."); + send(sender, "&7PAPI-Tokens erkannt: &e" + papiTokensJson); + break; + + default: + send(sender, "&cUnbekannter Unterbefehl. Nutze &e/statusapi help&c."); + break; + } + } + + private static void send(net.md_5.bungee.api.CommandSender s, String text) { + s.sendMessage(new net.md_5.bungee.api.chat.TextComponent( + net.md_5.bungee.api.ChatColor.translateAlternateColorCodes('&', text))); + } + } } \ No newline at end of file diff --git a/StatusAPI/src/main/java/net/viper/status/module/ModuleManager.java b/StatusAPI/src/main/java/net/viper/status/module/ModuleManager.java index 34620e7..6d62d7a 100644 --- a/StatusAPI/src/main/java/net/viper/status/module/ModuleManager.java +++ b/StatusAPI/src/main/java/net/viper/status/module/ModuleManager.java @@ -47,6 +47,14 @@ public class ModuleManager { return modules.get(name.toLowerCase()); } + /** + * Ersetzt ein bestehendes Modul durch eine neue Instanz (für Reload). + * Das alte Modul muss bereits deaktiviert worden sein. + */ + public void replaceModule(String name, Module newModule) { + modules.put(name.toLowerCase(), newModule); + } + @SuppressWarnings("unchecked") public T getModule(Class clazz) { for (Module m : modules.values()) { 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 3ff5448..a41d53b 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 @@ -67,6 +67,8 @@ public class ScoreboardModule implements Module, Listener { public static final java.util.concurrent.atomic.AtomicInteger ticketRatingBad = new java.util.concurrent.atomic.AtomicInteger(0); private final ConcurrentHashMap joinTimes = new ConcurrentHashMap<>(); + /** Aktuell gerenderter Spieler – für PAPI-Auflösung in ph() */ + private UUID currentPlayerUuid = null; // Spieler, die das Scoreboard ausgeblendet haben /** FIX: Referenz auf NetworkInfoModule für TPS-Fallback */ private net.viper.status.modules.network.NetworkInfoModule networkInfoModule = null; @@ -302,7 +304,7 @@ public class ScoreboardModule implements Module, Listener { team.setDisplayName(Either.right(new net.md_5.bungee.api.chat.TextComponent(""))); team.setNameTagVisibility(Either.right(net.md_5.bungee.protocol.packet.Team.NameTagVisibility.ALWAYS)); team.setCollisionRule(Either.right(net.md_5.bungee.protocol.packet.Team.CollisionRule.ALWAYS)); - team.setColor(21); + team.setColor(Optional.of(21)); team.setFriendlyFire((byte) 3); sendPkt.invoke(p, team); } @@ -466,6 +468,7 @@ public class ScoreboardModule implements Module, Listener { boolean hasTicker = !tickerText.isEmpty() && !isAdmin && !isSupporter; if (hasTicker) lines.add(ticker(rawTicker, tOff, rIdx)); // Maximale Inhaltszeilen: MAX_LINES insgesamt (Ticker zählt als eine) + currentPlayerUuid = id; // für PAPI-Auflösung in ph() for (String tpl : srcLines) { if (lines.size() >= MAX_LINES) break; lines.add(c(ph(tpl, pn, rank, money, srv, comp, hp, hpNum, ping, online, maxpl, tps, ram, time, playtime, @@ -564,7 +567,7 @@ public class ScoreboardModule implements Module, Listener { team.setDisplayName(Either.right(new net.md_5.bungee.api.chat.TextComponent(""))); team.setNameTagVisibility(Either.right(NameTagVisibility.ALWAYS)); team.setCollisionRule(Either.right(CollisionRule.ALWAYS)); - team.setColor(21); // RESET + team.setColor(Optional.of(21)); // RESET team.setFriendlyFire((byte) 3); team.setPlayers(new String[]{ ENTRIES[i] }); sendPkt.invoke(p, team); @@ -835,7 +838,9 @@ public class ScoreboardModule implements Module, Listener { String ticketMyOpen, String ticketTotalOpen, String ticketTotalClaimed, String ticketRatingGood, String ticketRatingBad, String ticketRatingPct) { if (tpl == null) return " "; - String s = tpl + // PAPI-Werte zuerst einsetzen; native Tokens überschreiben sie danach + String s = resolvePapiPlaceholders(tpl, currentPlayerUuid); + s = s .replace("%player%", player) .replace("%rank%", rank) .replace("%money%", money) .replace("%server%", server) .replace("%compass%", compass) .replace("%health%", health) @@ -864,6 +869,31 @@ public class ScoreboardModule implements Module, Listener { return s.isEmpty() ? " " : s; } + private static String resolvePapiPlaceholders(String text, UUID uuid) { + if (text == null || !text.contains("%")) return text; + if (uuid == null) return text; + java.util.Map papiMap = net.viper.status.StatusAPI.playerPapi.get(uuid); + if (papiMap == null || papiMap.isEmpty()) return text; + StringBuilder sb = new StringBuilder(); + int i = 0; + while (i < text.length()) { + int start = text.indexOf('%', i); + if (start < 0) { sb.append(text.substring(i)); break; } + int end = text.indexOf('%', start + 1); + if (end < 0) { sb.append(text.substring(i)); break; } + String token = text.substring(start + 1, end); + if (papiMap.containsKey(token)) { + sb.append(text, i, start); + sb.append(papiMap.get(token)); + i = end + 1; + } else { + sb.append(text, i, end + 1); + i = end + 1; + } + } + return sb.toString(); + } + // ── Daten-Helfer ───────────────────────────────────────────────────────── private String getRank(ProxiedPlayer p) { 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 9374c35..90189a2 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 @@ -73,6 +73,9 @@ public class TablistModule implements Module, Listener { private boolean compactFooter4Spacer = false; private String colorSrvHeader = "&6&l"; + private boolean showFooterServerList = true; + private String columnHeaderMode = "none"; + private final Map serverSymbols = new LinkedHashMap<>(); private String timeFormat = "HH:mm:ss / h:mm a"; private String timeZone = "Europe/Berlin"; private SimpleDateFormat sdf; @@ -307,7 +310,7 @@ public class TablistModule implements Module, Listener { StringBuilder sb = new StringBuilder(); appendLine(sb, compactFooter1, compactFooter1Spacer, viewer, srv, world, rank, time, balance, online); List servers = getServerOrder(); - if (!servers.isEmpty()) { + if (showFooterServerList && !servers.isEmpty()) { StringBuilder sLine = new StringBuilder(); for (String sName : servers) { ServerInfo si = ProxyServer.getInstance().getServerInfo(sName); @@ -340,6 +343,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); + boolean useSlotHeader = "full".equalsIgnoreCase(columnHeaderMode); // Info-Spalte (nur classic) if (!compact) { @@ -372,16 +376,18 @@ public class TablistModule implements Module, Listener { List servers = getServerOrder(); int startCol = compact ? 0 : 1; for (int col = startCol; col < columns && (col - startCol) < servers.size(); col++) { - int base = col * rows, row = 0; + int base = col * rows; + int row = 0; String sName = servers.get(col - startCol); - row = set(texts, base, row, c(colorSrvHeader + capitalize(sName))); + 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); - set(texts, base, row, prefix.isEmpty() ? c("&7" + p.getName()) : c(prefix + "&r " + p.getName())); - // Skin aus Cache – immer aktuell + 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(); @@ -412,14 +418,20 @@ public class TablistModule implements Module, Listener { if (sendPacketQueuedMethod == null) return; try { Collection online = ProxyServer.getInstance().getPlayers(); - if (online.isEmpty()) return; + List toHide = new ArrayList<>(); + for (ProxiedPlayer p : online) toHide.add(p.getUniqueId()); + 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))); + } catch (Exception ignored) {} + } + if (toHide.isEmpty()) return; PlayerListItemUpdate pkt = new PlayerListItemUpdate(); pkt.setActions(EnumSet.of(PlayerListItemUpdate.Action.UPDATE_LISTED)); - Item[] items = new Item[online.size()]; + Item[] items = new Item[toHide.size()]; int idx = 0; - for (ProxiedPlayer p : online) { - Item it = new Item(); it.setUuid(p.getUniqueId()); 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()); } @@ -461,6 +473,13 @@ public class TablistModule implements Module, Listener { // ── Helpers ──────────────────────────────────────────────────────────────── + private String getServerSymbol(ProxiedPlayer player) { + if (serverSymbols.isEmpty() || player.getServer() == null) return ""; + String raw = serverSymbols.get(player.getServer().getInfo().getName().toLowerCase()); + if (raw == null || raw.isEmpty()) return ""; + return c(raw); + } + private net.md_5.bungee.protocol.data.Property[] fetchSkin(ProxiedPlayer player) { try { Object pending = player.getPendingConnection(); @@ -546,7 +565,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); - if (pfx != null) return ChatColor.translateAlternateColorCodes('&', pfx.toString()); + if (pfx != null) return c(pfx.toString()); } } catch (Exception ignored) {} return ""; @@ -563,10 +582,37 @@ 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 ""; - return text.replace("%player%", viewer.getName()).replace("%rank%", rank) + // 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) .replace("%balance%", balance).replace("%ping%", String.valueOf(viewer.getPing())) .replace("%online%", String.valueOf(online)); + return result; + } + + private static String resolvePapiPlaceholders(String text, UUID uuid) { + if (text == null || !text.contains("%")) return text; + java.util.Map papiMap = net.viper.status.StatusAPI.playerPapi.get(uuid); + if (papiMap == null || papiMap.isEmpty()) return text; + StringBuilder sb = new StringBuilder(); + int i = 0; + while (i < text.length()) { + int start = text.indexOf('%', i); + if (start < 0) { sb.append(text.substring(i)); break; } + int end = text.indexOf('%', start + 1); + if (end < 0) { sb.append(text.substring(i)); break; } + String token = text.substring(start + 1, end); + if (papiMap.containsKey(token)) { + sb.append(text, i, start); + sb.append(papiMap.get(token)); + i = end + 1; + } else { + sb.append(text, i, end + 1); + i = end + 1; + } + } + return sb.toString(); } private int set(String[] arr, int base, int row, String text) { @@ -580,7 +626,8 @@ public class TablistModule implements Module, Listener { } private static String replaceHexColors(String text) { - if (text == null || (!text.contains("&#") && !text.contains("{#"))) return text; + if (text == null) return null; + if (!text.contains("&#") && !text.contains("{#") && !text.contains("<#")) return text; StringBuilder sb = new StringBuilder(); int i = 0; while (i < text.length()) { @@ -603,6 +650,18 @@ public class TablistModule implements Module, Listener { } } } + // 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++; } return sb.toString(); @@ -653,6 +712,10 @@ public class TablistModule implements Module, Listener { "tablist.compact.footer.line4=\n" + "tablist.compact.footer.line4.spacer=false\n\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" + + "tablist.compact.footer.serverlist=true\n" + "tablist.time_format=HH:mm:ss / h:mm a\n" + "tablist.timezone=Europe/Berlin\n\n" + "# ── Info-Spalte (nur classic) ────────────────────────────────────────\n" + @@ -679,7 +742,11 @@ public class TablistModule implements Module, Listener { "tablist.info.teamspeak.enabled=true\n" + "tablist.info.teamspeak.label=&b&lTeamspeak:\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" + + "# Format: tablist.symbol.=&FarbCode Symbol\n" + + "tablist.symbol.lobby=&f\uD83C\uDFE0\n" + + "tablist.symbol.sv1=&6\u26CF\uFE0F\n"; try (OutputStream out = new FileOutputStream(f)) { out.write(content.getBytes(StandardCharsets.UTF_8)); } catch (Exception e) { plugin.getLogger().warning("[TablistModule] Config: " + e.getMessage()); } } @@ -723,6 +790,8 @@ public class TablistModule implements Module, Listener { compactFooter1Spacer = Boolean.parseBoolean(get.apply("tablist.compact.footer.line1.spacer", "false")); compactFooter4Spacer = Boolean.parseBoolean(get.apply("tablist.compact.footer.line4.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)); } @@ -759,5 +828,17 @@ public class TablistModule implements Module, Listener { infoEntries.add(new InfoEntry("&b&lTime:", "time", "", true)); infoEntries.add(new InfoEntry("&b&lTeamspeak:", "teamspeak", "&fts.viper-network.de", true)); } + + // Server-Symbole + serverSymbols.clear(); + for (Map.Entry entry : map.entrySet()) { + String key = entry.getKey(); + if (key.startsWith("tablist.symbol.")) { + String srvName = key.substring("tablist.symbol.".length()).trim().toLowerCase(); + String symbol = entry.getValue().trim(); + if (!srvName.isEmpty() && !symbol.isEmpty()) + serverSymbols.put(srvName, symbol); + } + } } } \ No newline at end of file diff --git a/StatusAPI/src/main/resources/network-guard.properties b/StatusAPI/src/main/resources/network-guard.properties index d4c5754..fa98c59 100644 --- a/StatusAPI/src/main/resources/network-guard.properties +++ b/StatusAPI/src/main/resources/network-guard.properties @@ -8,7 +8,7 @@ networkinfo.include_player_names=false # Discord Webhook fuer Status-, Warn- und Attack-Meldungen networkinfo.webhook.enabled=true -networkinfo.webhook.url=https://discord.com/api/webhooks/1488630083164831844/o7L5Mhy5P_xE_n-2Dq9usIVX40o7fCpPHgaGQOVIQHjfK7SDrVJbdeZM-G6vVRVhvzT9 +networkinfo.webhook.url= networkinfo.webhook.username=StatusAPI networkinfo.webhook.thumbnail_url= networkinfo.webhook.notify_start_stop=true diff --git a/StatusAPI/src/main/resources/plugin.yml b/StatusAPI/src/main/resources/plugin.yml index 910bfd8..5d98cf2 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.0 +version: 4.1.1 author: M_Viper description: StatusAPI für BungeeCord inkl. Update-Checker, Modul-System und ChatModule # Mindestanforderung: Minecraft 1.20 / BungeeCord mit PlayerChatEvent-Unterstützung @@ -16,6 +16,13 @@ commands: usage: /scoreboard [hide|show|player|admin] aliases: [sb, togglesb] + # ── StatusAPI Admin ─────────────────────────────────────── + statusapi: + description: StatusAPI verwalten (Reload, Info) + usage: /statusapi reload + aliases: [sapi] + permission: statusapi.admin + # /pay und /ecoadmin werden von NexEco (Spigot) verwaltet # ── VanishModule ────────────────────────────────────────── @@ -192,6 +199,10 @@ commands: permissions: # ── StatusAPI Core ──────────────────────────────────────── + statusapi.admin: + description: Zugang zu StatusAPI-Administrationsbefehlen (reload etc.) + default: op + statusapi.update.notify: description: Erlaubt Update-Benachrichtigungen default: op