Upload via GUI (4 Dateien)

This commit is contained in:
Git Manager GUI
2026-08-11 14:06:17 +02:00
parent d9e773b152
commit 44a1b6a2b7
2 changed files with 161 additions and 51 deletions
+31 -9
View File
@@ -6,14 +6,20 @@
<groupId>net.viper</groupId>
<artifactId>StatusAPIBridge</artifactId>
<version>1.0.2</version>
<version>1.0.3</version>
<packaging>jar</packaging>
<properties>
<!-- Niedrigste gemeinsame Basis: Java 17 (läuft auf 1.21.1 und 26.1.2) -->
<java.version>17</java.version>
<!--
Java 21 (LTS) kompatibel mit 1.21.x (benötigt mind. Java 21) und 26.1.x
(benötigt Java 25 zum Starten, akzeptiert aber Java-21-Bytecode problemlos,
da Java abwärtskompatibel ist).
Für einen reinen 26.1.x-Build kann java.version auf 25 gesetzt werden.
-->
<java.version>21</java.version>
<maven.compiler.source>${java.version}</maven.compiler.source>
<maven.compiler.target>${java.version}</maven.compiler.target>
<maven.compiler.release>${java.version}</maven.compiler.release>
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
<!-- Standard-API-Version (wird durch Profile überschrieben) -->
<spigot.version>1.21.1-R0.1-SNAPSHOT</spigot.version>
@@ -60,8 +66,9 @@
<!-- ══════════════════════════════════════════════════════════════════════
Maven-Profile für Multi-Version-Build
mvn package → mc-1.21.1 (Standard)
mvn package -P mc-26.1.2 → mc-26.1.2
mvn package → mc-1.21.1 (Standard, Java 21)
mvn package -P mc-26.1.2 → mc-26.1.2 (Java 21 Bytecode, läuft auf Java 25)
mvn package -P mc-26.1.2-j25 → mc-26.1.2 mit Java-25-Bytecode (benötigt JDK 25)
══════════════════════════════════════════════════════════════════════ -->
<profiles>
@@ -76,11 +83,25 @@
</properties>
</profile>
<!-- Profil 2: Minecraft 26.1.2 -->
<!-- Profil 2: Minecraft 26.1.2 kompiliert gegen 1.21.1-API (kompatibel),
erzeugt Java-21-Bytecode der auf Java 25 läuft -->
<profile>
<id>mc-26.1.2</id>
<properties>
<spigot.version>1.21.1-R0.1-SNAPSHOT</spigot.version>
<spigot.version>26.1.2-R0.1-SNAPSHOT</spigot.version>
</properties>
</profile>
<!-- Profil 3: Minecraft 26.1.2 mit nativem Java-25-Bytecode.
Benötigt JDK 25 als JAVA_HOME beim Build. -->
<profile>
<id>mc-26.1.2-j25</id>
<properties>
<spigot.version>26.1.2-R0.1-SNAPSHOT</spigot.version>
<java.version>25</java.version>
<maven.compiler.source>25</maven.compiler.source>
<maven.compiler.target>25</maven.compiler.target>
<maven.compiler.release>25</maven.compiler.release>
</properties>
</profile>
@@ -92,17 +113,18 @@
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-compiler-plugin</artifactId>
<version>3.13.0</version>
<version>3.14.0</version>
<configuration>
<source>${java.version}</source>
<target>${java.version}</target>
<release>${java.version}</release>
<encoding>UTF-8</encoding>
</configuration>
</plugin>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-shade-plugin</artifactId>
<version>3.5.1</version>
<version>3.6.0</version>
<executions>
<execution>
<phase>package</phase>
@@ -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<UUID, Double> lastPushedBalance = new ConcurrentHashMap<>();
private final Map<UUID, Double> lastPushedHealth = new ConcurrentHashMap<>();
@@ -40,6 +45,7 @@ public class StatusAPIBridge extends JavaPlugin implements Listener {
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 final Map<UUID, Long> lastCompassPushAt = new ConcurrentHashMap<>();
private String lastPushedTicketGlobal = "";
// ── PlaceholderAPI ────────────────────────────────────────────────────────
@@ -54,17 +60,31 @@ public class StatusAPIBridge extends JavaPlugin implements Listener {
private final Map<UUID, String> 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<UUID, Boolean> 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 -> {
// 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
if (!team.hasEntry(player.getName())) {
team.addEntry(player.getName());
}
// Scoreboard dem Spieler zuweisen
// 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);
// 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());
}
@@ -566,7 +639,9 @@ public class StatusAPIBridge extends JavaPlugin implements Listener {
try {
org.bukkit.scoreboard.Team team = nametagBoard.getTeam(teamName);
if (team != null) {
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");
}
}