diff --git a/StatusAPIBridge/pom.xml b/StatusAPIBridge/pom.xml index cb91520..edfe89a 100644 --- a/StatusAPIBridge/pom.xml +++ b/StatusAPIBridge/pom.xml @@ -6,14 +6,20 @@ net.viper StatusAPIBridge - 1.0.2 + 1.0.3 jar - - 17 + + 21 ${java.version} ${java.version} + ${java.version} UTF-8 1.21.1-R0.1-SNAPSHOT @@ -60,8 +66,9 @@ @@ -76,11 +83,25 @@ - + mc-26.1.2 - 1.21.1-R0.1-SNAPSHOT + 26.1.2-R0.1-SNAPSHOT + + + + + + mc-26.1.2-j25 + + 26.1.2-R0.1-SNAPSHOT + 25 + 25 + 25 + 25 @@ -92,17 +113,18 @@ org.apache.maven.plugins maven-compiler-plugin - 3.13.0 + 3.14.0 ${java.version} ${java.version} + ${java.version} UTF-8 org.apache.maven.plugins maven-shade-plugin - 3.5.1 + 3.6.0 package diff --git a/StatusAPIBridge/src/main/java/net/viper/statusapibridge/StatusAPIBridge.java b/StatusAPIBridge/src/main/java/net/viper/statusapibridge/StatusAPIBridge.java index d0eadfa..3c21d5f 100644 --- a/StatusAPIBridge/src/main/java/net/viper/statusapibridge/StatusAPIBridge.java +++ b/StatusAPIBridge/src/main/java/net/viper/statusapibridge/StatusAPIBridge.java @@ -14,6 +14,7 @@ import org.bukkit.event.player.PlayerMoveEvent; import org.bukkit.event.player.PlayerQuitEvent; import org.bukkit.plugin.RegisteredServiceProvider; import org.bukkit.plugin.java.JavaPlugin; +import org.bukkit.plugin.messaging.PluginMessageListener; import java.io.OutputStream; import java.net.HttpURLConnection; @@ -24,15 +25,19 @@ import java.util.Set; import java.util.UUID; import java.util.concurrent.ConcurrentHashMap; import java.util.concurrent.ExecutorService; -import java.util.concurrent.Executors; +import java.util.concurrent.LinkedBlockingQueue; +import java.util.concurrent.RejectedExecutionException; +import java.util.concurrent.ThreadPoolExecutor; +import java.util.concurrent.TimeUnit; -public class StatusAPIBridge extends JavaPlugin implements Listener { +public class StatusAPIBridge extends JavaPlugin implements Listener, PluginMessageListener { private Economy economy; private String statusApiUrl; private int pushDelayTicks; private int liveSyncIntervalTicks; private int scoreboardSyncIntervalTicks; + private int compassMinPushIntervalMs; private final Map lastPushedBalance = new ConcurrentHashMap<>(); private final Map lastPushedHealth = new ConcurrentHashMap<>(); @@ -40,6 +45,7 @@ public class StatusAPIBridge extends JavaPlugin implements Listener { private final Map lastPushedWorld = new ConcurrentHashMap<>(); private final Map lastPushedData = new ConcurrentHashMap<>(); private final Map lastPushedStats = new ConcurrentHashMap<>(); + private final Map lastCompassPushAt = new ConcurrentHashMap<>(); private String lastPushedTicketGlobal = ""; // ── PlaceholderAPI ──────────────────────────────────────────────────────── @@ -54,17 +60,31 @@ public class StatusAPIBridge extends JavaPlugin implements Listener { private final Map lastNametagPrefix = new ConcurrentHashMap<>(); /** Feature aktivierbar via config: nametag-enabled */ private boolean nametagEnabled = true; + /** AFK-Status pro Spieler – wird via Plugin-Message von BungeeCord aktualisiert */ + private final Map afkState = new ConcurrentHashMap<>(); // ── 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; - }); + // 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ß). + private final ExecutorService httpExecutor = new ThreadPoolExecutor( + 2, + 2, + 30L, + TimeUnit.SECONDS, + new LinkedBlockingQueue<>(2000), + r -> { + Thread t = new Thread(r, "StatusAPIBridge-HTTP"); + t.setDaemon(true); + return t; + }, + new ThreadPoolExecutor.CallerRunsPolicy() + ); @Override public void onEnable() { @@ -80,6 +100,7 @@ public class StatusAPIBridge extends JavaPlugin implements Listener { 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)); + compassMinPushIntervalMs = Math.max(50, getConfig().getInt("compass-min-push-interval-ms", 150)); if (!setupEconomy()) { getLogger().warning("Vault/Economy nicht gefunden – Economy-Push deaktiviert."); @@ -88,6 +109,8 @@ public class StatusAPIBridge extends JavaPlugin implements Listener { } Bukkit.getPluginManager().registerEvents(this, this); + // Plugin-Message-Channel für AFK-Status-Updates von BungeeCord registrieren + getServer().getMessenger().registerIncomingPluginChannel(this, "statusapi:afk", this); Bukkit.getScheduler().runTaskTimer(this, this::pushChangedBalancesForOnlinePlayers, liveSyncIntervalTicks, liveSyncIntervalTicks); Bukkit.getScheduler().runTaskTimer(this, this::pushScoreboardData, @@ -123,6 +146,7 @@ public class StatusAPIBridge extends JavaPlugin implements Listener { @Override public void onDisable() { + getServer().getMessenger().unregisterIncomingPluginChannel(this, "statusapi:afk"); httpExecutor.shutdownNow(); } @@ -161,6 +185,7 @@ public class StatusAPIBridge extends JavaPlugin implements Listener { lastPushedWorld.remove(id); lastPushedData.remove(id); lastPushedStats.remove(id); + lastCompassPushAt.remove(id); lastPapiValues.remove(id); lastNametagPrefix.remove(id); // Nametag-Team beim Quit aufräumen @@ -215,8 +240,9 @@ public class StatusAPIBridge extends JavaPlugin implements Listener { pushWorldIfChanged(player); pushPlayerDataIfChanged(player); pushStatsIfChanged(player); - // Nametag periodisch aktualisieren (reagiert auf Rang-Änderungen) - if (nametagEnabled) applyNametag(player); + // BUGFIX: applyNametag() hier NICHT aufrufen – es überschreibt bei jedem Scoreboard-Sync-Tick + // das Scoreboard ALLER Online-Spieler und zerstört dabei Scoreboards anderer Plugins. + // Nametag-Updates laufen jetzt nur noch event-basiert (Join + expliziter Rang-Wechsel). } // ── Push-Methoden ───────────────────────────────────────────────────────── @@ -226,7 +252,7 @@ public class StatusAPIBridge extends JavaPlugin implements Listener { } private void pushEconomyAsync(UUID uuid, String name, double balance) { - httpExecutor.execute(() -> { + submitHttp(() -> { try { sendPost(statusApiUrl + "/economy/update", "{\"uuid\":\"" + uuid + "\",\"name\":\"" + escapeName(name) @@ -235,7 +261,7 @@ public class StatusAPIBridge extends JavaPlugin implements Listener { } catch (Exception e) { getLogger().warning("Economy-Push fehlgeschlagen fuer " + name + ": " + e.getMessage()); } - }); + }, "economy"); } private void pushHealthIfChanged(Player player) { @@ -244,7 +270,7 @@ public class StatusAPIBridge extends JavaPlugin implements Listener { 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(() -> { + submitHttp(() -> { try { sendPost(statusApiUrl + "/scoreboard/health", "{\"uuid\":\"" + uuid + "\",\"name\":\"" + escapeName(name) @@ -252,17 +278,22 @@ public class StatusAPIBridge extends JavaPlugin implements Listener { } catch (Exception e) { getLogger().warning("Health-Push fehlgeschlagen: " + e.getMessage()); } - }); + }, "health"); } private void pushCompassIfChanged(Player player) { + UUID uuid = player.getUniqueId(); + long now = System.currentTimeMillis(); + Long lastPushAt = lastCompassPushAt.get(uuid); + if (lastPushAt != null && (now - lastPushAt) < compassMinPushIntervalMs) return; + // 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()); + String lastStr = lastPushedCompass.get(uuid); if (lastStr != null) { try { float lastYaw = Float.parseFloat(lastStr); @@ -272,9 +303,10 @@ public class StatusAPIBridge extends JavaPlugin implements Listener { } catch (NumberFormatException ignored) {} } - lastPushedCompass.put(player.getUniqueId(), yawStr); - UUID uuid = player.getUniqueId(); String name = player.getName(); - httpExecutor.execute(() -> { + lastPushedCompass.put(uuid, yawStr); + lastCompassPushAt.put(uuid, now); + String name = player.getName(); + submitHttp(() -> { try { sendPost(statusApiUrl + "/scoreboard/compass", "{\"uuid\":\"" + uuid + "\",\"name\":\"" + escapeName(name) @@ -282,7 +314,7 @@ public class StatusAPIBridge extends JavaPlugin implements Listener { } catch (Exception e) { getLogger().warning("Compass-Push fehlgeschlagen: " + e.getMessage()); } - }); + }, "compass"); } private void pushWorldIfChanged(Player player) { @@ -290,7 +322,7 @@ public class StatusAPIBridge extends JavaPlugin implements Listener { if (world.equals(lastPushedWorld.get(player.getUniqueId()))) return; lastPushedWorld.put(player.getUniqueId(), world); UUID uuid = player.getUniqueId(); String name = player.getName(); - httpExecutor.execute(() -> { + submitHttp(() -> { try { sendPost(statusApiUrl + "/player/world", "{\"uuid\":\"" + uuid + "\",\"name\":\"" + escapeName(name) @@ -298,7 +330,7 @@ public class StatusAPIBridge extends JavaPlugin implements Listener { } catch (Exception e) { getLogger().warning("World-Push fehlgeschlagen: " + e.getMessage()); } - }); + }, "world"); } private void pushPlayerDataIfChanged(Player player) { @@ -315,7 +347,7 @@ public class StatusAPIBridge extends JavaPlugin implements Listener { lastPushedData.put(player.getUniqueId(), key); UUID uuid = player.getUniqueId(); String name = player.getName(); - httpExecutor.execute(() -> { + submitHttp(() -> { try { sendPost(statusApiUrl + "/player/data", "{\"uuid\":\"" + uuid + "\",\"name\":\"" + escapeName(name) @@ -330,7 +362,7 @@ public class StatusAPIBridge extends JavaPlugin implements Listener { } catch (Exception e) { getLogger().warning("PlayerData-Push fehlgeschlagen: " + e.getMessage()); } - }); + }, "player-data"); } private void pushStatsIfChanged(Player player) { @@ -346,7 +378,7 @@ public class StatusAPIBridge extends JavaPlugin implements Listener { UUID uuid = player.getUniqueId(); String name = player.getName(); - httpExecutor.execute(() -> { + submitHttp(() -> { try { sendPost(statusApiUrl + "/stats/update", "{\"uuid\":\"" + uuid + "\",\"name\":\"" + escapeName(name) @@ -356,7 +388,7 @@ public class StatusAPIBridge extends JavaPlugin implements Listener { } catch (Exception e) { getLogger().warning("Stats-Push fehlgeschlagen: " + e.getMessage()); } - }); + }, "stats"); } // ── TicketSystem Push ────────────────────────────────────────────────── @@ -485,26 +517,59 @@ public class StatusAPIBridge extends JavaPlugin implements Listener { if (snapshot.equals(lastPapiValues.get(p.getUniqueId()))) return; lastPapiValues.put(p.getUniqueId(), snapshot); String json = "{\"uuid\":\"" + p.getUniqueId() + "\",\"placeholders\":{" + snapshot + "}}"; - httpExecutor.execute(() -> { + submitHttp(() -> { try { sendPost(statusApiUrl + "/player/papi", json); } catch (Exception e) { getLogger().warning("[PAPI] Push fehlgeschlagen: " + e.getMessage()); } - }); + }, "papi"); } 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("\"", "\\\""); + // BUGFIX: Steuerzeichen escapen (JSON-Injection-Schutz für PAPI-Werte) + return s.replace("\\", "\\\\") + .replace("\"", "\\\"") + .replace("\n", "\\n") + .replace("\r", "\\r") + .replace("\t", "\\t"); } private void pushTpsAsync(UUID uuid, double tps) { - httpExecutor.execute(() -> { + submitHttp(() -> { try { sendPost(statusApiUrl + "/scoreboard/tps", "{\"uuid\":\"" + uuid + "\",\"tps\":" + tps + "}"); } catch (Exception ignored) {} - }); + }, "tps"); + } + + // ── Plugin-Message-Handler ─────────────────────────────────────────────── + + /** + * Empfängt AFK-Status-Updates von BungeeCord (AfkModule). + * Channel: "statusapi:afk", Format: "UUID:true" oder "UUID:false" + * Aktualisiert den Nametag-Prefix des Spielers sofort. + */ + @Override + public void onPluginMessageReceived(String channel, Player unused, byte[] message) { + if (!"statusapi:afk".equals(channel)) return; + try { + String msg = new String(message, java.nio.charset.StandardCharsets.UTF_8); + int sep = msg.lastIndexOf(':'); + if (sep < 0) return; + UUID uuid = UUID.fromString(msg.substring(0, sep)); + boolean afk = Boolean.parseBoolean(msg.substring(sep + 1)); + afkState.put(uuid, afk); + // Nametag sofort aktualisieren wenn Spieler auf diesem Server ist + Player target = Bukkit.getPlayer(uuid); + if (target != null && target.isOnline() && nametagEnabled) { + // Muss im Hauptthread laufen (Scoreboard-API ist nicht thread-safe) + Bukkit.getScheduler().runTask(this, () -> applyNametag(target)); + } + } catch (Exception e) { + getLogger().warning("[StatusAPIBridge] Fehler beim Verarbeiten der AFK-Message: " + e.getMessage()); + } } // ── Nametag-Methoden ────────────────────────────────────────────────────── @@ -517,7 +582,16 @@ public class StatusAPIBridge extends JavaPlugin implements Listener { @SuppressWarnings("deprecation") private void applyNametag(Player player) { if (!nametagEnabled || nametagBoard == null) return; - String prefix = getLuckPermsPrefix(player); + String rawPrefix = getLuckPermsPrefix(player); + // AFK-Prefix ERSETZT den Rang-Prefix (nicht voranstellen). Muss identisch zu + // ScoreboardModule.updateNametag() im StatusAPI-Proxy sein: beide Systeme + // setzen Nametag-Teams, und je nachdem wer zuletzt geschrieben hat gewinnt + // eines von beiden. Bei unterschiedlichen Formaten sieht der Spieler sonst + // mal "[AFK] Rang Name" und mal "[AFK] Name". + // Ergebnis hier nach dem angehaengten Leerzeichen: "§7[AFK]§r " – exakt wie + // "§7[AFK] §r" auf der Proxy-Seite. + boolean isAfk = Boolean.TRUE.equals(afkState.get(player.getUniqueId())); + String prefix = isAfk ? "&7[AFK]&r" : rawPrefix; // Change-Detection: nicht neu setzen wenn Prefix gleich geblieben String last = lastNametagPrefix.get(player.getUniqueId()); @@ -541,17 +615,16 @@ public class StatusAPIBridge extends JavaPlugin implements Listener { 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); - } + if (!team.hasEntry(player.getName())) { + team.addEntry(player.getName()); } + + // BUGFIX: Scoreboard NUR dem betroffenen Spieler selbst zuweisen. + // Das vorherige Verhalten hat bei jedem Nametag-Update das Scoreboard ALLER + // Online-Spieler überschrieben, was Scoreboards anderer Plugins (Sidebar etc.) zerstört. + // Clients sehen Nametag-Teams korrekt sobald BEIDE Spieler dasselbe Scoreboard haben – + // das wird beim Join jedes neuen Spielers via onJoin() automatisch gesetzt. + player.setScoreboard(nametagBoard); } catch (Exception e) { getLogger().warning("[Nametag] Fehler beim Setzen des Prefixes für " + player.getName() + ": " + e.getMessage()); } @@ -566,7 +639,9 @@ public class StatusAPIBridge extends JavaPlugin implements Listener { try { org.bukkit.scoreboard.Team team = nametagBoard.getTeam(teamName); if (team != null) { - team.removeEntry(player.getName()); + if (team.hasEntry(player.getName())) { + team.removeEntry(player.getName()); + } // Team löschen wenn leer if (team.getEntries().isEmpty()) team.unregister(); } @@ -656,6 +731,14 @@ public class StatusAPIBridge extends JavaPlugin implements Listener { return 20.0; } + private void submitHttp(Runnable task, String kind) { + try { + httpExecutor.execute(task); + } catch (RejectedExecutionException ex) { + getLogger().warning("HTTP-Queue voll, verwerfe " + kind + " Push."); + } + } + private void sendPost(String urlStr, String json) throws Exception { @SuppressWarnings("deprecation") URL url = new java.net.URI(urlStr).toURL(); @@ -676,6 +759,11 @@ public class StatusAPIBridge extends JavaPlugin implements Listener { private String escapeName(String name) { if (name == null) return ""; - return name.replace("\\", "\\\\").replace("\"", "\\\""); + // BUGFIX: Neben \ und " auch Steuerzeichen escapen (JSON-Injection-Schutz) + return name.replace("\\", "\\\\") + .replace("\"", "\\\"") + .replace("\n", "\\n") + .replace("\r", "\\r") + .replace("\t", "\\t"); } } \ No newline at end of file