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.HttpURLConnection;
|
||||||
import java.net.URL;
|
import java.net.URL;
|
||||||
import java.nio.charset.StandardCharsets;
|
import java.nio.charset.StandardCharsets;
|
||||||
|
import java.util.ArrayList;
|
||||||
|
import java.util.List;
|
||||||
import java.util.Map;
|
import java.util.Map;
|
||||||
import java.util.Set;
|
import java.util.Set;
|
||||||
import java.util.UUID;
|
import java.util.UUID;
|
||||||
@@ -34,6 +36,7 @@ public class StatusAPIBridge extends JavaPlugin implements Listener, PluginMessa
|
|||||||
|
|
||||||
private Economy economy;
|
private Economy economy;
|
||||||
private String statusApiUrl;
|
private String statusApiUrl;
|
||||||
|
private String apiKey = "";
|
||||||
private int pushDelayTicks;
|
private int pushDelayTicks;
|
||||||
private int liveSyncIntervalTicks;
|
private int liveSyncIntervalTicks;
|
||||||
private int scoreboardSyncIntervalTicks;
|
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)
|
// false = 26.1.x-Modus (neuere Server-Version, kein NMS-Fallback)
|
||||||
private boolean isLegacyMode = true;
|
private boolean isLegacyMode = true;
|
||||||
|
|
||||||
// BUGFIX: DiscardOldestPolicy kann HTTP-Requests silently verwerfen obwohl lastPushed* schon
|
// WICHTIG: Niemals CallerRunsPolicy verwenden – bei voller Queue (z.B. StatusAPI langsam/nicht erreichbar)
|
||||||
// aktualisiert wurde → der nächste Sync-Zyklus schickt keinen erneuten Push mehr.
|
// wuerde der HTTP-Request (bis 3 s Timeout) auf dem Server-Hauptthread laufen → starker Lag.
|
||||||
// CallerRunsPolicy blockiert stattdessen den aufrufenden Thread, was verhindert dass
|
// Stattdessen wird der aelteste Push verworfen ("letzter Stand gewinnt"). Verlorene Updates
|
||||||
// Economy/Stats-Updates verloren gehen. Queue-Größe auf 2000 reduziert (war 4000, unnötig groß).
|
// werden durch den vollstaendigen Resync alle 30 s (siehe onEnable) automatisch nachgeholt.
|
||||||
|
private long lastHttpWarnAt = 0L;
|
||||||
private final ExecutorService httpExecutor = new ThreadPoolExecutor(
|
private final ExecutorService httpExecutor = new ThreadPoolExecutor(
|
||||||
2,
|
2,
|
||||||
2,
|
2,
|
||||||
@@ -83,12 +87,20 @@ public class StatusAPIBridge extends JavaPlugin implements Listener, PluginMessa
|
|||||||
t.setDaemon(true);
|
t.setDaemon(true);
|
||||||
return t;
|
return t;
|
||||||
},
|
},
|
||||||
new ThreadPoolExecutor.CallerRunsPolicy()
|
new ThreadPoolExecutor.DiscardOldestPolicy()
|
||||||
);
|
);
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void onEnable() {
|
public void onEnable() {
|
||||||
saveDefaultConfig();
|
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();
|
detectMinecraftVersion();
|
||||||
nametagEnabled = getConfig().getBoolean("nametag-enabled", true);
|
nametagEnabled = getConfig().getBoolean("nametag-enabled", true);
|
||||||
if (nametagEnabled) {
|
if (nametagEnabled) {
|
||||||
@@ -97,6 +109,7 @@ public class StatusAPIBridge extends JavaPlugin implements Listener, PluginMessa
|
|||||||
getLogger().info("Nametag-Prefix aktiviert (LuckPerms).");
|
getLogger().info("Nametag-Prefix aktiviert (LuckPerms).");
|
||||||
}
|
}
|
||||||
statusApiUrl = getConfig().getString("statusapi-url", "http://127.0.0.1:9191").trim();
|
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);
|
pushDelayTicks = getConfig().getInt("push-delay-ticks", 40);
|
||||||
liveSyncIntervalTicks = Math.max(20, getConfig().getInt("live-sync-interval-ticks", 20));
|
liveSyncIntervalTicks = Math.max(20, getConfig().getInt("live-sync-interval-ticks", 20));
|
||||||
scoreboardSyncIntervalTicks = Math.max(20, getConfig().getInt("scoreboard-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,
|
Bukkit.getScheduler().runTaskTimer(this, this::pushScoreboardData,
|
||||||
scoreboardSyncIntervalTicks, scoreboardSyncIntervalTicks);
|
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)
|
// TicketSystem-Daten alle 5 Sekunden pushen (100 Ticks)
|
||||||
Bukkit.getScheduler().runTaskTimerAsynchronously(this, this::pushTicketData, 100L, 100L);
|
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) {
|
public void onQuit(PlayerQuitEvent e) {
|
||||||
Player player = e.getPlayer();
|
Player player = e.getPlayer();
|
||||||
UUID id = player.getUniqueId();
|
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);
|
lastPushedBalance.remove(id);
|
||||||
lastPushedHealth.remove(id);
|
lastPushedHealth.remove(id);
|
||||||
lastPushedCompass.remove(id);
|
lastPushedCompass.remove(id);
|
||||||
@@ -218,12 +242,20 @@ public class StatusAPIBridge extends JavaPlugin implements Listener, PluginMessa
|
|||||||
|
|
||||||
private void pushChangedBalancesForOnlinePlayers() {
|
private void pushChangedBalancesForOnlinePlayers() {
|
||||||
if (economy == null) return;
|
if (economy == null) return;
|
||||||
for (Player player : Bukkit.getOnlinePlayers()) {
|
// Spielerliste auf dem Hauptthread kopieren, Kontostaende ABER asynchron abfragen:
|
||||||
double current = economy.getBalance(player);
|
// 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());
|
Double last = lastPushedBalance.get(player.getUniqueId());
|
||||||
if (last == null || Math.abs(current - last) > 0.000001d)
|
if (last == null || Math.abs(current - last) > 0.000001d)
|
||||||
pushEconomyAsync(player.getUniqueId(), player.getName(), current);
|
pushEconomyAsync(player.getUniqueId(), player.getName(), current);
|
||||||
}
|
}
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
private void pushScoreboardData() {
|
private void pushScoreboardData() {
|
||||||
@@ -248,16 +280,23 @@ public class StatusAPIBridge extends JavaPlugin implements Listener, PluginMessa
|
|||||||
// ── Push-Methoden ─────────────────────────────────────────────────────────
|
// ── Push-Methoden ─────────────────────────────────────────────────────────
|
||||||
|
|
||||||
public void pushEconomy(Player player) {
|
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) {
|
private void pushEconomyAsync(UUID uuid, String name, double balance) {
|
||||||
submitHttp(() -> {
|
submitHttp(() -> {
|
||||||
try {
|
try {
|
||||||
sendPost(statusApiUrl + "/economy/update",
|
boolean ok = sendPost(statusApiUrl + "/economy/update",
|
||||||
"{\"uuid\":\"" + uuid + "\",\"name\":\"" + escapeName(name)
|
"{\"uuid\":\"" + uuid + "\",\"name\":\"" + escapeName(name)
|
||||||
+ "\",\"balance\":" + balance + "}");
|
+ "\",\"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) {
|
} catch (Exception e) {
|
||||||
getLogger().warning("Economy-Push fehlgeschlagen fuer " + name + ": " + e.getMessage());
|
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")
|
@SuppressWarnings("deprecation")
|
||||||
URL url = new java.net.URI(urlStr).toURL();
|
URL url = new java.net.URI(urlStr).toURL();
|
||||||
HttpURLConnection conn = (HttpURLConnection) url.openConnection();
|
HttpURLConnection conn = (HttpURLConnection) url.openConnection();
|
||||||
@@ -748,13 +788,24 @@ public class StatusAPIBridge extends JavaPlugin implements Listener, PluginMessa
|
|||||||
conn.setConnectTimeout(3000);
|
conn.setConnectTimeout(3000);
|
||||||
conn.setReadTimeout(3000);
|
conn.setReadTimeout(3000);
|
||||||
conn.setRequestProperty("Content-Type", "application/json; charset=UTF-8");
|
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);
|
byte[] body = json.getBytes(StandardCharsets.UTF_8);
|
||||||
conn.setRequestProperty("Content-Length", String.valueOf(body.length));
|
conn.setRequestProperty("Content-Length", String.valueOf(body.length));
|
||||||
try (OutputStream os = conn.getOutputStream()) { os.write(body); }
|
try (OutputStream os = conn.getOutputStream()) { os.write(body); }
|
||||||
int code = conn.getResponseCode();
|
int code = conn.getResponseCode();
|
||||||
if (code != 200)
|
|
||||||
getLogger().warning("StatusAPI antwortete mit Code " + code + " fuer " + urlStr);
|
|
||||||
conn.disconnect();
|
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) {
|
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);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,6 +1,10 @@
|
|||||||
# URL der BungeeCord StatusAPI (kein Slash am Ende)
|
# URL der BungeeCord StatusAPI (kein Slash am Ende)
|
||||||
statusapi-url: "http://127.0.0.1:9191"
|
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)
|
# Wie viele Ticks nach dem Join wird die Balance gepusht? (20 Ticks = 1 Sekunde)
|
||||||
push-delay-ticks: 40
|
push-delay-ticks: 40
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user