From dcf9b940fd886114beee423bdd5505a031817576 Mon Sep 17 00:00:00 2001 From: M_Viper Date: Fri, 13 Nov 2026 12:08:41 +0000 Subject: [PATCH] Upload via GUI (5 Dateien) --- .../statusapibridge/StatusAPIBridge.java | 87 +++++++++++++++---- .../statusapibridge/util/ConfigMigrator.java | 85 ++++++++++++++++++ StatusAPIBridge/src/main/resources/config.yml | 4 + 3 files changed, 158 insertions(+), 18 deletions(-) create mode 100644 StatusAPIBridge/src/main/java/net/viper/statusapibridge/util/ConfigMigrator.java diff --git a/StatusAPIBridge/src/main/java/net/viper/statusapibridge/StatusAPIBridge.java b/StatusAPIBridge/src/main/java/net/viper/statusapibridge/StatusAPIBridge.java index 3c21d5f..4d97694 100644 --- a/StatusAPIBridge/src/main/java/net/viper/statusapibridge/StatusAPIBridge.java +++ b/StatusAPIBridge/src/main/java/net/viper/statusapibridge/StatusAPIBridge.java @@ -20,6 +20,8 @@ import java.io.OutputStream; import java.net.HttpURLConnection; import java.net.URL; import java.nio.charset.StandardCharsets; +import java.util.ArrayList; +import java.util.List; import java.util.Map; import java.util.Set; import java.util.UUID; @@ -34,6 +36,7 @@ public class StatusAPIBridge extends JavaPlugin implements Listener, PluginMessa private Economy economy; private String statusApiUrl; + private String apiKey = ""; private int pushDelayTicks; private int liveSyncIntervalTicks; private int scoreboardSyncIntervalTicks; @@ -68,10 +71,11 @@ public class StatusAPIBridge extends JavaPlugin implements Listener, PluginMessa // false = 26.1.x-Modus (neuere Server-Version, kein NMS-Fallback) private boolean isLegacyMode = true; - // BUGFIX: DiscardOldestPolicy kann HTTP-Requests silently verwerfen obwohl lastPushed* schon - // aktualisiert wurde → der nächste Sync-Zyklus schickt keinen erneuten Push mehr. - // CallerRunsPolicy blockiert stattdessen den aufrufenden Thread, was verhindert dass - // Economy/Stats-Updates verloren gehen. Queue-Größe auf 2000 reduziert (war 4000, unnötig groß). + // WICHTIG: Niemals CallerRunsPolicy verwenden – bei voller Queue (z.B. StatusAPI langsam/nicht erreichbar) + // wuerde der HTTP-Request (bis 3 s Timeout) auf dem Server-Hauptthread laufen → starker Lag. + // Stattdessen wird der aelteste Push verworfen ("letzter Stand gewinnt"). Verlorene Updates + // werden durch den vollstaendigen Resync alle 30 s (siehe onEnable) automatisch nachgeholt. + private long lastHttpWarnAt = 0L; private final ExecutorService httpExecutor = new ThreadPoolExecutor( 2, 2, @@ -83,12 +87,20 @@ public class StatusAPIBridge extends JavaPlugin implements Listener, PluginMessa t.setDaemon(true); return t; }, - new ThreadPoolExecutor.CallerRunsPolicy() + new ThreadPoolExecutor.DiscardOldestPolicy() ); @Override public void onEnable() { saveDefaultConfig(); + // Bestehende config.yml um neue Optionen ergaenzen (vorhandene Werte bleiben unveraendert) + if (net.viper.statusapibridge.util.ConfigMigrator.migrateYamlKey( + new java.io.File(getDataFolder(), "config.yml"), "api-key", + "# API-Key fuer die StatusAPI (muss identisch zu api.write_key in network-guard.properties sein).\n" + + "# Leer lassen, wenn dort kein Key gesetzt ist.\napi-key: \"\"\n", + getLogger())) { + reloadConfig(); + } detectMinecraftVersion(); nametagEnabled = getConfig().getBoolean("nametag-enabled", true); if (nametagEnabled) { @@ -97,6 +109,7 @@ public class StatusAPIBridge extends JavaPlugin implements Listener, PluginMessa getLogger().info("Nametag-Prefix aktiviert (LuckPerms)."); } statusApiUrl = getConfig().getString("statusapi-url", "http://127.0.0.1:9191").trim(); + apiKey = getConfig().getString("api-key", "").trim(); pushDelayTicks = getConfig().getInt("push-delay-ticks", 40); liveSyncIntervalTicks = Math.max(20, getConfig().getInt("live-sync-interval-ticks", 20)); scoreboardSyncIntervalTicks = Math.max(20, getConfig().getInt("scoreboard-sync-interval-ticks", 20)); @@ -116,6 +129,16 @@ public class StatusAPIBridge extends JavaPlugin implements Listener, PluginMessa Bukkit.getScheduler().runTaskTimer(this, this::pushScoreboardData, scoreboardSyncIntervalTicks, scoreboardSyncIntervalTicks); + // Vollstaendiger Resync alle 30 s: "zuletzt gesendet"-Caches leeren, damit ALLE Werte neu gepusht werden. + // Behebt falsche/alte Anzeigen nach StatusAPI-/Proxy-Neustart, verworfenen oder abgelehnten Requests. + Bukkit.getScheduler().runTaskTimer(this, () -> { + lastPushedBalance.clear(); + lastPushedHealth.clear(); + lastPushedWorld.clear(); + lastPushedData.clear(); + lastPushedStats.clear(); + }, 600L, 600L); + // TicketSystem-Daten alle 5 Sekunden pushen (100 Ticks) Bukkit.getScheduler().runTaskTimerAsynchronously(this, this::pushTicketData, 100L, 100L); @@ -178,7 +201,8 @@ public class StatusAPIBridge extends JavaPlugin implements Listener, PluginMessa public void onQuit(PlayerQuitEvent e) { Player player = e.getPlayer(); UUID id = player.getUniqueId(); - if (economy != null) pushEconomyAsync(id, player.getName(), economy.getBalance(player)); + // Kein Balance-Push beim Quit: er kam bei Server-Wechseln oft NACH dem Join-Push des neuen + // Servers an und ueberschrieb den aktuellen Kontostand mit einem alten Wert. lastPushedBalance.remove(id); lastPushedHealth.remove(id); lastPushedCompass.remove(id); @@ -218,12 +242,20 @@ public class StatusAPIBridge extends JavaPlugin implements Listener, PluginMessa private void pushChangedBalancesForOnlinePlayers() { if (economy == null) return; - for (Player player : Bukkit.getOnlinePlayers()) { - double current = economy.getBalance(player); - Double last = lastPushedBalance.get(player.getUniqueId()); - if (last == null || Math.abs(current - last) > 0.000001d) - pushEconomyAsync(player.getUniqueId(), player.getName(), current); - } + // Spielerliste auf dem Hauptthread kopieren, Kontostaende ABER asynchron abfragen: + // Vault/NexEco liest aus der Datenbank – auf dem Hauptthread blockiert das den ganzen Server. + final List snapshot = new ArrayList<>(Bukkit.getOnlinePlayers()); + if (snapshot.isEmpty()) return; + Bukkit.getScheduler().runTaskAsynchronously(this, () -> { + for (Player player : snapshot) { + double current; + try { current = economy.getBalance(player); } + catch (Exception e) { continue; } + Double last = lastPushedBalance.get(player.getUniqueId()); + if (last == null || Math.abs(current - last) > 0.000001d) + pushEconomyAsync(player.getUniqueId(), player.getName(), current); + } + }); } private void pushScoreboardData() { @@ -248,16 +280,23 @@ public class StatusAPIBridge extends JavaPlugin implements Listener, PluginMessa // ── Push-Methoden ───────────────────────────────────────────────────────── public void pushEconomy(Player player) { - pushEconomyAsync(player.getUniqueId(), player.getName(), economy.getBalance(player)); + final UUID id = player.getUniqueId(); + final String name = player.getName(); + Bukkit.getScheduler().runTaskAsynchronously(this, () -> { + double bal; + try { bal = economy.getBalance(player); } catch (Exception e) { return; } + pushEconomyAsync(id, name, bal); + }); } private void pushEconomyAsync(UUID uuid, String name, double balance) { submitHttp(() -> { try { - sendPost(statusApiUrl + "/economy/update", + boolean ok = sendPost(statusApiUrl + "/economy/update", "{\"uuid\":\"" + uuid + "\",\"name\":\"" + escapeName(name) + "\",\"balance\":" + balance + "}"); - lastPushedBalance.put(uuid, balance); + // Nur als "gesendet" merken, wenn die StatusAPI den Wert wirklich akzeptiert hat + if (ok) lastPushedBalance.put(uuid, balance); } catch (Exception e) { getLogger().warning("Economy-Push fehlgeschlagen fuer " + name + ": " + e.getMessage()); } @@ -739,7 +778,8 @@ public class StatusAPIBridge extends JavaPlugin implements Listener, PluginMessa } } - private void sendPost(String urlStr, String json) throws Exception { + /** @return true bei HTTP 2xx, sonst false (Warnung max. alle 30 s, damit das Log nicht ueberlaeuft) */ + private boolean sendPost(String urlStr, String json) throws Exception { @SuppressWarnings("deprecation") URL url = new java.net.URI(urlStr).toURL(); HttpURLConnection conn = (HttpURLConnection) url.openConnection(); @@ -748,13 +788,24 @@ public class StatusAPIBridge extends JavaPlugin implements Listener, PluginMessa conn.setConnectTimeout(3000); conn.setReadTimeout(3000); conn.setRequestProperty("Content-Type", "application/json; charset=UTF-8"); + if (!apiKey.isEmpty()) conn.setRequestProperty("X-API-Key", apiKey); byte[] body = json.getBytes(StandardCharsets.UTF_8); conn.setRequestProperty("Content-Length", String.valueOf(body.length)); try (OutputStream os = conn.getOutputStream()) { os.write(body); } int code = conn.getResponseCode(); - if (code != 200) - getLogger().warning("StatusAPI antwortete mit Code " + code + " fuer " + urlStr); conn.disconnect(); + if (code >= 200 && code < 300) return true; + long now = System.currentTimeMillis(); + if (now - lastHttpWarnAt > 30_000L) { + lastHttpWarnAt = now; + if (code == 403) + getLogger().warning("StatusAPI lehnt Anfrage ab (403): api-key in der Bridge-config.yml pruefen (api.write_key der StatusAPI). URL: " + urlStr); + else if (code == 429) + getLogger().warning("StatusAPI-Rate-Limit erreicht (429): api.ratelimit.* in network-guard.properties erhoehen oder api-key setzen. URL: " + urlStr); + else + getLogger().warning("StatusAPI antwortete mit Code " + code + " fuer " + urlStr); + } + return false; } private String escapeName(String name) { diff --git a/StatusAPIBridge/src/main/java/net/viper/statusapibridge/util/ConfigMigrator.java b/StatusAPIBridge/src/main/java/net/viper/statusapibridge/util/ConfigMigrator.java new file mode 100644 index 0000000..b851a44 --- /dev/null +++ b/StatusAPIBridge/src/main/java/net/viper/statusapibridge/util/ConfigMigrator.java @@ -0,0 +1,85 @@ +package net.viper.statusapibridge.util; + +import java.io.File; +import java.io.IOException; +import java.io.InputStreamReader; +import java.io.FileInputStream; +import java.io.Reader; +import java.nio.charset.StandardCharsets; +import java.nio.file.Files; +import java.nio.file.StandardOpenOption; +import java.util.Properties; +import java.util.logging.Logger; + +/** + * Ergaenzt fehlende Schluessel in bestehenden Konfigurationsdateien, ohne vorhandene Werte, + * Kommentare oder die Reihenfolge zu veraendern. Neue Optionen erscheinen so nach einem + * Plugin-Update automatisch (mit Kommentar) in der Datei. + * + * Existiert die Datei nicht, passiert nichts (sie wird vom Plugin mit allen Standardwerten erzeugt). + */ +public final class ConfigMigrator { + + private ConfigMigrator() {} + + /** + * .properties-Datei (UTF-8). entries: je {schluessel, standardwert, kommentar}; der Kommentar darf + * mehrere Zeilen ("\n") enthalten. Gibt die Anzahl ergaenzter Schluessel zurueck. + */ + public static int migrateProperties(File file, String[][] entries, Logger log) { + if (file == null || !file.isFile()) return 0; + try { + Properties existing = new Properties(); + try (Reader r = new InputStreamReader(new FileInputStream(file), StandardCharsets.UTF_8)) { + existing.load(r); + } + StringBuilder add = new StringBuilder(); + int count = 0; + for (String[] e : entries) { + if (existing.containsKey(e[0])) continue; + if (count == 0) add.append("\n# -- Automatisch ergaenzt (neue Optionen nach Update) --\n"); + if (e.length > 2 && e[2] != null && !e[2].isEmpty()) { + for (String line : e[2].split("\n")) add.append("# ").append(line).append('\n'); + } + add.append(e[0]).append('=').append(e[1]).append("\n\n"); + count++; + } + if (count > 0) { + appendWithNewline(file, add.toString()); + if (log != null) log.info("[Config] " + file.getName() + ": " + count + " neue Option(en) ergaenzt."); + } + return count; + } catch (IOException ex) { + if (log != null) log.warning("[Config] Konnte " + file.getName() + " nicht aktualisieren: " + ex.getMessage()); + return 0; + } + } + + /** + * YAML-Datei: haengt block an, wenn der Top-Level-Schluessel {@code key} fehlt. + * @return true wenn ergaenzt wurde + */ + public static boolean migrateYamlKey(File file, String key, String block, Logger log) { + if (file == null || !file.isFile()) return false; + try { + String text = new String(Files.readAllBytes(file.toPath()), StandardCharsets.UTF_8); + if (java.util.regex.Pattern.compile("(?m)^" + java.util.regex.Pattern.quote(key) + "\\s*:").matcher(text).find()) { + return false; + } + appendWithNewline(file, "\n# -- Automatisch ergaenzt (neue Option nach Update) --\n" + block + "\n"); + if (log != null) log.info("[Config] " + file.getName() + ": Option '" + key + "' ergaenzt."); + return true; + } catch (IOException ex) { + if (log != null) log.warning("[Config] Konnte " + file.getName() + " nicht aktualisieren: " + ex.getMessage()); + return false; + } + } + + private static void appendWithNewline(File file, String text) throws IOException { + byte[] existing = Files.readAllBytes(file.toPath()); + StringBuilder sb = new StringBuilder(); + if (existing.length > 0 && existing[existing.length - 1] != '\n') sb.append('\n'); + sb.append(text); + Files.write(file.toPath(), sb.toString().getBytes(StandardCharsets.UTF_8), StandardOpenOption.APPEND); + } +} diff --git a/StatusAPIBridge/src/main/resources/config.yml b/StatusAPIBridge/src/main/resources/config.yml index 825c675..d4affd9 100644 --- a/StatusAPIBridge/src/main/resources/config.yml +++ b/StatusAPIBridge/src/main/resources/config.yml @@ -1,6 +1,10 @@ # URL der BungeeCord StatusAPI (kein Slash am Ende) statusapi-url: "http://127.0.0.1:9191" +# API-Key fuer die StatusAPI (muss identisch zu api.write_key in network-guard.properties sein). +# Leer lassen, wenn dort kein Key gesetzt ist. +api-key: "" + # Wie viele Ticks nach dem Join wird die Balance gepusht? (20 Ticks = 1 Sekunde) push-delay-ticks: 40