diff --git a/StatusAPIBridge/pom.xml b/StatusAPIBridge/pom.xml index 2ef98af..cb91520 100644 --- a/StatusAPIBridge/pom.xml +++ b/StatusAPIBridge/pom.xml @@ -6,14 +6,17 @@ net.viper StatusAPIBridge - 1.0.0 + 1.0.2 jar + 17 ${java.version} ${java.version} UTF-8 + + 1.21.1-R0.1-SNAPSHOT @@ -25,14 +28,18 @@ vault-repo https://nexus.hc.to/content/repositories/pub_releases/ + + placeholderapi + https://repo.extendedclip.com/content/repositories/placeholderapi/ + - + org.spigotmc spigot-api - 1.21-R0.1-SNAPSHOT + ${spigot.version} provided @@ -42,11 +49,56 @@ 1.7 provided + + + me.clip + placeholderapi + 2.11.6 + provided + + + + + + + mc-1.21.1 + + true + + + 1.21.1-R0.1-SNAPSHOT + + + + + + mc-26.1.2 + + 1.21.1-R0.1-SNAPSHOT + + + + + StatusAPIBridge + + org.apache.maven.plugins + maven-compiler-plugin + 3.13.0 + + ${java.version} + ${java.version} + UTF-8 + + org.apache.maven.plugins maven-shade-plugin diff --git a/StatusAPIBridge/src/main/java/net/viper/statusapibridge/StatusAPIBridge.java b/StatusAPIBridge/src/main/java/net/viper/statusapibridge/StatusAPIBridge.java index c8e0be7..afdeb4f 100644 --- a/StatusAPIBridge/src/main/java/net/viper/statusapibridge/StatusAPIBridge.java +++ b/StatusAPIBridge/src/main/java/net/viper/statusapibridge/StatusAPIBridge.java @@ -20,6 +20,7 @@ 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; @@ -41,6 +42,16 @@ public class StatusAPIBridge extends JavaPlugin implements Listener { 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; + + // ── 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); @@ -50,6 +61,7 @@ public class StatusAPIBridge extends JavaPlugin implements Listener { @Override public void onEnable() { saveDefaultConfig(); + detectMinecraftVersion(); 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)); @@ -70,6 +82,28 @@ public class StatusAPIBridge extends JavaPlugin implements Listener { // 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); } @@ -96,6 +130,7 @@ public class StatusAPIBridge extends JavaPlugin implements Listener { if (!player.isOnline()) return; if (economy != null) pushEconomy(player); pushPlayerScoreboardData(player); + if (papiEnabled && !papiTokens.isEmpty()) pushPapiValues(player); }, pushDelayTicks); } @@ -110,26 +145,27 @@ public class StatusAPIBridge extends JavaPlugin implements Listener { lastPushedWorld.remove(id); lastPushedData.remove(id); lastPushedStats.remove(id); + lastPapiValues.remove(id); } @EventHandler(priority = EventPriority.MONITOR, ignoreCancelled = true) public void onDamage(EntityDamageEvent e) { - if (!(e.getEntity() instanceof Player)) return; - Player player = (Player) e.getEntity(); + 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)) return; - Player player = (Player) e.getEntity(); + 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()); } @@ -368,6 +404,79 @@ public class StatusAPIBridge extends JavaPlugin implements Listener { } } + // ── 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 { @@ -379,24 +488,65 @@ public class StatusAPIBridge extends JavaPlugin implements Listener { // ── Hilfsmethoden ───────────────────────────────────────────────────────── - /** TPS – Paper-API zuerst, dann Spigot-Reflection-Fallback */ + /** + * 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()); - return Math.min(20.0, tps[0]); - } catch (Exception ignored) {} - try { - Object ms = Bukkit.getServer().getClass() - .getMethod("getServer").invoke(Bukkit.getServer()); - double[] tps = (double[]) ms.getClass().getField("recentTps").get(ms); - return Math.min(20.0, tps[0]); + 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 { - URL url = new URL(urlStr); + @SuppressWarnings("deprecation") + URL url = new java.net.URI(urlStr).toURL(); HttpURLConnection conn = (HttpURLConnection) url.openConnection(); conn.setRequestMethod("POST"); conn.setDoOutput(true); diff --git a/StatusAPIBridge/src/main/resources/plugin.yml b/StatusAPIBridge/src/main/resources/plugin.yml index 694d0ae..3c6dd4d 100644 --- a/StatusAPIBridge/src/main/resources/plugin.yml +++ b/StatusAPIBridge/src/main/resources/plugin.yml @@ -1,7 +1,8 @@ name: StatusAPIBridge -version: 1.0.0 +version: 1.0.2 main: net.viper.statusapibridge.StatusAPIBridge +# 1.21 als niedrigste gemeinsame Basis – wird von 1.21.1 und 26.1.2 akzeptiert api-version: 1.21 -description: Sendet Vault-Economy-Daten an die BungeeCord StatusAPI +description: Sendet Spielerdaten an die BungeeCord StatusAPI authors: [Viper] -softdepend: [Vault] +softdepend: [Vault, PlaceholderAPI]