Files
StatusAPI/StatusAPIBridge/src/main/java/net/viper/statusapibridge/StatusAPIBridge.java
2026-05-22 19:27:22 +02:00

681 lines
32 KiB
Java
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
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<UUID, Double> lastPushedBalance = new ConcurrentHashMap<>();
private final Map<UUID, Double> lastPushedHealth = new ConcurrentHashMap<>();
private final Map<UUID, String> lastPushedCompass = new ConcurrentHashMap<>();
private final Map<UUID, String> lastPushedWorld = new ConcurrentHashMap<>();
private final Map<UUID, String> lastPushedData = new ConcurrentHashMap<>();
private final Map<UUID, String> lastPushedStats = new ConcurrentHashMap<>();
private String lastPushedTicketGlobal = "";
// ── PlaceholderAPI ────────────────────────────────────────────────────────
private final Set<String> papiTokens = new java.util.LinkedHashSet<>();
private final Map<UUID, String> 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<UUID, String> 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<String> 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<Economy> 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("\"", "\\\"");
}
}