Upload via GUI (5 Dateien)

This commit is contained in:
2026-11-13 12:08:41 +00:00
parent 173e8001ff
commit dcf9b940fd
3 changed files with 158 additions and 18 deletions
@@ -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<Player> 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) {
@@ -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);
}
}