681 lines
32 KiB
Java
681 lines
32 KiB
Java
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("\"", "\\\"");
|
||
}
|
||
} |