package net.viper.statusapibridge; import net.milkbowl.vault.economy.Economy; import org.bukkit.Bukkit; import org.bukkit.Location; import org.bukkit.entity.Player; import org.bukkit.event.EventHandler; import org.bukkit.event.EventPriority; import org.bukkit.event.Listener; import org.bukkit.event.entity.EntityRegainHealthEvent; import org.bukkit.event.entity.EntityDamageEvent; import org.bukkit.event.player.PlayerJoinEvent; import org.bukkit.event.player.PlayerMoveEvent; import org.bukkit.event.player.PlayerQuitEvent; import org.bukkit.plugin.RegisteredServiceProvider; import org.bukkit.plugin.java.JavaPlugin; import java.io.OutputStream; import java.net.HttpURLConnection; import java.net.URL; import java.nio.charset.StandardCharsets; import java.util.Map; import java.util.Set; import java.util.UUID; import java.util.concurrent.ConcurrentHashMap; import java.util.concurrent.ExecutorService; import java.util.concurrent.Executors; public class StatusAPIBridge extends JavaPlugin implements Listener { private Economy economy; private String statusApiUrl; private int pushDelayTicks; private int liveSyncIntervalTicks; private int scoreboardSyncIntervalTicks; private final Map lastPushedBalance = new ConcurrentHashMap<>(); private final Map lastPushedHealth = new ConcurrentHashMap<>(); private final Map lastPushedCompass = new ConcurrentHashMap<>(); private final Map lastPushedWorld = new ConcurrentHashMap<>(); private final Map lastPushedData = new ConcurrentHashMap<>(); private final Map lastPushedStats = new ConcurrentHashMap<>(); private String lastPushedTicketGlobal = ""; // ── PlaceholderAPI ──────────────────────────────────────────────────────── private final Set papiTokens = new java.util.LinkedHashSet<>(); private final Map lastPapiValues = new ConcurrentHashMap<>(); private boolean papiEnabled = false; // ── Nametag ─────────────────────────────────────────────────────────────── /** Scoreboard für Nametag-Teams (einmalig pro Server erstellt) */ private org.bukkit.scoreboard.Scoreboard nametagBoard = null; /** Zuletzt gesetzter Prefix pro Spieler (Change-Detection) */ private final Map lastNametagPrefix = new ConcurrentHashMap<>(); /** Feature aktivierbar via config: nametag-enabled */ private boolean nametagEnabled = true; // ── Versions-Detection ──────────────────────────────────────────────────── // true = 1.21.x-Modus (Spigot/Paper) // false = 26.1.x-Modus (neuere Server-Version, kein NMS-Fallback) private boolean isLegacyMode = true; private final ExecutorService httpExecutor = Executors.newSingleThreadExecutor(r -> { Thread t = new Thread(r, "StatusAPIBridge-HTTP"); t.setDaemon(true); return t; }); @Override public void onEnable() { saveDefaultConfig(); detectMinecraftVersion(); nametagEnabled = getConfig().getBoolean("nametag-enabled", true); if (nametagEnabled) { // Eigenes Scoreboard für Nametag-Teams erstellen nametagBoard = Bukkit.getScoreboardManager().getNewScoreboard(); getLogger().info("Nametag-Prefix aktiviert (LuckPerms)."); } statusApiUrl = getConfig().getString("statusapi-url", "http://127.0.0.1:9191").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)); if (!setupEconomy()) { getLogger().warning("Vault/Economy nicht gefunden – Economy-Push deaktiviert."); } else { getLogger().info("Vault Economy gefunden: " + economy.getName()); } Bukkit.getPluginManager().registerEvents(this, this); Bukkit.getScheduler().runTaskTimer(this, this::pushChangedBalancesForOnlinePlayers, liveSyncIntervalTicks, liveSyncIntervalTicks); Bukkit.getScheduler().runTaskTimer(this, this::pushScoreboardData, scoreboardSyncIntervalTicks, scoreboardSyncIntervalTicks); // TicketSystem-Daten alle 5 Sekunden pushen (100 Ticks) Bukkit.getScheduler().runTaskTimerAsynchronously(this, this::pushTicketData, 100L, 100L); // PlaceholderAPI-Integration papiEnabled = getServer().getPluginManager().getPlugin("PlaceholderAPI") != null; if (papiEnabled) { // Tokens alle 30s von StatusAPI holen, nur bei Änderung loggen Bukkit.getScheduler().runTaskTimerAsynchronously(this, () -> { Set before = new java.util.LinkedHashSet<>(papiTokens); boolean fetched = fetchPapiTokensFromStatusAPI(); if (fetched && !papiTokens.equals(before)) { if (papiTokens.isEmpty()) { getLogger().info("[PAPI] Keine Placeholder in der StatusAPI-Config gefunden."); } else { getLogger().info("[PAPI] " + papiTokens.size() + " Placeholder erkannt: " + papiTokens); } } }, 40L, 600L); // nach 2s starten, alle 30s wiederholen // Sync-Task läuft dauerhaft – tut nichts wenn papiTokens leer Bukkit.getScheduler().runTaskTimer(this, this::syncPapiValues, scoreboardSyncIntervalTicks, scoreboardSyncIntervalTicks); } else { getLogger().info("[PAPI] PlaceholderAPI nicht gefunden – Placeholder werden nicht aufgelöst."); } getLogger().info("StatusAPIBridge gestartet. Ziel: " + statusApiUrl); } @Override public void onDisable() { httpExecutor.shutdownNow(); } private boolean setupEconomy() { if (getServer().getPluginManager().getPlugin("Vault") == null) return false; RegisteredServiceProvider rsp = getServer().getServicesManager().getRegistration(Economy.class); if (rsp == null) return false; economy = rsp.getProvider(); return economy != null; } // ── Events ──────────────────────────────────────────────────────────────── @EventHandler(priority = EventPriority.MONITOR) public void onJoin(PlayerJoinEvent e) { Player player = e.getPlayer(); Bukkit.getScheduler().runTaskLater(this, () -> { if (!player.isOnline()) return; if (economy != null) pushEconomy(player); pushPlayerScoreboardData(player); if (papiEnabled && !papiTokens.isEmpty()) pushPapiValues(player); // Nametag: LuckPerms-Prefix über dem Kopf setzen if (nametagEnabled) applyNametag(player); }, pushDelayTicks); } @EventHandler(priority = EventPriority.MONITOR) public void onQuit(PlayerQuitEvent e) { Player player = e.getPlayer(); UUID id = player.getUniqueId(); if (economy != null) pushEconomyAsync(id, player.getName(), economy.getBalance(player)); lastPushedBalance.remove(id); lastPushedHealth.remove(id); lastPushedCompass.remove(id); lastPushedWorld.remove(id); lastPushedData.remove(id); lastPushedStats.remove(id); lastPapiValues.remove(id); lastNametagPrefix.remove(id); // Nametag-Team beim Quit aufräumen if (nametagEnabled) removeNametag(player); } @EventHandler(priority = EventPriority.MONITOR, ignoreCancelled = true) public void onDamage(EntityDamageEvent e) { if (!(e.getEntity() instanceof Player player)) return; Bukkit.getScheduler().runTaskLater(this, () -> { if (player.isOnline()) pushHealthIfChanged(player); }, 1L); } @EventHandler(priority = EventPriority.MONITOR, ignoreCancelled = true) public void onHeal(EntityRegainHealthEvent e) { if (!(e.getEntity() instanceof Player player)) return; Bukkit.getScheduler().runTaskLater(this, () -> { if (player.isOnline()) pushHealthIfChanged(player); }, 1L); } @EventHandler(priority = EventPriority.MONITOR, ignoreCancelled = true) public void onMove(PlayerMoveEvent e) { // getTo() kann in 1.20.5+ bei reinen Head-Rotationen null sein if (e.getTo() == null) return; if (e.getFrom().getYaw() == e.getTo().getYaw()) return; pushCompassIfChanged(e.getPlayer()); } // ── Periodische Tasks ───────────────────────────────────────────────────── 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); } } private void pushScoreboardData() { double tps = getCurrentTps(); for (Player player : Bukkit.getOnlinePlayers()) { pushPlayerScoreboardData(player); pushTpsAsync(player.getUniqueId(), tps); } } private void pushPlayerScoreboardData(Player player) { pushHealthIfChanged(player); pushCompassIfChanged(player); pushWorldIfChanged(player); pushPlayerDataIfChanged(player); pushStatsIfChanged(player); // Nametag periodisch aktualisieren (reagiert auf Rang-Änderungen) if (nametagEnabled) applyNametag(player); } // ── Push-Methoden ───────────────────────────────────────────────────────── public void pushEconomy(Player player) { pushEconomyAsync(player.getUniqueId(), player.getName(), economy.getBalance(player)); } private void pushEconomyAsync(UUID uuid, String name, double balance) { httpExecutor.execute(() -> { try { sendPost(statusApiUrl + "/economy/update", "{\"uuid\":\"" + uuid + "\",\"name\":\"" + escapeName(name) + "\",\"balance\":" + balance + "}"); lastPushedBalance.put(uuid, balance); } catch (Exception e) { getLogger().warning("Economy-Push fehlgeschlagen fuer " + name + ": " + e.getMessage()); } }); } private void pushHealthIfChanged(Player player) { double health = player.getHealth(); Double last = lastPushedHealth.get(player.getUniqueId()); if (last != null && Math.abs(health - last) < 0.01) return; lastPushedHealth.put(player.getUniqueId(), health); UUID uuid = player.getUniqueId(); String name = player.getName(); httpExecutor.execute(() -> { try { sendPost(statusApiUrl + "/scoreboard/health", "{\"uuid\":\"" + uuid + "\",\"name\":\"" + escapeName(name) + "\",\"health\":" + health + "}"); } catch (Exception e) { getLogger().warning("Health-Push fehlgeschlagen: " + e.getMessage()); } }); } private void pushCompassIfChanged(Player player) { // Rohen Yaw normalisieren auf 0..360 (0 = Süden, wie MC-Konvention) float rawYaw = player.getLocation().getYaw(); float normYaw = ((rawYaw % 360) + 360) % 360; String yawStr = String.format(java.util.Locale.US, "%.1f", normYaw); // Nur senden wenn Änderung >= 0.5° – fein genug für 1-Grad-Slots String lastStr = lastPushedCompass.get(player.getUniqueId()); if (lastStr != null) { try { float lastYaw = Float.parseFloat(lastStr); float diff = Math.abs(normYaw - lastYaw); if (diff > 180) diff = 360 - diff; // kürzester Bogenweg if (diff < 0.5f) return; } catch (NumberFormatException ignored) {} } lastPushedCompass.put(player.getUniqueId(), yawStr); UUID uuid = player.getUniqueId(); String name = player.getName(); httpExecutor.execute(() -> { try { sendPost(statusApiUrl + "/scoreboard/compass", "{\"uuid\":\"" + uuid + "\",\"name\":\"" + escapeName(name) + "\",\"compass\":\"" + yawStr + "\"}"); } catch (Exception e) { getLogger().warning("Compass-Push fehlgeschlagen: " + e.getMessage()); } }); } private void pushWorldIfChanged(Player player) { String world = player.getWorld().getName(); if (world.equals(lastPushedWorld.get(player.getUniqueId()))) return; lastPushedWorld.put(player.getUniqueId(), world); UUID uuid = player.getUniqueId(); String name = player.getName(); httpExecutor.execute(() -> { try { sendPost(statusApiUrl + "/player/world", "{\"uuid\":\"" + uuid + "\",\"name\":\"" + escapeName(name) + "\",\"world\":\"" + escapeName(world) + "\"}"); } catch (Exception e) { getLogger().warning("World-Push fehlgeschlagen: " + e.getMessage()); } }); } private void pushPlayerDataIfChanged(Player player) { int x = player.getLocation().getBlockX(); int y = player.getLocation().getBlockY(); int z = player.getLocation().getBlockZ(); String gm = player.getGameMode().name(); int exp= player.getLevel(); int fd = player.getFoodLevel(); double sp = player.getWalkSpeed(); String wld= player.getWorld().getName(); String key = x+","+y+","+z+","+gm+","+exp+","+fd+","+String.format("%.2f",sp)+","+wld; if (key.equals(lastPushedData.get(player.getUniqueId()))) return; lastPushedData.put(player.getUniqueId(), key); UUID uuid = player.getUniqueId(); String name = player.getName(); httpExecutor.execute(() -> { try { sendPost(statusApiUrl + "/player/data", "{\"uuid\":\"" + uuid + "\",\"name\":\"" + escapeName(name) + "\",\"x\":" + x + ",\"y\":" + y + ",\"z\":" + z + ",\"gamemode\":\"" + gm + "\"" + ",\"exp\":" + exp + ",\"food\":" + fd + ",\"speed\":" + String.format(java.util.Locale.US, "%.4f", sp) + ",\"world\":\"" + escapeName(wld) + "\"}"); } catch (Exception e) { getLogger().warning("PlayerData-Push fehlgeschlagen: " + e.getMessage()); } }); } private void pushStatsIfChanged(Player player) { int kills = player.getStatistic(org.bukkit.Statistic.PLAYER_KILLS); int deaths = player.getStatistic(org.bukkit.Statistic.DEATHS); // Playtime in Ticks aus Minecraft-Statistik → umrechnen in Sekunden long playtimeTicks = player.getStatistic(org.bukkit.Statistic.PLAY_ONE_MINUTE); // tatsächlich in Ticks long playtimeSecs = playtimeTicks / 20; String key = kills + "," + deaths + "," + playtimeSecs; if (key.equals(lastPushedStats.get(player.getUniqueId()))) return; lastPushedStats.put(player.getUniqueId(), key); UUID uuid = player.getUniqueId(); String name = player.getName(); httpExecutor.execute(() -> { try { sendPost(statusApiUrl + "/stats/update", "{\"uuid\":\"" + uuid + "\",\"name\":\"" + escapeName(name) + "\",\"kills\":" + kills + ",\"deaths\":" + deaths + ",\"playtime\":" + playtimeSecs + "}"); } catch (Exception e) { getLogger().warning("Stats-Push fehlgeschlagen: " + e.getMessage()); } }); } // ── TicketSystem Push ────────────────────────────────────────────────── /** * Pushed TicketSystem-Daten an die StatusAPI. * Globale Werte (offene Tickets, Bewertungen) werden einmal pro Intervall gesendet. * Pro Spieler wird die Anzahl eigener aktiver Tickets mitgeschickt. * * Voraussetzung: TicketSystem muss auf demselben Bukkit-Server laufen. */ private void pushTicketData() { try { Class pluginClass = Class.forName("de.ticketsystem.TicketPlugin"); Object tsPlugin = Bukkit.getPluginManager().getPlugin("TicketSystem"); if (tsPlugin == null) return; Object db = pluginClass.getMethod("getDatabaseManager").invoke(tsPlugin); if (db == null) return; Class dbClass = db.getClass(); Class statsClass = Class.forName("de.ticketsystem.database.DatabaseManager$TicketStats"); Object stats = dbClass.getMethod("getTicketStats").invoke(db); int totalOpen = (int) statsClass.getField("open").get(stats); int totalClaimed = (int) statsClass.getField("closed").get(stats); // "closed" im stats-Kontext = bearbeitet // CLAIMED direkt zählen via getTicketsByStatus Class statusEnum = Class.forName("de.ticketsystem.model.TicketStatus"); Object claimed = statusEnum.getField("CLAIMED").get(null); // Varargs via Reflection: typisiertes Array (TicketStatus[]) erzeugen, kein Object[] Object statusArray = java.lang.reflect.Array.newInstance(statusEnum, 1); java.lang.reflect.Array.set(statusArray, 0, claimed); @SuppressWarnings("unchecked") java.util.List claimedTickets = (java.util.List) dbClass .getMethod("getTicketsByStatus", statusArray.getClass()) .invoke(db, statusArray); int totalClaimedCount = claimedTickets == null ? 0 : claimedTickets.size(); int ratGood = (int) statsClass.getField("thumbsUp").get(stats); int ratBad = (int) statsClass.getField("thumbsDown").get(stats); // Globale Werte nur senden wenn geändert String globalKey = totalOpen + "," + totalClaimedCount + "," + ratGood + "," + ratBad; if (!globalKey.equals(lastPushedTicketGlobal)) { lastPushedTicketGlobal = globalKey; String globalJson = "{\"total_open\":" + totalOpen + ",\"total_claimed\":" + totalClaimedCount + ",\"rating_good\":" + ratGood + ",\"rating_bad\":" + ratBad + "}"; sendPost(statusApiUrl + "/ticket/update", globalJson); } // Pro Spieler: eigene aktive Tickets for (org.bukkit.entity.Player player : Bukkit.getOnlinePlayers()) { int myOpen = (int) dbClass .getMethod("countOpenTicketsByPlayer", java.util.UUID.class) .invoke(db, player.getUniqueId()); String playerJson = "{\"uuid\":\"" + player.getUniqueId() + "\",\"my_open\":" + myOpen + "}"; sendPost(statusApiUrl + "/ticket/update", playerJson); } } catch (ClassNotFoundException e) { // TicketSystem nicht installiert – kein Fehler loggen } catch (Exception e) { getLogger().warning("[TicketPush] Fehler: " + e.getMessage()); } } // ── PlaceholderAPI ──────────────────────────────────────────────────────── private boolean fetchPapiTokensFromStatusAPI() { try { @SuppressWarnings("deprecation") java.net.URL url = new java.net.URI(statusApiUrl + "/papi/tokens").toURL(); java.net.HttpURLConnection c = (java.net.HttpURLConnection) url.openConnection(); c.setRequestMethod("GET"); c.setConnectTimeout(3000); c.setReadTimeout(3000); if (c.getResponseCode() != 200) { c.disconnect(); return false; } java.io.InputStream is = c.getInputStream(); StringBuilder sb = new StringBuilder(); int ch; while ((ch = is.read()) != -1) sb.append((char) ch); c.disconnect(); String body = sb.toString().trim(); papiTokens.clear(); if (body.startsWith("[") && body.endsWith("]")) { String inner = body.substring(1, body.length() - 1).trim(); if (!inner.isEmpty()) { int i = 0; while (i < inner.length()) { while (i < inner.length() && inner.charAt(i) != '"') i++; if (i >= inner.length()) break; i++; StringBuilder token = new StringBuilder(); while (i < inner.length() && inner.charAt(i) != '"') { char c2 = inner.charAt(i++); if (c2 == '\\' && i < inner.length()) c2 = inner.charAt(i++); token.append(c2); } i++; if (token.length() > 0) papiTokens.add(token.toString()); } } } return true; } catch (Exception e) { return false; } } private void syncPapiValues() { if (!papiEnabled || papiTokens.isEmpty()) return; for (Player p : Bukkit.getOnlinePlayers()) pushPapiValues(p); } private void pushPapiValues(Player p) { try { Class papiClass = Class.forName("me.clip.placeholderapi.PlaceholderAPI"); java.lang.reflect.Method setPlaceholders = papiClass.getMethod("setPlaceholders", Player.class, String.class); StringBuilder jsonValues = new StringBuilder(); for (String token : papiTokens) { String resolved = (String) setPlaceholders.invoke(null, p, "%" + token + "%"); if (resolved == null) resolved = ""; if (jsonValues.length() > 0) jsonValues.append(","); jsonValues.append("\"").append(esc(token)).append("\":\"").append(esc(resolved)).append("\""); } String snapshot = jsonValues.toString(); if (snapshot.equals(lastPapiValues.get(p.getUniqueId()))) return; lastPapiValues.put(p.getUniqueId(), snapshot); String json = "{\"uuid\":\"" + p.getUniqueId() + "\",\"placeholders\":{" + snapshot + "}}"; httpExecutor.execute(() -> { try { sendPost(statusApiUrl + "/player/papi", json); } catch (Exception e) { getLogger().warning("[PAPI] Push fehlgeschlagen: " + e.getMessage()); } }); } catch (ClassNotFoundException ignored) { } catch (Exception e) { getLogger().warning("[PAPI] Fehler: " + e.getMessage()); } } private static String esc(String s) { if (s == null) return ""; return s.replace("\\", "\\\\").replace("\"", "\\\""); } private void pushTpsAsync(UUID uuid, double tps) { httpExecutor.execute(() -> { try { sendPost(statusApiUrl + "/scoreboard/tps", "{\"uuid\":\"" + uuid + "\",\"tps\":" + tps + "}"); } catch (Exception ignored) {} }); } // ── Nametag-Methoden ────────────────────────────────────────────────────── /** * Setzt den LuckPerms-Prefix als Nametag über dem Spieler-Kopf. * Nutzt die Bukkit Scoreboard Team API – zuverlässig auf allen Spigot/Paper-Versionen. * Wird bei Join und periodisch (scoreboard-sync) aufgerufen. */ @SuppressWarnings("deprecation") private void applyNametag(Player player) { if (!nametagEnabled || nametagBoard == null) return; String prefix = getLuckPermsPrefix(player); // Change-Detection: nicht neu setzen wenn Prefix gleich geblieben String last = lastNametagPrefix.get(player.getUniqueId()); if (prefix.equals(last)) return; lastNametagPrefix.put(player.getUniqueId(), prefix); // Team-Name: "vnt_" + erste 12 Zeichen der UUID (ohne Bindestriche) // Minecraft-Limit: 16 Zeichen für Teamnamen String teamName = "vnt" + player.getUniqueId().toString().replace("-", "").substring(0, 13); try { // Bestehendes Team holen oder neu erstellen org.bukkit.scoreboard.Team team = nametagBoard.getTeam(teamName); if (team == null) { team = nametagBoard.registerNewTeam(teamName); } // Prefix setzen (Bukkit konvertiert §-Codes automatisch) String coloredPrefix = org.bukkit.ChatColor.translateAlternateColorCodes('&', prefix) + " "; team.setPrefix(coloredPrefix); team.setSuffix(""); // Spieler dem Team zuweisen team.addEntry(player.getName()); // Scoreboard dem Spieler zuweisen player.setScoreboard(nametagBoard); // Alle anderen Spieler auf dasselbe Scoreboard setzen damit sie den Prefix sehen for (Player other : Bukkit.getOnlinePlayers()) { if (!other.equals(player) && other.getScoreboard() != nametagBoard) { other.setScoreboard(nametagBoard); } } } catch (Exception e) { getLogger().warning("[Nametag] Fehler beim Setzen des Prefixes für " + player.getName() + ": " + e.getMessage()); } } /** * Entfernt den Spieler aus seinem Nametag-Team beim Disconnect. */ private void removeNametag(Player player) { if (nametagBoard == null) return; String teamName = "vnt" + player.getUniqueId().toString().replace("-", "").substring(0, 13); try { org.bukkit.scoreboard.Team team = nametagBoard.getTeam(teamName); if (team != null) { team.removeEntry(player.getName()); // Team löschen wenn leer if (team.getEntries().isEmpty()) team.unregister(); } } catch (Exception ignored) {} } /** * Holt den LuckPerms-Prefix eines Spielers via Reflection (keine harte Dependency). * Gibt leeren String zurück wenn LuckPerms nicht vorhanden oder kein Prefix gesetzt. */ private String getLuckPermsPrefix(Player player) { try { Class provClass = Class.forName("net.luckperms.api.LuckPermsProvider"); Object api = provClass.getMethod("get").invoke(null); Object um = api.getClass().getMethod("getUserManager").invoke(api); Object usr = um.getClass().getMethod("getUser", UUID.class).invoke(um, player.getUniqueId()); if (usr == null) return ""; Class qoClass = Class.forName("net.luckperms.api.query.QueryOptions"); Object opts = qoClass.getMethod("defaultContextualOptions").invoke(null); Object cache = usr.getClass().getMethod("getCachedData").invoke(usr); Object meta = cache.getClass().getMethod("getMetaData", qoClass).invoke(cache, opts); Object pfx = meta.getClass().getMethod("getPrefix").invoke(meta); if (pfx != null && !pfx.toString().isEmpty()) return pfx.toString(); } catch (ClassNotFoundException ignored) { // LuckPerms nicht installiert } catch (Exception e) { getLogger().warning("[Nametag] LuckPerms-Prefix konnte nicht gelesen werden: " + e.getMessage()); } return ""; } // ── Hilfsmethoden ───────────────────────────────────────────────────────── /** * Erkennt beim Start die Server-Version und setzt den internen Modus. * Sichtbar im Server-Log als [StatusAPIBridge] Versions-Modus: ... */ private void detectMinecraftVersion() { String bukkitVersion = Bukkit.getBukkitVersion(); // z.B. "1.21.1-R0.1-SNAPSHOT" oder "26.1.2-R0.1-SNAPSHOT" // Alles ab 26.x gilt als "neuer Modus" ohne NMS-Fallback try { String major = bukkitVersion.split("\\.")[0]; int majorVersion = Integer.parseInt(major); isLegacyMode = majorVersion < 26; } catch (Exception e) { isLegacyMode = true; // Fallback: sicherer Legacy-Modus } getLogger().info("Versions-Modus: " + (isLegacyMode ? "1.21.x-Modus (NMS-Fallback aktiv)" : "26.1.x-Modus (kein NMS-Fallback)") + " | BukkitVersion: " + bukkitVersion); } /** * TPS auslesen – kompatibel mit Paper 1.21+, Spigot 1.21+, Java 17/21. * Reihenfolge: * 1. Paper-API: getTPS() direkt auf dem Server (sauberster Weg) * 2. Spigot-Reflection: recentTps-Feld auf dem NMS-MinecraftServer * 3. Fallback: 20.0 */ private double getCurrentTps() { // 1. Bevorzugt: Bukkit.getTPS() – funktioniert auf beiden Versionen try { double[] tps = (double[]) Bukkit.getServer().getClass() .getMethod("getTPS").invoke(Bukkit.getServer()); if (tps != null && tps.length > 0) return Math.min(20.0, tps[0]); } catch (Exception ignored) {} // 2. NMS-Reflection-Fallback – nur im 1.21.x-Modus // Auf 26.1.x schlägt recentTps fehl → wird bewusst übersprungen if (isLegacyMode) { try { Object nmsServer = Bukkit.getServer().getClass() .getMethod("getServer").invoke(Bukkit.getServer()); for (String fieldName : new String[]{"recentTps", "tps"}) { try { java.lang.reflect.Field f = nmsServer.getClass().getField(fieldName); Object val = f.get(nmsServer); if (val instanceof double[]) { double[] tps = (double[]) val; if (tps.length > 0) return Math.min(20.0, tps[0]); } } catch (NoSuchFieldException ignored2) {} } } catch (Exception ignored) {} } return 20.0; } private void sendPost(String urlStr, String json) throws Exception { @SuppressWarnings("deprecation") URL url = new java.net.URI(urlStr).toURL(); HttpURLConnection conn = (HttpURLConnection) url.openConnection(); conn.setRequestMethod("POST"); conn.setDoOutput(true); conn.setConnectTimeout(3000); conn.setReadTimeout(3000); conn.setRequestProperty("Content-Type", "application/json; charset=UTF-8"); 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(); } private String escapeName(String name) { if (name == null) return ""; return name.replace("\\", "\\\\").replace("\"", "\\\""); } }