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]