Upload via GUI (5 Dateien)
This commit is contained in:
@@ -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);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user