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