From 1a53977db0c2f03aee12dd59f50cadd450673826 Mon Sep 17 00:00:00 2001 From: Git Manager GUI Date: Mon, 1 Jun 2026 21:50:15 +0200 Subject: [PATCH] Upload folder via GUI - src --- .../main/java/net/viper/status/StatusAPI.java | 121 +------------- .../java/net/viper/status/UpdateChecker.java | 59 ++++--- .../AutoMessage/AutoMessageModule.java | 133 +-------------- .../viper/status/modules/afk/AfkModule.java | 152 ++++-------------- .../status/modules/antibot/AntiBotModule.java | 90 ++++------- .../viper/status/modules/chat/ChatLogger.java | 13 +- .../status/modules/chat/ReportManager.java | 6 +- .../modules/scoreboard/ScoreboardModule.java | 107 ++++-------- .../net/viper/status/stats/StatsStorage.java | 13 +- StatusAPI/src/main/resources/messages.txt | 70 +++----- 10 files changed, 165 insertions(+), 599 deletions(-) diff --git a/StatusAPI/src/main/java/net/viper/status/StatusAPI.java b/StatusAPI/src/main/java/net/viper/status/StatusAPI.java index c06fee2..29fdcab 100644 --- a/StatusAPI/src/main/java/net/viper/status/StatusAPI.java +++ b/StatusAPI/src/main/java/net/viper/status/StatusAPI.java @@ -3,17 +3,7 @@ package net.viper.status; import net.md_5.bungee.api.ProxyServer; import net.md_5.bungee.api.config.ListenerInfo; import net.md_5.bungee.api.connection.ProxiedPlayer; -import net.md_5.bungee.api.chat.ClickEvent; -import net.md_5.bungee.api.chat.HoverEvent; -import net.md_5.bungee.api.chat.hover.content.Text; -import net.md_5.bungee.api.chat.ClickEvent; -import net.md_5.bungee.api.chat.HoverEvent; -import net.md_5.bungee.api.chat.hover.content.Text; -import net.md_5.bungee.api.event.PlayerDisconnectEvent; -import net.md_5.bungee.api.event.PostLoginEvent; -import net.md_5.bungee.api.plugin.Listener; import net.md_5.bungee.api.plugin.Plugin; -import net.md_5.bungee.event.EventHandler; import net.viper.status.module.ModuleManager; import net.viper.status.modules.economy.EconomyModule; import net.viper.status.modules.tablist.TablistModule; @@ -58,7 +48,7 @@ import net.md_5.bungee.api.scheduler.ScheduledTask; /** * StatusAPI - zentraler BungeeCord HTTP-Status- und Broadcast-Endpunkt */ -public class StatusAPI extends Plugin implements Runnable, Listener { +public class StatusAPI extends Plugin implements Runnable { // Welt pro Spieler (UUID -> Weltname), wird von StatusAPIBridge gepusht public static final ConcurrentHashMap playerWorlds = new ConcurrentHashMap<>(); @@ -178,9 +168,6 @@ public class StatusAPI extends Plugin implements Runnable, Listener { getLogger().info("[PlayerLoginLogger] Login-Logging aktiv -> " + getDataFolder() + "/player-logins.log (Zeitzone: " + loginZone + ")"); } - // Memory-Leak-Fix: playerWorlds/playerPapi beim Disconnect bereinigen - ProxyServer.getInstance().getPluginManager().registerListener(this, this); - // FIX: ScoreboardModule mit NetworkInfoModule verbinden (TPS-Fallback) try { net.viper.status.modules.scoreboard.ScoreboardModule sbMod = @@ -304,107 +291,17 @@ public class StatusAPI extends Plugin implements Runnable, Listener { updateChecker.checkNow(); String currentVersion = getDescription() != null ? getDescription().getVersion() : "0.0.0"; if (updateChecker.isUpdateAvailable(currentVersion)) { - String newVersion = updateChecker.getLatestVersion(); - String releaseUrl = updateChecker.getLatestUrl(); - boolean preRelease = updateChecker.isLatestPreRelease(); - // Konsolen-Log - getLogger().warning((preRelease ? "[Pre-Release] " : "") + "Update verfuegbar: " + newVersion + " -> " + releaseUrl); - // Ingame-Meldung an alle Spieler mit statusapi.update.notify - broadcastUpdateNotice(newVersion, releaseUrl, preRelease); + String newVersion = updateChecker.getLatestVersion(); + getLogger().warning("----------------------------------------"); + getLogger().warning("Neue Version verf\u00fcgbar: " + newVersion); + getLogger().warning("Download: " + updateChecker.getLatestUrl()); + getLogger().warning("----------------------------------------"); } } catch (Exception e) { getLogger().severe("Fehler beim Update-Check: " + e.getMessage()); } } - /** Sendet die Update-Meldung an alle online Spieler mit statusapi.update.notify */ - private void broadcastUpdateNotice(String newVersion, String releaseUrl, boolean preRelease) { - String current = getDescription() != null ? getDescription().getVersion() : "?"; - sendUpdateMessage(ProxyServer.getInstance().getConsole(), newVersion, current, releaseUrl, preRelease); - for (net.md_5.bungee.api.connection.ProxiedPlayer p : ProxyServer.getInstance().getPlayers()) { - if (p.hasPermission("statusapi.update.notify")) { - sendUpdateMessage(p, newVersion, current, releaseUrl, preRelease); - } - } - } - - private static final String RELEASES_URL = "https://git.viper.ipv64.net/M_Viper/StatusAPI/releases"; - - /** Formatiert und schickt die Update-Meldung an einen einzelnen Empfaenger */ - private static void sendUpdateMessage(net.md_5.bungee.api.CommandSender target, - String newVersion, String currentVersion, String releaseUrl, - boolean preRelease) { - // Einfache Textzeilen - String typeLabel = preRelease ? "&e&lPre-Release" : "&a&lRelease"; - String typeNotice = preRelease ? "&eVorsicht: Dies ist ein Pre-Release und kann instabil sein!" : ""; - String[] plainLines = preRelease ? new String[]{ - "&8&m" + repeat("-", 44), - "&6&l StatusAPI &7\u2013 &e&lPre-Release verf\u00fcgbar!", - "&7Aktuelle Version: &c" + currentVersion, - "&7Neue Version: &e" + newVersion + " &7[Pre-Release]", - "&eVorsicht: &7Kann instabil sein, bitte testen!", - } : new String[]{ - "&8&m" + repeat("-", 44), - "&6&l StatusAPI &7\u2013 Update verf\u00fcgbar!", - "&7Aktuelle Version: &c" + currentVersion, - "&7Neue Version: &a" + newVersion, - }; - for (String line : plainLines) { - target.sendMessage(new net.md_5.bungee.api.chat.TextComponent( - net.md_5.bungee.api.ChatColor.translateAlternateColorCodes('&', line))); - } - - // Klickbare Link-Zeile (nur fuer ProxiedPlayer, Konsole bekommt Plaintext) - if (target instanceof net.md_5.bungee.api.connection.ProxiedPlayer) { - net.md_5.bungee.api.chat.TextComponent prefix = new net.md_5.bungee.api.chat.TextComponent( - net.md_5.bungee.api.ChatColor.translateAlternateColorCodes('&', "&7Releases: ")); - net.md_5.bungee.api.chat.TextComponent link = new net.md_5.bungee.api.chat.TextComponent( - net.md_5.bungee.api.ChatColor.translateAlternateColorCodes('&', "&b&n" + RELEASES_URL)); - link.setClickEvent(new ClickEvent(ClickEvent.Action.OPEN_URL, RELEASES_URL)); - link.setHoverEvent(new HoverEvent(HoverEvent.Action.SHOW_TEXT, - new Text(net.md_5.bungee.api.ChatColor.translateAlternateColorCodes('&', - "&7Klicke um die Release-Seite zu \u00f6ffnen")))); - prefix.addExtra(link); - target.sendMessage(prefix); - } else { - target.sendMessage(new net.md_5.bungee.api.chat.TextComponent( - net.md_5.bungee.api.ChatColor.translateAlternateColorCodes('&', "&7Releases: &b" + RELEASES_URL))); - } - - target.sendMessage(new net.md_5.bungee.api.chat.TextComponent( - net.md_5.bungee.api.ChatColor.translateAlternateColorCodes('&', "&8&m" + repeat("-", 44)))); - } - - private static String repeat(String s, int n) { - StringBuilder sb = new StringBuilder(); - for (int i = 0; i < n; i++) sb.append(s); - return sb.toString(); - } - - // Memory-Leak-Fix: playerWorlds und playerPapi beim Spieler-Logout bereinigen. - // Diese Maps werden über HTTP von der Bridge befüllt und haben sonst keinen - // Disconnect-Handler, was zu unbegrenztem Wachstum führt. - @EventHandler - public void onPlayerDisconnect(PlayerDisconnectEvent event) { - UUID id = event.getPlayer().getUniqueId(); - playerWorlds.remove(id); - playerPapi.remove(id); - } - - /** Update-Hinweis beim Login für Spieler mit statusapi.update.notify */ - @EventHandler - public void onPostLogin(PostLoginEvent event) { - net.md_5.bungee.api.connection.ProxiedPlayer p = event.getPlayer(); - if (!p.hasPermission("statusapi.update.notify")) return; - String current = getDescription() != null ? getDescription().getVersion() : "0.0.0"; - if (updateChecker != null && updateChecker.isUpdateAvailable(current)) { - // Kurze Verzögerung damit der Spieler erst spawnt - ProxyServer.getInstance().getScheduler().schedule(this, () -> - sendUpdateMessage(p, updateChecker.getLatestVersion(), current, updateChecker.getLatestUrl(), updateChecker.isLatestPreRelease()), - 3, TimeUnit.SECONDS); - } - } - // --- WebServer --- @Override public void run() { @@ -1221,19 +1118,13 @@ public class StatusAPI extends Plugin implements Runnable, Listener { /** * Liest den HTTP-Body basierend auf Content-Length. - * Max. 1 MB – schützt vor überdimensionierten Requests (DoS via Content-Length). */ - private static final int MAX_BODY_SIZE = 1024 * 1024; // 1 MB private String readBody(BufferedReader in, Map headers) throws IOException { int contentLength = 0; if (headers.containsKey("content-length")) { try { contentLength = Integer.parseInt(headers.get("content-length")); } catch (NumberFormatException ignored) {} } if (contentLength <= 0) return ""; - if (contentLength > MAX_BODY_SIZE) { - getLogger().warning("[StatusAPI] Request abgelehnt: Content-Length " + contentLength + " > " + MAX_BODY_SIZE + " Bytes."); - return ""; - } char[] bodyChars = new char[contentLength]; int read = 0; while (read < contentLength) { diff --git a/StatusAPI/src/main/java/net/viper/status/UpdateChecker.java b/StatusAPI/src/main/java/net/viper/status/UpdateChecker.java index cd4c491..00c6c6f 100644 --- a/StatusAPI/src/main/java/net/viper/status/UpdateChecker.java +++ b/StatusAPI/src/main/java/net/viper/status/UpdateChecker.java @@ -19,13 +19,12 @@ public class UpdateChecker { // Neue Domain und korrekter API-Pfad f\u00fcr Releases private final String apiUrl = "https://git.viper.ipv64.net/api/v1/repos/M_Viper/StatusAPI/releases"; - private volatile String latestVersion = ""; - private volatile String latestUrl = ""; - private volatile boolean latestPreRelease = false; + private volatile String latestVersion = ""; + private volatile String latestUrl = ""; - private static final Pattern TAG_NAME_PATTERN = Pattern.compile("\"tag_name\"\\s*:\\s*\"([^\"]+)\"", Pattern.CASE_INSENSITIVE); - private static final Pattern HTML_URL_PATTERN = Pattern.compile("\"html_url\"\\s*:\\s*\"([^\"]+)\"", Pattern.CASE_INSENSITIVE); - private static final Pattern PRERELEASE_PATTERN = Pattern.compile("\"prerelease\"\\s*:\\s*(true|false)", Pattern.CASE_INSENSITIVE); + private static final Pattern ASSET_NAME_PATTERN = Pattern.compile("\"name\"\\s*:\\s*\"([^\"]+)\"", Pattern.CASE_INSENSITIVE); + private static final Pattern DOWNLOAD_PATTERN = Pattern.compile("\"browser_download_url\"\\s*:\\s*\"([^\"]+)\"", Pattern.CASE_INSENSITIVE); + private static final Pattern TAG_NAME_PATTERN = Pattern.compile("\"tag_name\"\\s*:\\s*\"([^\"]+)\"", Pattern.CASE_INSENSITIVE); public UpdateChecker(Plugin plugin, String currentVersion, int intervalHours) { this.plugin = plugin; @@ -72,23 +71,39 @@ public class UpdateChecker { foundVersion = foundVersion.substring(1); } - // Release-Seiten-URL fuer die Ingame-Meldung - String foundUrl = "https://git.viper.ipv64.net/M_Viper/StatusAPI/releases"; - Matcher urlM = HTML_URL_PATTERN.matcher(body); - if (urlM.find()) { - foundUrl = urlM.group(1).trim(); + String foundUrl = null; + + // Wir suchen im gesamten Body nach der JAR-Datei "StatusAPI.jar" + // Da das neueste Release zuerst kommt, brechen wir ab, sobald wir eine passende JAR finden + Matcher nameMatcher = ASSET_NAME_PATTERN.matcher(body); + Matcher downloadMatcher = DOWNLOAD_PATTERN.matcher(body); + + java.util.List names = new java.util.ArrayList<>(); + java.util.List urls = new java.util.ArrayList<>(); + + while (nameMatcher.find()) { + names.add(nameMatcher.group(1)); + } + while (downloadMatcher.find()) { + urls.add(downloadMatcher.group(1)); } - // prerelease-Flag aus dem ersten Release-Block lesen - boolean foundPreRelease = false; - Matcher preM = PRERELEASE_PATTERN.matcher(body); - if (preM.find()) { - foundPreRelease = Boolean.parseBoolean(preM.group(1).trim()); + int pairs = Math.min(names.size(), urls.size()); + for (int i = 0; i < pairs; i++) { + String name = names.get(i).trim(); + String url = urls.get(i); + if ("StatusAPI.jar".equalsIgnoreCase(name)) { + foundUrl = url; + break; // Erste (also neueste) passende JAR nehmen + } } - latestVersion = foundVersion; - latestUrl = foundUrl; - latestPreRelease = foundPreRelease; + if (foundUrl == null) { + plugin.getLogger().warning("Keine StatusAPI.jar im neuesten Release gefunden."); + return; + } + latestVersion = foundVersion; + latestUrl = foundUrl; } catch (Exception e) { plugin.getLogger().log(Level.SEVERE, "Fehler beim Update-Check: " + e.getMessage(), e); @@ -103,15 +118,9 @@ public class UpdateChecker { return latestUrl != null ? latestUrl : ""; } - public boolean isLatestPreRelease() { - return latestPreRelease; - } - public boolean isUpdateAvailable(String currentVer) { String lv = getLatestVersion(); if (lv.isEmpty()) return false; - // Nur melden wenn latest STRIKT groesser als current ist. - // Laeuft eine Dev-Version (current > latest), kein Hinweis. return compareVersions(lv, currentVer) > 0; } diff --git a/StatusAPI/src/main/java/net/viper/status/modules/AutoMessage/AutoMessageModule.java b/StatusAPI/src/main/java/net/viper/status/modules/AutoMessage/AutoMessageModule.java index 2f396b3..29b9636 100644 --- a/StatusAPI/src/main/java/net/viper/status/modules/AutoMessage/AutoMessageModule.java +++ b/StatusAPI/src/main/java/net/viper/status/modules/AutoMessage/AutoMessageModule.java @@ -3,7 +3,6 @@ package net.viper.status.modules.AutoMessage; import net.md_5.bungee.api.ChatColor; import net.md_5.bungee.api.CommandSender; import net.md_5.bungee.api.ProxyServer; -import net.md_5.bungee.api.chat.BaseComponent; import net.md_5.bungee.api.chat.TextComponent; import net.md_5.bungee.api.plugin.Command; import net.md_5.bungee.api.plugin.Plugin; @@ -14,7 +13,6 @@ import java.io.File; import java.io.IOException; import java.nio.charset.StandardCharsets; import java.nio.file.Files; -import java.util.ArrayList; import java.util.List; import java.util.Properties; import java.util.concurrent.TimeUnit; @@ -147,13 +145,12 @@ public class AutoMessageModule implements Module { if (idx >= messages.size()) idx = 0; String raw = messages.get(idx); - String prefixPart = prefix.isEmpty() ? "" : ChatColor.translateAlternateColorCodes('&', applyGradients(prefix)) + " "; - // Gradient zuerst auflösen, dann §/&-Codes übersetzen - String normalized = raw.replace("\u00a7", "&").replace("§", "&"); - String text = prefixPart + ChatColor.translateAlternateColorCodes('&', applyGradients(normalized)); + String prefixPart = prefix.isEmpty() ? "" : ChatColor.translateAlternateColorCodes('&', prefix) + " "; + // Fix: §-Codes direkt \u00fcbersetzen (messages.txt nutzt §-Codes) + String text = prefixPart + ChatColor.translateAlternateColorCodes('&', + raw.replace("\u00a7", "&").replace("§", "&")); - BaseComponent[] components = TextComponent.fromLegacyText(text); - ProxyServer.getInstance().broadcast(components); + ProxyServer.getInstance().broadcast(new TextComponent(text)); }, intervalSeconds, intervalSeconds, TimeUnit.SECONDS).getId(); } @@ -163,122 +160,4 @@ public class AutoMessageModule implements Module { taskId = -1; } } - - // ── Gradient-Support (%gradient:FARBE1:FARBE2:...:TEXT%) ───────────────── - - private String applyGradients(String input) { - if (input == null || !input.contains("%gradient:")) return input; - StringBuilder result = new StringBuilder(); - int i = 0; - while (i < input.length()) { - int start = input.indexOf("%gradient:", i); - if (start < 0) { result.append(input.substring(i)); break; } - result.append(input, i, start); - int end = input.indexOf("%", start + 10); - if (end < 0) { result.append(input.substring(start)); break; } - String inner = input.substring(start + 10, end); - List stops = new ArrayList<>(); - int colonIdx = 0; - while (colonIdx < inner.length()) { - int nextColon = inner.indexOf(':', colonIdx); - if (nextColon < 0) break; - String candidate = inner.substring(colonIdx, nextColon); - int[] rgb = parseGradientColor(candidate); - if (rgb != null) { stops.add(rgb); colonIdx = nextColon + 1; } - else break; - } - if (stops.size() < 2) { result.append(input, start, end + 1); i = end + 1; continue; } - String text = inner.substring(colonIdx); - String plain = ChatColor.stripColor(ChatColor.translateAlternateColorCodes('&', text)); - boolean bold = text.contains("&l") || text.contains("\u00A7l"); - boolean italic = text.contains("&o") || text.contains("\u00A7o"); - boolean underline = text.contains("&n") || text.contains("\u00A7n"); - boolean strike = text.contains("&m") || text.contains("\u00A7m"); - String fmt = (bold ? "\u00A7l" : "") + (italic ? "\u00A7o" : "") - + (underline ? "\u00A7n" : "") + (strike ? "\u00A7m" : ""); - int visLen = 0; - for (char ch : plain.toCharArray()) if (ch != ' ') visLen++; - if (visLen == 0) visLen = 1; - int charIdx = 0; - int[] lastRgb = stops.get(0); - for (char ch : plain.toCharArray()) { - if (ch == ' ') { - result.append('\u00A7').append('x'); - result.append('\u00A7').append(String.format("%02X", lastRgb[0]).charAt(0)); - result.append('\u00A7').append(String.format("%02X", lastRgb[0]).charAt(1)); - result.append('\u00A7').append(String.format("%02X", lastRgb[1]).charAt(0)); - result.append('\u00A7').append(String.format("%02X", lastRgb[1]).charAt(1)); - result.append('\u00A7').append(String.format("%02X", lastRgb[2]).charAt(0)); - result.append('\u00A7').append(String.format("%02X", lastRgb[2]).charAt(1)); - result.append(fmt).append(ch); - continue; - } - float pos = visLen <= 1 ? 0f : (float) charIdx / (visLen - 1); - lastRgb = interpolateGradient(stops, pos); - result.append('\u00A7').append('x'); - result.append('\u00A7').append(String.format("%02X", lastRgb[0]).charAt(0)); - result.append('\u00A7').append(String.format("%02X", lastRgb[0]).charAt(1)); - result.append('\u00A7').append(String.format("%02X", lastRgb[1]).charAt(0)); - result.append('\u00A7').append(String.format("%02X", lastRgb[1]).charAt(1)); - result.append('\u00A7').append(String.format("%02X", lastRgb[2]).charAt(0)); - result.append('\u00A7').append(String.format("%02X", lastRgb[2]).charAt(1)); - result.append(fmt).append(ch); - charIdx++; - } - // §r nach dem Gradient-Block, damit nachfolgende §8/§7 etc. wieder greifen - result.append('\u00A7').append('r'); - i = end + 1; - } - return result.toString(); - } - - private int[] parseGradientColor(String s) { - s = s.trim(); - if (s.startsWith("&#")) s = s.substring(1); - if (s.startsWith("#") && s.length() == 7) { - try { - return new int[]{ - Integer.parseInt(s.substring(1,3),16), - Integer.parseInt(s.substring(3,5),16), - Integer.parseInt(s.substring(5,7),16) - }; - } catch (Exception ignored) {} - } - if (s.startsWith("&") && s.length() == 2) return mcColorToRgb(s.charAt(1)); - return null; - } - - private int[] interpolateGradient(List stops, float pos) { - if (stops.size() == 1) return stops.get(0); - float scaled = pos * (stops.size() - 1); - int i0 = Math.min((int) scaled, stops.size() - 2); - float t = scaled - i0; - return new int[]{ - (int)(stops.get(i0)[0] * (1-t) + stops.get(i0+1)[0] * t), - (int)(stops.get(i0)[1] * (1-t) + stops.get(i0+1)[1] * t), - (int)(stops.get(i0)[2] * (1-t) + stops.get(i0+1)[2] * t) - }; - } - - private static int[] mcColorToRgb(char code) { - switch (Character.toLowerCase(code)) { - case '0': return new int[]{ 0, 0, 0}; - case '1': return new int[]{ 0, 0, 170}; - case '2': return new int[]{ 0, 170, 0}; - case '3': return new int[]{ 0, 170, 170}; - case '4': return new int[]{170, 0, 0}; - case '5': return new int[]{170, 0, 170}; - case '6': return new int[]{255, 170, 0}; - case '7': return new int[]{170, 170, 170}; - case '8': return new int[]{ 85, 85, 85}; - case '9': return new int[]{ 85, 85, 255}; - case 'a': return new int[]{ 85, 255, 85}; - case 'b': return new int[]{ 85, 255, 255}; - case 'c': return new int[]{255, 85, 85}; - case 'd': return new int[]{255, 85, 255}; - case 'e': return new int[]{255, 255, 85}; - case 'f': return new int[]{255, 255, 255}; - default: return null; - } - } -} \ No newline at end of file +} diff --git a/StatusAPI/src/main/java/net/viper/status/modules/afk/AfkModule.java b/StatusAPI/src/main/java/net/viper/status/modules/afk/AfkModule.java index 52fcb62..504458e 100644 --- a/StatusAPI/src/main/java/net/viper/status/modules/afk/AfkModule.java +++ b/StatusAPI/src/main/java/net/viper/status/modules/afk/AfkModule.java @@ -20,7 +20,6 @@ import net.viper.status.module.Module; import java.util.*; import java.util.concurrent.ConcurrentHashMap; import java.util.concurrent.TimeUnit; -import java.util.logging.Level; /** * AfkModule – /afk Befehl + automatische AFK-Erkennung nach Inaktivit\u00e4t. @@ -92,8 +91,6 @@ public class AfkModule implements Module, Listener { if (!enabled) { plugin.getLogger().info("[AfkModule] Deaktiviert."); return; } ProxyServer.getInstance().getPluginManager().registerListener(plugin, this); - // Plugin-Message-Channel registrieren (BungeeCord → Spigot) - ProxyServer.getInstance().registerChannel("statusapi:afk"); ProxyServer.getInstance().getPluginManager().registerCommand(plugin, new Command("afk") { @Override @@ -125,7 +122,6 @@ public class AfkModule implements Module, Listener { StatusAPI.playerAfk.clear(); lastActivity.clear(); INSTANCE = null; - try { ProxyServer.getInstance().unregisterChannel("statusapi:afk"); } catch (Exception ignored) {} } // ── Events ─────────────────────────────────────────────────────────────── @@ -135,9 +131,7 @@ public class AfkModule implements Module, Listener { if (!(e.getSender() instanceof ProxiedPlayer)) return; ProxiedPlayer p = (ProxiedPlayer) e.getSender(); recordActivity(p.getUniqueId()); - String msg = e.getMessage(); - if (msg == null) return; - if (!msg.toLowerCase(Locale.ROOT).startsWith("/afk") && isAfk(p.getUniqueId())) + if (!e.getMessage().toLowerCase().startsWith("/afk") && isAfk(p.getUniqueId())) setAfk(p, false); } @@ -197,56 +191,23 @@ public class AfkModule implements Module, Listener { } else { StatusAPI.playerAfk.remove(id); lastActivity.put(id, System.currentTimeMillis()); - TitlePair pair = activePair.remove(id); + TitlePair pair = activePair.get(id); stopTitleTask(id); clearTitle(p); if (pair != null) sendTitleEntry(p, pair.unset); } - // AFK-Status an alle Spigot-Server broadcasten damit die Bridge - // den Nametag-Prefix sofort aktualisieren kann. - broadcastAfkState(id, afk); - } - - /** - * Schickt eine Plugin-Message (Channel "statusapi:afk") an alle Spigot-Server, - * auf denen der betroffene Spieler ODER irgendein anderer Spieler eingeloggt ist. - * Format: "UUID:true" / "UUID:false" - */ - private void broadcastAfkState(UUID uuid, boolean afk) { - byte[] payload = (uuid.toString() + ":" + afk).getBytes(java.nio.charset.StandardCharsets.UTF_8); - Set reached = new java.util.HashSet<>(); - // Zuerst: Server des betroffenen Spielers direkt ansprechen - ProxiedPlayer self = ProxyServer.getInstance().getPlayer(uuid); - if (self != null && self.isConnected() && self.getServer() != null) { - net.md_5.bungee.api.config.ServerInfo srv = self.getServer().getInfo(); - if (srv != null) { srv.sendData("statusapi:afk", payload); reached.add(srv); } - } - // Dann: alle anderen Online-Spieler – deren Server brauchen das Update - // damit der Prefix über dem AFK-Spieler-Kopf auch bei Anderen stimmt. - for (ProxiedPlayer other : ProxyServer.getInstance().getPlayers()) { - if (!other.isConnected() || other.getServer() == null) continue; - net.md_5.bungee.api.config.ServerInfo srv = other.getServer().getInfo(); - if (srv != null && !reached.contains(srv)) { - srv.sendData("statusapi:afk", payload); - reached.add(srv); - } - } } private void checkIdle() { long now = System.currentTimeMillis(); long thresholdMs = idleSeconds * 1000L; for (ProxiedPlayer p : ProxyServer.getInstance().getPlayers()) { - try { - if (!p.isConnected()) continue; - UUID id = p.getUniqueId(); - if (p.hasPermission(bypassPerm) || isAfk(id)) continue; - Long last = lastActivity.get(id); - if (last == null) { lastActivity.put(id, now); continue; } - if (now - last >= thresholdMs) setAfk(p, true); - } catch (Exception ex) { - plugin.getLogger().log(Level.FINE, "[AfkModule] Fehler in checkIdle fuer Spieler.", ex); - } + if (!p.isConnected()) continue; + UUID id = p.getUniqueId(); + if (p.hasPermission(bypassPerm) || isAfk(id)) continue; + Long last = lastActivity.get(id); + if (last == null) { lastActivity.put(id, now); continue; } + if (now - last >= thresholdMs) setAfk(p, true); } } @@ -263,29 +224,17 @@ public class AfkModule implements Module, Listener { TitlePair pair = titlePairs.get(random.nextInt(titlePairs.size())); activePair.put(id, pair); activeTitleEntry.put(id, pair.set); - if (!sendTitleEntry(p, pair.set, true)) { // firstSend=true → TitleTimes mitsenden - stopTitleTask(id); - return; - } + sendTitleEntry(p, pair.set); - // Nicht zu aggressiv senden, um Verbindungsprobleme bei langen AFK-Phasen zu vermeiden. - long intervalMs = Math.max(1500, (titleStay - 20) * 50L); + long intervalMs = Math.max(500, (titleStay - 20) * 50L); ScheduledTask task = ProxyServer.getInstance().getScheduler().schedule(plugin, () -> { - try { - ProxiedPlayer online = ProxyServer.getInstance().getPlayer(id); - if (online == null || !online.isConnected() || !isAfk(id)) { - stopTitleTask(id); - return; - } - String[] current = activeTitleEntry.get(id); - // firstSend=false → kein TitleTimes → kein Blinken beim Repeat - if (current != null && !sendTitleEntry(online, current, false)) { - stopTitleTask(id); - } - } catch (Exception ex) { - plugin.getLogger().log(Level.FINE, "[AfkModule] Fehler im Title-Repeat-Task.", ex); + ProxiedPlayer online = ProxyServer.getInstance().getPlayer(id); + if (online == null || !online.isConnected() || !isAfk(id)) { stopTitleTask(id); + return; } + String[] current = activeTitleEntry.get(id); + if (current != null) sendTitleEntry(online, current); }, intervalMs, intervalMs, TimeUnit.MILLISECONDS); titleTasks.put(id, task); } @@ -294,15 +243,10 @@ public class AfkModule implements Module, Listener { ScheduledTask old = titleTasks.remove(id); if (old != null) old.cancel(); activeTitleEntry.remove(id); - // BUGFIX: activePair hier NICHT entfernen wenn der Spieler noch AFK ist – - // setAfk(false) liest activePair noch aus (für den Unset-Title) und entfernt es selbst. - // Wenn der Spieler aber NICHT mehr AFK ist (z.B. Exception im Task), muss activePair - // hier bereinigt werden, sonst Memory Leak. - if (!Boolean.TRUE.equals(StatusAPI.playerAfk.get(id))) { - activePair.remove(id); - } + // activePair bleibt bis setAfk(false) es ausliest, danach: } + /** Entfernt den Title sofort vom Bildschirm. */ /** Entfernt den Title sofort vom Bildschirm via ClearTitles-Packet. */ private void clearTitle(ProxiedPlayer p) { try { @@ -315,37 +259,20 @@ public class AfkModule implements Module, Listener { * Sendet Title + Subtitle als raw Packets (wie ScoreboardModule) – * dadurch werden Hex-Farben korrekt \u00fcbertragen, ohne durch TextComponent.fromArray() zu laufen. */ - /** - * Sendet Title + Subtitle an den Spieler. - * @param firstSend true = TitleTimes-Packet mitsenden (erster Aufruf). - * false = nur Title/Subtitle refreshen, KEIN TitleTimes → - * verhindert das Blinken (fade-in/-out) bei jedem Repeat-Tick. - */ - private boolean sendTitleEntry(ProxiedPlayer p, String[] entry, boolean firstSend) { - if (p == null || !p.isConnected() || entry == null || entry.length == 0) return false; - - String titleRaw = ChatColor.translateAlternateColorCodes('&', applyGradients( - entry[0] == null ? "" : entry[0] - )); - String subtitleRaw = entry.length > 1 - ? ChatColor.translateAlternateColorCodes('&', applyGradients(entry[1] == null ? "" : entry[1])) - : ""; - + private void sendTitleEntry(ProxiedPlayer p, String[] entry) { + String titleRaw = ChatColor.translateAlternateColorCodes('&', applyGradients(entry[0])); + String subtitleRaw = entry.length > 1 ? ChatColor.translateAlternateColorCodes('&', applyGradients(entry[1])) : ""; try { if (sendPkt == null) throw new IllegalStateException("sendPkt not initialized"); - // TitleTimes NUR beim ersten Senden schicken. - // Wird das Packet bei jedem Repeat-Tick gesendet, startet der Client - // jedes Mal eine neue fade-in-Animation → sichtbares Blinken. - if (firstSend) { - net.md_5.bungee.protocol.packet.TitleTimes times = new net.md_5.bungee.protocol.packet.TitleTimes(); - times.setFadeIn(titleFadeIn); - times.setStay(titleStay); - times.setFadeOut(titleFadeOut); - sendPkt.invoke(p, times); - } + // Times zuerst senden + net.md_5.bungee.protocol.packet.TitleTimes times = new net.md_5.bungee.protocol.packet.TitleTimes(); + times.setFadeIn(titleFadeIn); + times.setStay(titleStay); + times.setFadeOut(titleFadeOut); + sendPkt.invoke(p, times); - // Title-Packet + // Title-Packet (Action = TITLE, ordinal 0) net.md_5.bungee.protocol.packet.Title titlePkt = new net.md_5.bungee.protocol.packet.Title(); titlePkt.setAction(net.md_5.bungee.protocol.packet.Title.Action.TITLE); titlePkt.setText(mergeComponents(buildComponents(titleRaw))); @@ -356,37 +283,20 @@ public class AfkModule implements Module, Listener { subPkt.setText(mergeComponents(buildComponents(subtitleRaw))); sendPkt.invoke(p, subPkt); - return true; - } catch (Exception e) { // Fallback auf Title-API try { Title title = ProxyServer.getInstance().createTitle(); title.title(buildComponents(titleRaw)); title.subTitle(buildComponents(subtitleRaw)); - if (firstSend) { - title.fadeIn(titleFadeIn); - title.stay(titleStay); - title.fadeOut(titleFadeOut); - } else { - // Kein Fade → bleibt stabil stehen ohne Blink-Effekt - title.fadeIn(0); - title.stay(titleStay); - title.fadeOut(0); - } + title.fadeIn(titleFadeIn); + title.stay(titleStay); + title.fadeOut(titleFadeOut); p.sendTitle(title); - return true; - } catch (Exception ignored) { - return false; - } + } catch (Exception ignored) {} } } - /** Overload für Aufrufe ohne firstSend-Flag (z.B. unset-Title beim Zurückkommen). */ - private boolean sendTitleEntry(ProxiedPlayer p, String[] entry) { - return sendTitleEntry(p, entry, true); - } - /** Fasst BaseComponent[] in eine TextComponent zusammen (f\u00fcr Title/Subtitle setText). */ private static net.md_5.bungee.api.chat.BaseComponent mergeComponents(BaseComponent[] parts) { TextComponent root = new TextComponent(""); diff --git a/StatusAPI/src/main/java/net/viper/status/modules/antibot/AntiBotModule.java b/StatusAPI/src/main/java/net/viper/status/modules/antibot/AntiBotModule.java index f7b6c55..0a1ce97 100644 --- a/StatusAPI/src/main/java/net/viper/status/modules/antibot/AntiBotModule.java +++ b/StatusAPI/src/main/java/net/viper/status/modules/antibot/AntiBotModule.java @@ -246,73 +246,28 @@ public class AntiBotModule implements Module, Listener { } } - // FIX Netzwerk-Timeout: VPN-Check asynchron ausführen damit der BungeeCord- - // Netzwerk-Thread nicht blockiert wird. Ein blockierender HTTP-Request (bis zu - // vpnTimeoutMs = 2500 ms) im PreLoginEvent-Handler verhindert, dass KeepAlive- - // Pakete an bereits eingeloggte Spieler verarbeitet werden → Disconnect. if (vpnCheckEnabled) { - // Cache-Treffer: sofort prüfen, kein async nötig - VpnCacheEntry cached = vpnCache.get(ip); - if (cached != null && cached.expiresAt > now) { - VpnCheckResult info = cached.result; - if (info != null) { - boolean shouldBlock = (vpnBlockProxy && info.proxy) || (vpnBlockHosting && info.hosting); - if (shouldBlock) { - logSecurityEvent("vpn_detected", ip, event.getConnection(), "proxy=" + info.proxy + ", hosting=" + info.hosting); - if (learningModeEnabled) { - if (vpnBlockProxy && info.proxy) addLearningScore(ip, now, learningVpnProxyPoints, "vpn-proxy", false); - if (vpnBlockHosting && info.hosting) addLearningScore(ip, now, learningVpnHostingPoints, "vpn-hosting", false); - int current = getLearningScore(ip, now); - if (current >= learningScoreThreshold) { - blockIp(ip, now); - logSecurityEvent("learning_threshold_block", ip, event.getConnection(), "reason=vpn, score=" + current); - recordLearningEvent("BLOCK " + ip + " reason=vpn score=" + current); - blockEvent(event); - } - } else { + VpnCheckResult info = getVpnInfo(ip, now); + if (info != null) { + boolean shouldBlock = (vpnBlockProxy && info.proxy) || (vpnBlockHosting && info.hosting); + if (shouldBlock) { + logSecurityEvent("vpn_detected", ip, event.getConnection(), "proxy=" + info.proxy + ", hosting=" + info.hosting); + if (learningModeEnabled) { + if (vpnBlockProxy && info.proxy) addLearningScore(ip, now, learningVpnProxyPoints, "vpn-proxy", false); + if (vpnBlockHosting && info.hosting) addLearningScore(ip, now, learningVpnHostingPoints, "vpn-hosting", false); + int current = getLearningScore(ip, now); + if (current >= learningScoreThreshold) { blockIp(ip, now); - logSecurityEvent("vpn_block", ip, event.getConnection(), "mode=direct, proxy=" + info.proxy + ", hosting=" + info.hosting); + logSecurityEvent("learning_threshold_block", ip, event.getConnection(), "reason=vpn, score=" + current); + recordLearningEvent("BLOCK " + ip + " reason=vpn score=" + current); blockEvent(event); } + } else { + blockIp(ip, now); + logSecurityEvent("vpn_block", ip, event.getConnection(), "mode=direct, proxy=" + info.proxy + ", hosting=" + info.hosting); + blockEvent(event); } } - } else { - // Kein Cache-Treffer → HTTP-Request async ausführen - event.registerIntent(plugin); - final long nowFinal = now; - ProxyServer.getInstance().getScheduler().runAsync(plugin, () -> { - try { - VpnCheckResult info = requestIpApi(ip); - if (info != null) { - VpnCacheEntry entry = new VpnCacheEntry(); - entry.result = info; - entry.expiresAt = nowFinal + Math.max(1, vpnCacheMinutes) * 60_000L; - vpnCache.put(ip, entry); - - boolean shouldBlock = (vpnBlockProxy && info.proxy) || (vpnBlockHosting && info.hosting); - if (shouldBlock) { - logSecurityEvent("vpn_detected", ip, event.getConnection(), "proxy=" + info.proxy + ", hosting=" + info.hosting); - if (learningModeEnabled) { - if (vpnBlockProxy && info.proxy) addLearningScore(ip, nowFinal, learningVpnProxyPoints, "vpn-proxy", false); - if (vpnBlockHosting && info.hosting) addLearningScore(ip, nowFinal, learningVpnHostingPoints, "vpn-hosting", false); - int current = getLearningScore(ip, nowFinal); - if (current >= learningScoreThreshold) { - blockIp(ip, nowFinal); - logSecurityEvent("learning_threshold_block", ip, event.getConnection(), "reason=vpn, score=" + current); - recordLearningEvent("BLOCK " + ip + " reason=vpn score=" + current); - blockEvent(event); - } - } else { - blockIp(ip, nowFinal); - logSecurityEvent("vpn_block", ip, event.getConnection(), "mode=direct, proxy=" + info.proxy + ", hosting=" + info.hosting); - blockEvent(event); - } - } - } - } finally { - event.completeIntent(plugin); - } - }); } } } @@ -768,6 +723,19 @@ public class AntiBotModule implements Module, Listener { try { return Integer.parseInt(s == null ? "" : s.trim()); } catch (Exception ignored) { return fallback; } } + private VpnCheckResult getVpnInfo(String ip, long now) { + VpnCacheEntry cached = vpnCache.get(ip); + if (cached != null && cached.expiresAt > now) return cached.result; + VpnCheckResult fresh = requestIpApi(ip); + if (fresh != null) { + VpnCacheEntry entry = new VpnCacheEntry(); + entry.result = fresh; + entry.expiresAt = now + Math.max(1, vpnCacheMinutes) * 60_000L; + vpnCache.put(ip, entry); + } + return fresh; + } + private VpnCheckResult requestIpApi(String ip) { HttpURLConnection conn = null; try { diff --git a/StatusAPI/src/main/java/net/viper/status/modules/chat/ChatLogger.java b/StatusAPI/src/main/java/net/viper/status/modules/chat/ChatLogger.java index a943558..70e2e6f 100644 --- a/StatusAPI/src/main/java/net/viper/status/modules/chat/ChatLogger.java +++ b/StatusAPI/src/main/java/net/viper/status/modules/chat/ChatLogger.java @@ -22,11 +22,8 @@ public class ChatLogger { private final int retentionDays; private final AtomicInteger counter = new AtomicInteger(0); - // ThreadLocal: SimpleDateFormat ist NICHT thread-sicher – jeder Thread bekommt seine eigene Instanz. - private static final ThreadLocal DATE_FMT = - ThreadLocal.withInitial(() -> new SimpleDateFormat("yyyy-MM-dd")); - private static final ThreadLocal TIME_FMT = - ThreadLocal.withInitial(() -> new SimpleDateFormat("HH:mm:ss")); + private static final SimpleDateFormat DATE_FMT = new SimpleDateFormat("yyyy-MM-dd"); + private static final SimpleDateFormat TIME_FMT = new SimpleDateFormat("HH:mm:ss"); public ChatLogger(File dataFolder, Logger logger, int retentionDays) { this.logDir = new File(dataFolder, "chatlogs"); @@ -61,8 +58,8 @@ public class ChatLogger { * @param message Nachrichtentext (Rohtext, ohne Farbcodes) */ public void log(String msgId, String server, String channel, String player, String message) { - String date = DATE_FMT.get().format(new Date()); - String time = TIME_FMT.get().format(new Date()); + String date = DATE_FMT.format(new Date()); + String time = TIME_FMT.format(new Date()); // Minecraft-Farbcodes aus dem Log entfernen String cleanMsg = stripColor(message); @@ -122,7 +119,7 @@ public class ChatLogger { * @return Liste der Logzeilen (\u00e4lteste zuerst) */ public List readLastLines(String playerFilter, int maxLines) { - String date = DATE_FMT.get().format(new Date()); + String date = DATE_FMT.format(new Date()); File logFile = new File(logDir, "chatlog_" + date + ".log"); if (!logFile.exists()) return Collections.emptyList(); diff --git a/StatusAPI/src/main/java/net/viper/status/modules/chat/ReportManager.java b/StatusAPI/src/main/java/net/viper/status/modules/chat/ReportManager.java index 8b31815..73be0e3 100644 --- a/StatusAPI/src/main/java/net/viper/status/modules/chat/ReportManager.java +++ b/StatusAPI/src/main/java/net/viper/status/modules/chat/ReportManager.java @@ -31,9 +31,7 @@ public class ReportManager { /** Z\u00e4hler f\u00fcr Report-IDs. Wird beim Laden synchronisiert. */ private final AtomicInteger idCounter = new AtomicInteger(0); - // ThreadLocal: SimpleDateFormat ist NICHT thread-sicher. - private static final ThreadLocal DATE_FMT = - ThreadLocal.withInitial(() -> new SimpleDateFormat("dd.MM.yyyy HH:mm:ss")); + private static final SimpleDateFormat DATE_FMT = new SimpleDateFormat("dd.MM.yyyy HH:mm:ss"); // ===== Report-Datenklasse ===== @@ -50,7 +48,7 @@ public class ReportManager { public String closedBy; // Name des schlie\u00dfenden Admins (oder leer) public String getFormattedTime() { - return DATE_FMT.get().format(new Date(timestamp)); + return DATE_FMT.format(new Date(timestamp)); } } 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 adcd5d0..ba7552f 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 @@ -91,11 +91,7 @@ public class ScoreboardModule implements Module, Listener { private boolean nametagEnabled = true; // Spieler, f\u00fcr die bereits ein Nametag-Team gesetzt wurde (Teamname = "afk_" + player.getName() abgek\u00fcrzt) - // Pro Target: welche Viewer haben bereits ein CREATE-Packet bekommen. - // Fix: verhindert REMOVE ohne CREATE → "Player is either on another team" Crash. - private final ConcurrentHashMap> nametagViewers = new ConcurrentHashMap<>(); - // Dirty-Flag-Cache: letzter gesendeter Prefix pro Spieler (UUID → prefixStr) - private final ConcurrentHashMap nametagLastPrefix = new ConcurrentHashMap<>(); + private final Set nametagCreated = ConcurrentHashMap.newKeySet(); private int updateInterval = 500; // Millisekunden private int tickerSpeed = 1; private boolean rainbowEnabled = true; @@ -134,10 +130,8 @@ public class ScoreboardModule implements Module, Listener { private ScheduledTask updateTask; private ScheduledTask titleTask; private ScheduledTask newsTask; - // DateTimeFormatter ist thread-sicher (im Gegensatz zu SimpleDateFormat) - private java.time.format.DateTimeFormatter sdf; - private java.time.format.DateTimeFormatter sdfDate; - private java.time.ZoneId sdfZone = java.time.ZoneId.systemDefault(); + private java.text.SimpleDateFormat sdf; + private java.text.SimpleDateFormat sdfDate; private DecimalFormat df; private Method sendPkt; private boolean ready = false; @@ -216,7 +210,6 @@ public class ScoreboardModule implements Module, Listener { } created.clear(); createdAdmin.clear(); createdSupporter.clear(); tickerPos.clear(); rainbowIdx.clear(); hiddenPlayers.clear(); forceAdminView.clear(); forcePlayerView.clear(); forceSupporterView.clear(); newsPos.clear(); - nametagViewers.clear(); nametagLastPrefix.clear(); } @EventHandler @@ -236,11 +229,7 @@ public class ScoreboardModule implements Module, Listener { if (nametagEnabled) { ProxyServer.getInstance().getScheduler().schedule(plugin, () -> { // Alle bestehenden Spieler → neuer Spieler bekommt ihre Nametags - // Viewer-Eintrag des neuen Spielers aus allen anderen Target-Sets löschen - // damit sie beim nächsten updateNametag() CREATE (nicht UPDATE) bekommen. - nametagViewers.remove(id); - for (Set vs : nametagViewers.values()) { vs.remove(id); } - nametagLastPrefix.remove(id); + nametagCreated.remove(id); // Reset damit CREATE statt UPDATE gesendet wird for (ProxiedPlayer existing : ProxyServer.getInstance().getPlayers()) { if (existing.isConnected()) updateNametag(existing); } @@ -269,12 +258,8 @@ public class ScoreboardModule implements Module, Listener { playerX.remove(id); playerY.remove(id); playerZ.remove(id); playerWorld.remove(id); playerGamemode.remove(id); playerExp.remove(id); playerFood.remove(id); playerSpeed.remove(id); - joinTimes.remove(id); hiddenPlayers.remove(id); newsPos.remove(id); - ticketMyOpen.remove(id); // Memory-Leak-Fix: Ticket-Daten beim Logout bereinigen + joinTimes.remove(id); hiddenPlayers.remove(id); forceAdminView.remove(id); forcePlayerView.remove(id); newsPos.remove(id); // Nametag-Team f\u00fcr diesen Spieler bei allen anderen entfernen - nametagLastPrefix.remove(id); - // Viewer-Einträge dieses Spielers aus allen anderen Target-Sets entfernen - for (Set viewerSet : nametagViewers.values()) { viewerSet.remove(id); } if (nametagEnabled) removeNametag(e.getPlayer()); } @@ -357,9 +342,6 @@ public class ScoreboardModule implements Module, Listener { && (p.hasPermission(adminPermission) || forceAdminView.contains(id)); boolean isSupporter = !isAdmin && (forceSupporterView.contains(id) || (!forcePlayerView.contains(id) && p.hasPermission(supporterPermission))); Set activeCreated = isAdmin ? createdAdmin : isSupporter ? createdSupporter : created; - // BUG-FIX: UPDATE_TITLE (mode=2) darf nur gesendet werden wenn das Objective - // bereits mit CREATE (mode=0) angelegt wurde. Sonst: "Fehler im Netzwerkprotokoll". - if (!activeCreated.contains(id)) continue; try { int rIdx = (rainbowIdx.getOrDefault(id, 0) + 1) % 10000; rainbowIdx.put(id, rIdx); @@ -402,42 +384,28 @@ public class ScoreboardModule implements Module, Listener { /** * Sendet ein Team-Packet an alle online Spieler, das den Prefix - * über dem Kopf des 'target'-Spielers setzt. - * - * FIX: Pro-Viewer-Tracking via nametagViewers. - * Jeder Viewer bekommt beim ersten Mal CREATE (mode=0), danach UPDATE (mode=2). - * Nur wenn sich der Prefix geändert hat werden überhaupt Packets gesendet (Dirty-Flag). - * removeNametag() sendet REMOVE nur an Viewer die CREATE bekommen haben → kein Crash. + * \u00fcber dem Kopf des 'target'-Spielers setzt. + * AFK-Spieler bekommen §7[AFK] §r als Prefix, alle anderen ihren LuckPerms-Prefix. */ private void updateNametag(ProxiedPlayer target) { if (!ready || !target.isConnected()) return; try { - UUID id = target.getUniqueId(); - boolean isAfk = Boolean.TRUE.equals(net.viper.status.StatusAPI.playerAfk.get(id)); + boolean isAfk = Boolean.TRUE.equals(net.viper.status.StatusAPI.playerAfk.get(target.getUniqueId())); String lpPrefix = getLpPrefix(target); + // Teamname: "nt_" + erste 13 Zeichen des Playernamens (max 16 Zeichen insgesamt) String teamName = "nt_" + target.getName().substring(0, Math.min(13, target.getName().length())); + String prefixStr = isAfk ? "§7[AFK] §r" : (lpPrefix.isEmpty() ? "" : lpPrefix + "§r "); - String lastPrefix = nametagLastPrefix.get(id); - boolean prefixChanged = !prefixStr.equals(lastPrefix); - - // Viewer-Set für diesen Target holen (oder neu anlegen) - Set viewers = nametagViewers.computeIfAbsent(id, k -> ConcurrentHashMap.newKeySet()); - - // Nur senden wenn Prefix sich geändert hat ODER es neue Viewer gibt + // Packet an alle Online-Spieler senden (damit alle den ge\u00e4nderten Prefix sehen) for (ProxiedPlayer viewer : ProxyServer.getInstance().getPlayers()) { if (!viewer.isConnected()) continue; - UUID vid = viewer.getUniqueId(); - boolean viewerIsNew = !viewers.contains(vid); - - // Kein Update nötig wenn Prefix gleich und Viewer schon CREATE hatte - if (!prefixChanged && !viewerIsNew) continue; - try { Team team = new Team(); team.setName(teamName); - team.setMode(viewerIsNew ? (byte) 0 : (byte) 2); // 0=CREATE, 2=UPDATE + boolean firstTime = !nametagCreated.contains(target.getUniqueId()); + team.setMode(firstTime ? (byte) 0 : (byte) 2); // 0=CREATE, 2=UPDATE net.md_5.bungee.api.chat.TextComponent pfxComp = new net.md_5.bungee.api.chat.TextComponent(""); for (net.md_5.bungee.api.chat.BaseComponent bc : buildComponents(c(prefixStr))) @@ -449,14 +417,13 @@ public class ScoreboardModule implements Module, Listener { team.setCollisionRule(Either.right(CollisionRule.ALWAYS)); team.setColor(Optional.of(21)); // RESET team.setFriendlyFire((byte) 3); - if (viewerIsNew) team.setPlayers(new String[]{ target.getName() }); + if (firstTime) team.setPlayers(new String[]{ target.getName() }); sendPkt.invoke(viewer, team); - viewers.add(vid); // Viewer als "hat CREATE" markieren } catch (Exception ignored) {} } - nametagLastPrefix.put(id, prefixStr); + nametagCreated.add(target.getUniqueId()); } catch (Exception e) { - plugin.getLogger().warning("[ScoreboardModule] Nametag-Fehler für " + target.getName() + ": " + e.getMessage()); + plugin.getLogger().warning("[ScoreboardModule] Nametag-Fehler f\u00fcr " + target.getName() + ": " + e.getMessage()); } } @@ -464,25 +431,17 @@ public class ScoreboardModule implements Module, Listener { * Entfernt das Nametag-Team beim Disconnect sauber vom Client aller Spieler. */ private void removeNametag(ProxiedPlayer target) { - UUID id = target.getUniqueId(); String teamName = "nt_" + target.getName().substring(0, Math.min(13, target.getName().length())); - // REMOVE nur an Viewer senden die auch CREATE erhalten haben (per-Viewer-Tracking). - // Sonst: "Player is either on another team or not on any team" → Client-Crash. - Set viewers = nametagViewers.remove(id); - if (viewers != null) { - for (UUID vid : viewers) { - ProxiedPlayer viewer = ProxyServer.getInstance().getPlayer(vid); - if (viewer == null || !viewer.isConnected()) continue; - if (vid.equals(id)) continue; // Den Target selbst überspringen - try { - Team team = new Team(); - team.setName(teamName); - team.setMode((byte) 1); // REMOVE - sendPkt.invoke(viewer, team); - } catch (Exception ignored) {} - } + for (ProxiedPlayer viewer : ProxyServer.getInstance().getPlayers()) { + if (!viewer.isConnected() || viewer.getUniqueId().equals(target.getUniqueId())) continue; + try { + Team team = new Team(); + team.setName(teamName); + team.setMode((byte) 1); // REMOVE + sendPkt.invoke(viewer, team); + } catch (Exception ignored) {} } - nametagLastPrefix.remove(id); + nametagCreated.remove(target.getUniqueId()); } /** @@ -561,8 +520,8 @@ public class ScoreboardModule implements Module, Listener { String maxpl = rawLimit > 0 ? String.valueOf(rawLimit) : "∞"; String tps = isAdmin ? getTps(id) : ""; String ram = isAdmin ? getRam() : ""; - String time = sdf.format(java.time.LocalDateTime.now(sdfZone)); - String date = sdfDate.format(java.time.LocalDateTime.now(sdfZone)); + String time = sdf.format(new Date()); + String date = sdfDate.format(new Date()); String playtime = formatPlaytime(id); // Neue Placeholders String xCoord = String.valueOf(playerX.getOrDefault(id, 0)); @@ -1883,13 +1842,13 @@ public class ScoreboardModule implements Module, Listener { decimalSeparator = g.apply("scoreboard.money_decimal_separator",","); separator = g.apply("scoreboard.separator", "&8&m--------------------"); try { - sdfZone = java.time.ZoneId.of(timeZone); - sdf = java.time.format.DateTimeFormatter.ofPattern(timeFormat).withZone(sdfZone); - sdfDate = java.time.format.DateTimeFormatter.ofPattern(dateFormat).withZone(sdfZone); + sdf = new java.text.SimpleDateFormat(timeFormat); + sdf.setTimeZone(java.util.TimeZone.getTimeZone(timeZone)); + sdfDate = new java.text.SimpleDateFormat(dateFormat); + sdfDate.setTimeZone(java.util.TimeZone.getTimeZone(timeZone)); } catch (Exception e) { - sdfZone = java.time.ZoneId.systemDefault(); - sdf = java.time.format.DateTimeFormatter.ofPattern("HH:mm").withZone(sdfZone); - sdfDate = java.time.format.DateTimeFormatter.ofPattern("dd.MM.yyyy").withZone(sdfZone); + sdf = new java.text.SimpleDateFormat("HH:mm"); + sdfDate = new java.text.SimpleDateFormat("dd.MM.yyyy"); } try { df = new DecimalFormat(moneyFormat); diff --git a/StatusAPI/src/main/java/net/viper/status/stats/StatsStorage.java b/StatusAPI/src/main/java/net/viper/status/stats/StatsStorage.java index 3f9d6a9..7913271 100644 --- a/StatusAPI/src/main/java/net/viper/status/stats/StatsStorage.java +++ b/StatusAPI/src/main/java/net/viper/status/stats/StatsStorage.java @@ -1,8 +1,6 @@ package net.viper.status.stats; import java.io.*; -import java.nio.file.Files; -import java.nio.file.StandardCopyOption; /** * Fix #9: save() und load() sind jetzt synchronized um Race Conditions @@ -19,21 +17,12 @@ public class StatsStorage { public void save(StatsManager manager) { synchronized (fileLock) { - // Atomares Schreiben: erst in .tmp, dann umbenennen. - // Verhindert korrupte stats.dat bei Server-Absturz während des Schreibvorgangs. - File tmp = new File(file.getParentFile(), "stats.dat.tmp"); - try (BufferedWriter bw = new BufferedWriter(new FileWriter(tmp))) { + try (BufferedWriter bw = new BufferedWriter(new FileWriter(file))) { for (PlayerStats ps : manager.all()) { bw.write(ps.toLine()); bw.newLine(); } bw.flush(); - } catch (IOException e) { - e.printStackTrace(); - return; // Nicht umbenennen bei Fehler - } - try { - Files.move(tmp.toPath(), file.toPath(), StandardCopyOption.REPLACE_EXISTING, StandardCopyOption.ATOMIC_MOVE); } catch (IOException e) { e.printStackTrace(); } diff --git a/StatusAPI/src/main/resources/messages.txt b/StatusAPI/src/main/resources/messages.txt index 7277ccf..8c1f95a 100644 --- a/StatusAPI/src/main/resources/messages.txt +++ b/StatusAPI/src/main/resources/messages.txt @@ -1,52 +1,18 @@ -§8[%gradient:&b:&f:&b:&lViper-Network%§8] §7Der Server läuft 24/7 – also keine Hektik beim Spielen :) -§8[%gradient:&b:&f:&b:&lViper-Network%§8] §7Dies ist ein privater Server – hier zählt der Zusammenhalt. -§8[%gradient:&b:&f:&b:&lTipp%§8] §7Wenn du denkst, du bist sicher… schau nochmal nach. Creeper machen keine Geräusche beim Tippen. -§8[%gradient:&b:&f:&b:&lViper-Network%§8] §7Wähle einen Server, leg los – der Rest ergibt sich. Oder explodiert. -§8[%gradient:&b:&f:&b:&lViper-Network%§8] §7Mehr Server. Mehr Blöcke. Mehr Unfälle. Willkommen! -§8[%gradient:&b:&f:&b:&lTipp%§8] §7Halte eine Spitzhacke mit Glück bereit. Man weiß nie, wann das nächste Erz kommt. -§8[%gradient:&b:&f:&b:&lTipp%§8] §7Dein Bett ist dein Ankerpunkt – platziere es weise! -§8[%gradient:&b:&f:&b:&lViper-Network%§8] §7Das wichtigste Plugin? Du selbst. Spiel fair, sei kreativ! -§8[%gradient:&b:&f:&b:&lViper-Network%§8] §7Redstone ist keine Magie – aber fast. -§8[%gradient:&b:&f:&b:&lTipp%§8] §7Schilde sind cool. Besonders wenn Skelette zielen. -§8[%gradient:&b:&f:&b:&lViper-Network%§8] §7Wenn du in Lava fällst, bist du nicht der Erste. Nur der Nächste. -§8[%gradient:&b:&f:&b:&lTipp%§8] §7Villager sind nicht dumm – nur sehr… eigen. -§8[%gradient:&b:&f:&b:&lViper-Network%§8] §7Bau groß, bau sicher – oder bau eine Treppe zur Nachbarschaftsklage. -§8[%gradient:&b:&f:&b:&lViper-Network%§8] §7Gras wächst. Spieler auch. Gib jedem eine Chance! -§8[%gradient:&b:&f:&b:&lViper-Network%§8] §7Ein Creeper ist keine Begrüßung. Es sei denn, du willst es spannend machen. -§8[%gradient:&b:&f:&b:&lTipp%§8] §7Ein voller Magen ist halbe Miete. Farmen lohnt sich! -§8[%gradient:&b:&f:&b:&lViper-Network%§8] §7Wir haben keine Probleme – nur Redstone-Schaltungen mit Charakter. -§8[%gradient:&b:&f:&b:&lTipp%§8] §7Markiere dein Grundstück frühzeitig, bevor es jemand anderes tut! -§8[%gradient:&b:&f:&b:&lLobby%§8] §7Hier beginnt alles – such dir deinen Server aus und leg los! -§8[%gradient:&b:&f:&b:&lLobby%§8] §7Die Lobby ist kein Ziel, sondern der Start. Oder ein Treffpunkt. Oder beides. -§8[%gradient:&b:&f:&b:&lLobby%§8] §7Noch unentschlossen? Einfach mal reinschauen – auf allen Servern! -§8[%gradient:&b:&f:&b:&lCitybuild%§8] §7Auf Citybuild baust du deine eigene Stadt – Block für Block. -§8[%gradient:&b:&f:&b:&lCitybuild%§8] §7Grundstück sichern nicht vergessen – sonst baut jemand anderes drauf! -§8[%gradient:&b:&f:&b:&lCitybuild%§8] §7Gute Nachbarn bauen gute Städte. Oder zumindest hübschere. -§8[%gradient:&b:&f:&b:&lCitybuild%§8] §7Je größer die Stadt, desto mehr Redstone geht schief. Viel Erfolg! -§8[%gradient:&b:&f:&b:&lTipp%§8] §7Auf Citybuild kannst du dein Grundstück mit Freunden teilen – baut zusammen mehr! -§8[%gradient:&b:&f:&b:&lFreebuild%§8] §7Auf Freebuild gibt es keine Grenzen – außer deiner Fantasie. -§8[%gradient:&b:&f:&b:&lFreebuild%§8] §7Bauen ohne Regeln. Na ja, fast. Sei trotzdem nett zu deinen Nachbarn. -§8[%gradient:&b:&f:&b:&lFreebuild%§8] §7Riesige Projekte, verrückte Ideen – Freebuild macht's möglich. -§8[%gradient:&b:&f:&b:&lTipp%§8] §7Auf Freebuild gilt: Erst messen, dann bauen. Oder erst bauen und dann bereuen. -§8[%gradient:&b:&f:&b:&lSurvival%§8] §7Auf Survival zählt jeder Block – den du abbaust und den, der auf dich fällt. -§8[%gradient:&b:&f:&b:&lSurvival%§8] §7Tag 1: Holz hacken. Tag 2: Höhle finden. Tag 3: Creeper ärgert dich. Klassiker. -§8[%gradient:&b:&f:&b:&lSurvival%§8] §7Die Nether-Festung wartet. Bring Feuerresistenz mit – und Mut. -§8[%gradient:&b:&f:&b:&lSurvival%§8] §7Ein Bett in der Nähe spart lange Laufwege nach dem Tod. Spar dir den Fußmarsch! -§8[%gradient:&b:&f:&b:&lTipp%§8] §7Auf Survival lohnt sich ein Außenposten – Ressourcen gibt's nie genug. -§8[%gradient:&b:&f:&b:&lTipp%§8] §7Verzaubere deine Ausrüstung so früh wie möglich. Schutzverzauberung rettet Leben! -§8[%gradient:&b:&f:&b:&lSkyblock%§8] §7Auf Skyblock fängst du mit einer Insel an. Was du draus machst, liegt bei dir. -§8[%gradient:&b:&f:&b:&lSkyblock%§8] §7Wasser + Lava = Kobblestone. Das Fundament jeder Skyblock-Karriere. -§8[%gradient:&b:&f:&b:&lSkyblock%§8] §7Deine Insel, deine Regeln – aber Creeper kennen keine Regeln. Und keine Ränder. -§8[%gradient:&b:&f:&b:&lSkyblock%§8] §7Tipp vom Profi: Nie rückwärts auf einer Skyblock-Insel laufen. -§8[%gradient:&b:&f:&b:&lTipp%§8] §7Auf Skyblock ist ein Kompostierer Gold wert – Essen ist alles! -§8[%gradient:&b:&f:&b:&lTipp%§8] §7Baue deine Skyblock-Insel nach außen aus, bevor du in die Höhe gehst. -§8[%gradient:&b:&f:&b:&lMinigames%§8] §7Auf Minigames zählt Schnelligkeit, Köpfchen – und manchmal einfach Glück. -§8[%gradient:&b:&f:&b:&lMinigames%§8] §7Gewonnen oder verloren – beim nächsten Spiel ist alles wieder offen! -§8[%gradient:&b:&f:&b:&lMinigames%§8] §7Minigames sind der perfekte Ort, um Freunde zu finden. Oder Rivalen. -§8[%gradient:&b:&f:&b:&lMinigames%§8] §7Kurze Runden, viel Spaß – einfach Minigames starten und loslegen! -§8[%gradient:&b:&f:&b:&lViper-Network%§8] §7Sechs Server, ein Netzwerk – such dir deinen Lieblingsplatz! -§8[%gradient:&b:&f:&b:&lViper-Network%§8] §7Egal ob Bauen, Überleben oder Kämpfen – hier ist für jeden was dabei. -§8[%gradient:&b:&f:&b:&lViper-Network%§8] §7Neue Spieler sind immer willkommen. Zeig ihnen, wie es geht! -§8[%gradient:&b:&f:&b:&lViper-Network%§8] §7Respekt kostet nichts – macht aber den Server für alle besser. -§8[%gradient:&b:&f:&b:&lViper-Network%§8] §7Probleme oder Fragen? Wende dich ans Team – wir helfen gerne! -§8[%gradient:&b:&f:&b:&lTipp%§8] §7Du kannst zwischen allen Servern wechseln – einfach den Kompass nutzen! \ No newline at end of file +§8[§2Viper-Netzwerk§8] §7Der Server läuft 24/7 – also keine Hektik beim Spielen :) +§8[§2Viper-Netzwerk§8] §7Dies ist ein privater Server – hier zählt der Zusammenhalt. +§8[§dTipp§8] §7Wenn du denkst, du bist sicher… schau nochmal nach. Creeper machen keine Geräusche beim Tippen. +§8[§2Viper-Netzwerk§8] §7Wähle einen Server, leg los – der Rest ergibt sich. Oder explodiert. +§8[§2Viper-Netzwerk§8] §7Mehr Server. Mehr Blöcke. Mehr Unfälle. Willkommen! +§8[§dTipp§8] §7Halte eine Spitzhacke mit Glück bereit. Man weiß nie, wann das nächste Erz kommt. +§8[§dTipp§8] §7Mit §e/home§7 kannst du dich jederzeit nach Hause teleportieren. +§8[§2Viper-Netzwerk§8] §7Das wichtigste Plugin? Du selbst. Spiel fair, sei kreativ! +§8[§2Viper-Netzwerk§8] §7Redstone ist keine Magie – aber fast. +§8[§dTipp§8] §7Schilde sind cool. Besonders wenn Skelette zielen. +§8[§2Viper-Netzwerk§8] §7Wenn du in Lava fällst, bist du nicht der Erste. Nur der Nächste. +§8[§dTipp§8] §7Villager sind nicht dumm – nur sehr… eigen. +§8[§2Viper-Netzwerk§8] §7Bau groß, bau sicher – oder bau eine Treppe zur Nachbarschaftsklage. +§8[§2Viper-Netzwerk§8] §7Gras wächst. Spieler auch. Gib jedem eine Chance! +§8[§2Viper-Netzwerk§8] §7Ein Creeper ist keine Begrüßung. Es sei denn, du willst es spannend machen. +§8[§dTipp§8] §7Ein voller Magen ist halbe Miete. Farmen lohnt sich! +§8[§2Viper-Netzwerk§8] §7Wir haben keine Probleme – nur Redstone-Schaltungen mit Charakter. +§8[§dTipp§8] §7Markiere dein Grundstück mit §e/p claim§7, bevor es jemand anderes tut! \ No newline at end of file