Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
44a1b6a2b7 | ||
|
|
d9e773b152 |
+24
-7
@@ -7,15 +7,21 @@
|
||||
|
||||
<groupId>net.viper.bungee</groupId>
|
||||
<artifactId>StatusAPI</artifactId>
|
||||
<version>4.1.4</version>
|
||||
<version>4.1.5</version>
|
||||
<packaging>jar</packaging>
|
||||
|
||||
<name>StatusAPI</name>
|
||||
<description>BungeeCord Status API Plugin</description>
|
||||
|
||||
<properties>
|
||||
<maven.compiler.source>8</maven.compiler.source>
|
||||
<maven.compiler.target>8</maven.compiler.target>
|
||||
<!--
|
||||
Java 21 (LTS) – BungeeCord setzt seit 26.1.x mind. Java 17 voraus,
|
||||
empfohlen ist Java 21 (LTS). Java-21-Bytecode läuft problemlos auf
|
||||
Java 25-Servern (abwärtskompatibel).
|
||||
-->
|
||||
<maven.compiler.source>21</maven.compiler.source>
|
||||
<maven.compiler.target>21</maven.compiler.target>
|
||||
<maven.compiler.release>21</maven.compiler.release>
|
||||
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
|
||||
</properties>
|
||||
|
||||
@@ -42,7 +48,7 @@
|
||||
<dependency>
|
||||
<groupId>com.zaxxer</groupId>
|
||||
<artifactId>HikariCP</artifactId>
|
||||
<version>5.1.0</version>
|
||||
<version>6.2.1</version>
|
||||
<scope>compile</scope>
|
||||
</dependency>
|
||||
|
||||
@@ -50,7 +56,7 @@
|
||||
<dependency>
|
||||
<groupId>com.mysql</groupId>
|
||||
<artifactId>mysql-connector-j</artifactId>
|
||||
<version>9.1.0</version>
|
||||
<version>9.2.0</version>
|
||||
<scope>compile</scope>
|
||||
</dependency>
|
||||
</dependencies>
|
||||
@@ -77,10 +83,21 @@
|
||||
</resources>
|
||||
|
||||
<plugins>
|
||||
<plugin>
|
||||
<groupId>org.apache.maven.plugins</groupId>
|
||||
<artifactId>maven-compiler-plugin</artifactId>
|
||||
<version>3.14.0</version>
|
||||
<configuration>
|
||||
<source>21</source>
|
||||
<target>21</target>
|
||||
<release>21</release>
|
||||
<encoding>UTF-8</encoding>
|
||||
</configuration>
|
||||
</plugin>
|
||||
<plugin>
|
||||
<groupId>org.apache.maven.plugins</groupId>
|
||||
<artifactId>maven-shade-plugin</artifactId>
|
||||
<version>3.5.0</version>
|
||||
<version>3.6.0</version>
|
||||
<executions>
|
||||
<execution>
|
||||
<phase>package</phase>
|
||||
@@ -104,4 +121,4 @@
|
||||
</plugins>
|
||||
</build>
|
||||
|
||||
</project>
|
||||
</project>
|
||||
|
||||
@@ -3,7 +3,17 @@ package net.viper.status;
|
||||
import net.md_5.bungee.api.ProxyServer;
|
||||
import net.md_5.bungee.api.config.ListenerInfo;
|
||||
import net.md_5.bungee.api.connection.ProxiedPlayer;
|
||||
import net.md_5.bungee.api.chat.ClickEvent;
|
||||
import net.md_5.bungee.api.chat.HoverEvent;
|
||||
import net.md_5.bungee.api.chat.hover.content.Text;
|
||||
import net.md_5.bungee.api.chat.ClickEvent;
|
||||
import net.md_5.bungee.api.chat.HoverEvent;
|
||||
import net.md_5.bungee.api.chat.hover.content.Text;
|
||||
import net.md_5.bungee.api.event.PlayerDisconnectEvent;
|
||||
import net.md_5.bungee.api.event.PostLoginEvent;
|
||||
import net.md_5.bungee.api.plugin.Listener;
|
||||
import net.md_5.bungee.api.plugin.Plugin;
|
||||
import net.md_5.bungee.event.EventHandler;
|
||||
import net.viper.status.module.ModuleManager;
|
||||
import net.viper.status.modules.economy.EconomyModule;
|
||||
import net.viper.status.modules.tablist.TablistModule;
|
||||
@@ -48,7 +58,7 @@ import net.md_5.bungee.api.scheduler.ScheduledTask;
|
||||
/**
|
||||
* StatusAPI - zentraler BungeeCord HTTP-Status- und Broadcast-Endpunkt
|
||||
*/
|
||||
public class StatusAPI extends Plugin implements Runnable {
|
||||
public class StatusAPI extends Plugin implements Runnable, Listener {
|
||||
|
||||
// Welt pro Spieler (UUID -> Weltname), wird von StatusAPIBridge gepusht
|
||||
public static final ConcurrentHashMap<UUID, String> playerWorlds = new ConcurrentHashMap<>();
|
||||
@@ -168,6 +178,9 @@ public class StatusAPI extends Plugin implements Runnable {
|
||||
getLogger().info("[PlayerLoginLogger] Login-Logging aktiv -> " + getDataFolder() + "/player-logins.log (Zeitzone: " + loginZone + ")");
|
||||
}
|
||||
|
||||
// Memory-Leak-Fix: playerWorlds/playerPapi beim Disconnect bereinigen
|
||||
ProxyServer.getInstance().getPluginManager().registerListener(this, this);
|
||||
|
||||
// FIX: ScoreboardModule mit NetworkInfoModule verbinden (TPS-Fallback)
|
||||
try {
|
||||
net.viper.status.modules.scoreboard.ScoreboardModule sbMod =
|
||||
@@ -291,17 +304,107 @@ public class StatusAPI extends Plugin implements Runnable {
|
||||
updateChecker.checkNow();
|
||||
String currentVersion = getDescription() != null ? getDescription().getVersion() : "0.0.0";
|
||||
if (updateChecker.isUpdateAvailable(currentVersion)) {
|
||||
String newVersion = updateChecker.getLatestVersion();
|
||||
getLogger().warning("----------------------------------------");
|
||||
getLogger().warning("Neue Version verf\u00fcgbar: " + newVersion);
|
||||
getLogger().warning("Download: " + updateChecker.getLatestUrl());
|
||||
getLogger().warning("----------------------------------------");
|
||||
String newVersion = updateChecker.getLatestVersion();
|
||||
String releaseUrl = updateChecker.getLatestUrl();
|
||||
boolean preRelease = updateChecker.isLatestPreRelease();
|
||||
// Konsolen-Log
|
||||
getLogger().warning((preRelease ? "[Pre-Release] " : "") + "Update verfuegbar: " + newVersion + " -> " + releaseUrl);
|
||||
// Ingame-Meldung an alle Spieler mit statusapi.update.notify
|
||||
broadcastUpdateNotice(newVersion, releaseUrl, preRelease);
|
||||
}
|
||||
} catch (Exception e) {
|
||||
getLogger().severe("Fehler beim Update-Check: " + e.getMessage());
|
||||
}
|
||||
}
|
||||
|
||||
/** Sendet die Update-Meldung an alle online Spieler mit statusapi.update.notify */
|
||||
private void broadcastUpdateNotice(String newVersion, String releaseUrl, boolean preRelease) {
|
||||
String current = getDescription() != null ? getDescription().getVersion() : "?";
|
||||
sendUpdateMessage(ProxyServer.getInstance().getConsole(), newVersion, current, releaseUrl, preRelease);
|
||||
for (net.md_5.bungee.api.connection.ProxiedPlayer p : ProxyServer.getInstance().getPlayers()) {
|
||||
if (p.hasPermission("statusapi.update.notify")) {
|
||||
sendUpdateMessage(p, newVersion, current, releaseUrl, preRelease);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private static final String RELEASES_URL = "https://git.viper.ipv64.net/M_Viper/StatusAPI/releases";
|
||||
|
||||
/** Formatiert und schickt die Update-Meldung an einen einzelnen Empfaenger */
|
||||
private static void sendUpdateMessage(net.md_5.bungee.api.CommandSender target,
|
||||
String newVersion, String currentVersion, String releaseUrl,
|
||||
boolean preRelease) {
|
||||
// Einfache Textzeilen
|
||||
String typeLabel = preRelease ? "&e&lPre-Release" : "&a&lRelease";
|
||||
String typeNotice = preRelease ? "&eVorsicht: Dies ist ein Pre-Release und kann instabil sein!" : "";
|
||||
String[] plainLines = preRelease ? new String[]{
|
||||
"&8&m" + repeat("-", 44),
|
||||
"&6&l StatusAPI &7\u2013 &e&lPre-Release verf\u00fcgbar!",
|
||||
"&7Aktuelle Version: &c" + currentVersion,
|
||||
"&7Neue Version: &e" + newVersion + " &7[Pre-Release]",
|
||||
"&eVorsicht: &7Kann instabil sein, bitte testen!",
|
||||
} : new String[]{
|
||||
"&8&m" + repeat("-", 44),
|
||||
"&6&l StatusAPI &7\u2013 Update verf\u00fcgbar!",
|
||||
"&7Aktuelle Version: &c" + currentVersion,
|
||||
"&7Neue Version: &a" + newVersion,
|
||||
};
|
||||
for (String line : plainLines) {
|
||||
target.sendMessage(new net.md_5.bungee.api.chat.TextComponent(
|
||||
net.md_5.bungee.api.ChatColor.translateAlternateColorCodes('&', line)));
|
||||
}
|
||||
|
||||
// Klickbare Link-Zeile (nur fuer ProxiedPlayer, Konsole bekommt Plaintext)
|
||||
if (target instanceof net.md_5.bungee.api.connection.ProxiedPlayer) {
|
||||
net.md_5.bungee.api.chat.TextComponent prefix = new net.md_5.bungee.api.chat.TextComponent(
|
||||
net.md_5.bungee.api.ChatColor.translateAlternateColorCodes('&', "&7Releases: "));
|
||||
net.md_5.bungee.api.chat.TextComponent link = new net.md_5.bungee.api.chat.TextComponent(
|
||||
net.md_5.bungee.api.ChatColor.translateAlternateColorCodes('&', "&b&n" + RELEASES_URL));
|
||||
link.setClickEvent(new ClickEvent(ClickEvent.Action.OPEN_URL, RELEASES_URL));
|
||||
link.setHoverEvent(new HoverEvent(HoverEvent.Action.SHOW_TEXT,
|
||||
new Text(net.md_5.bungee.api.ChatColor.translateAlternateColorCodes('&',
|
||||
"&7Klicke um die Release-Seite zu \u00f6ffnen"))));
|
||||
prefix.addExtra(link);
|
||||
target.sendMessage(prefix);
|
||||
} else {
|
||||
target.sendMessage(new net.md_5.bungee.api.chat.TextComponent(
|
||||
net.md_5.bungee.api.ChatColor.translateAlternateColorCodes('&', "&7Releases: &b" + RELEASES_URL)));
|
||||
}
|
||||
|
||||
target.sendMessage(new net.md_5.bungee.api.chat.TextComponent(
|
||||
net.md_5.bungee.api.ChatColor.translateAlternateColorCodes('&', "&8&m" + repeat("-", 44))));
|
||||
}
|
||||
|
||||
private static String repeat(String s, int n) {
|
||||
StringBuilder sb = new StringBuilder();
|
||||
for (int i = 0; i < n; i++) sb.append(s);
|
||||
return sb.toString();
|
||||
}
|
||||
|
||||
// Memory-Leak-Fix: playerWorlds und playerPapi beim Spieler-Logout bereinigen.
|
||||
// Diese Maps werden über HTTP von der Bridge befüllt und haben sonst keinen
|
||||
// Disconnect-Handler, was zu unbegrenztem Wachstum führt.
|
||||
@EventHandler
|
||||
public void onPlayerDisconnect(PlayerDisconnectEvent event) {
|
||||
UUID id = event.getPlayer().getUniqueId();
|
||||
playerWorlds.remove(id);
|
||||
playerPapi.remove(id);
|
||||
}
|
||||
|
||||
/** Update-Hinweis beim Login für Spieler mit statusapi.update.notify */
|
||||
@EventHandler
|
||||
public void onPostLogin(PostLoginEvent event) {
|
||||
net.md_5.bungee.api.connection.ProxiedPlayer p = event.getPlayer();
|
||||
if (!p.hasPermission("statusapi.update.notify")) return;
|
||||
String current = getDescription() != null ? getDescription().getVersion() : "0.0.0";
|
||||
if (updateChecker != null && updateChecker.isUpdateAvailable(current)) {
|
||||
// Kurze Verzögerung damit der Spieler erst spawnt
|
||||
ProxyServer.getInstance().getScheduler().schedule(this, () ->
|
||||
sendUpdateMessage(p, updateChecker.getLatestVersion(), current, updateChecker.getLatestUrl(), updateChecker.isLatestPreRelease()),
|
||||
3, TimeUnit.SECONDS);
|
||||
}
|
||||
}
|
||||
|
||||
// --- WebServer ---
|
||||
@Override
|
||||
public void run() {
|
||||
@@ -1118,13 +1221,19 @@ public class StatusAPI extends Plugin implements Runnable {
|
||||
|
||||
/**
|
||||
* Liest den HTTP-Body basierend auf Content-Length.
|
||||
* Max. 1 MB – schützt vor überdimensionierten Requests (DoS via Content-Length).
|
||||
*/
|
||||
private static final int MAX_BODY_SIZE = 1024 * 1024; // 1 MB
|
||||
private String readBody(BufferedReader in, Map<String, String> headers) throws IOException {
|
||||
int contentLength = 0;
|
||||
if (headers.containsKey("content-length")) {
|
||||
try { contentLength = Integer.parseInt(headers.get("content-length")); } catch (NumberFormatException ignored) {}
|
||||
}
|
||||
if (contentLength <= 0) return "";
|
||||
if (contentLength > MAX_BODY_SIZE) {
|
||||
getLogger().warning("[StatusAPI] Request abgelehnt: Content-Length " + contentLength + " > " + MAX_BODY_SIZE + " Bytes.");
|
||||
return "";
|
||||
}
|
||||
char[] bodyChars = new char[contentLength];
|
||||
int read = 0;
|
||||
while (read < contentLength) {
|
||||
@@ -1435,21 +1544,28 @@ public class StatusAPI extends Plugin implements Runnable {
|
||||
* Alle anderen Module (Chat, AntiBot, etc.) bleiben unber\u00fchrt.
|
||||
*/
|
||||
public void reloadModules() {
|
||||
getLogger().info("[StatusAPI] Reload von Scoreboard und Tablist...");
|
||||
getLogger().info("[StatusAPI] Reload von Scoreboard, Tablist und AFK...");
|
||||
|
||||
net.viper.status.module.Module sbMod = moduleManager.getModule("ScoreboardModule");
|
||||
net.viper.status.module.Module tabMod = moduleManager.getModule("TablistModule");
|
||||
net.viper.status.module.Module afkMod = moduleManager.getModule("AfkModule");
|
||||
|
||||
if (sbMod != null) sbMod.onDisable(this);
|
||||
if (tabMod != null) tabMod.onDisable(this);
|
||||
// AfkModule meldet Listener, Command und Plugin-Channel in onDisable sauber ab
|
||||
// und setzt laufende AFK-Zustaende auf den Spigot-Servern zurueck.
|
||||
if (afkMod != null) afkMod.onDisable(this);
|
||||
|
||||
// Neue Instanzen erstellen und registrieren
|
||||
net.viper.status.modules.scoreboard.ScoreboardModule newSb = new net.viper.status.modules.scoreboard.ScoreboardModule();
|
||||
net.viper.status.modules.tablist.TablistModule newTab = new net.viper.status.modules.tablist.TablistModule();
|
||||
net.viper.status.modules.afk.AfkModule newAfk = new net.viper.status.modules.afk.AfkModule();
|
||||
|
||||
moduleManager.replaceModule("ScoreboardModule", newSb);
|
||||
moduleManager.replaceModule("TablistModule", newTab);
|
||||
moduleManager.replaceModule("AfkModule", newAfk);
|
||||
|
||||
newAfk.onEnable(this);
|
||||
newSb.onEnable(this);
|
||||
newTab.onEnable(this);
|
||||
|
||||
@@ -1483,7 +1599,7 @@ public class StatusAPI extends Plugin implements Runnable {
|
||||
send(sender, "&8&m──────────────────────────────────────────");
|
||||
send(sender, "&6&lStatusAPI &7| Befehle");
|
||||
if (isAdmin) {
|
||||
send(sender, "&e/statusapi reload &7– Scoreboard & Tablist neu laden");
|
||||
send(sender, "&e/statusapi reload &7– Scoreboard, Tablist & AFK neu laden");
|
||||
} else {
|
||||
send(sender, "&7Keine weiteren Unterbefehle verf\u00fcgbar.");
|
||||
}
|
||||
|
||||
@@ -19,12 +19,13 @@ public class UpdateChecker {
|
||||
// Neue Domain und korrekter API-Pfad f\u00fcr Releases
|
||||
private final String apiUrl = "https://git.viper.ipv64.net/api/v1/repos/M_Viper/StatusAPI/releases";
|
||||
|
||||
private volatile String latestVersion = "";
|
||||
private volatile String latestUrl = "";
|
||||
private volatile String latestVersion = "";
|
||||
private volatile String latestUrl = "";
|
||||
private volatile boolean latestPreRelease = false;
|
||||
|
||||
private static final Pattern ASSET_NAME_PATTERN = Pattern.compile("\"name\"\\s*:\\s*\"([^\"]+)\"", Pattern.CASE_INSENSITIVE);
|
||||
private static final Pattern DOWNLOAD_PATTERN = Pattern.compile("\"browser_download_url\"\\s*:\\s*\"([^\"]+)\"", Pattern.CASE_INSENSITIVE);
|
||||
private static final Pattern TAG_NAME_PATTERN = Pattern.compile("\"tag_name\"\\s*:\\s*\"([^\"]+)\"", Pattern.CASE_INSENSITIVE);
|
||||
private static final Pattern TAG_NAME_PATTERN = Pattern.compile("\"tag_name\"\\s*:\\s*\"([^\"]+)\"", Pattern.CASE_INSENSITIVE);
|
||||
private static final Pattern HTML_URL_PATTERN = Pattern.compile("\"html_url\"\\s*:\\s*\"([^\"]+)\"", Pattern.CASE_INSENSITIVE);
|
||||
private static final Pattern PRERELEASE_PATTERN = Pattern.compile("\"prerelease\"\\s*:\\s*(true|false)", Pattern.CASE_INSENSITIVE);
|
||||
|
||||
public UpdateChecker(Plugin plugin, String currentVersion, int intervalHours) {
|
||||
this.plugin = plugin;
|
||||
@@ -71,39 +72,23 @@ public class UpdateChecker {
|
||||
foundVersion = foundVersion.substring(1);
|
||||
}
|
||||
|
||||
String foundUrl = null;
|
||||
|
||||
// Wir suchen im gesamten Body nach der JAR-Datei "StatusAPI.jar"
|
||||
// Da das neueste Release zuerst kommt, brechen wir ab, sobald wir eine passende JAR finden
|
||||
Matcher nameMatcher = ASSET_NAME_PATTERN.matcher(body);
|
||||
Matcher downloadMatcher = DOWNLOAD_PATTERN.matcher(body);
|
||||
|
||||
java.util.List<String> names = new java.util.ArrayList<>();
|
||||
java.util.List<String> urls = new java.util.ArrayList<>();
|
||||
|
||||
while (nameMatcher.find()) {
|
||||
names.add(nameMatcher.group(1));
|
||||
}
|
||||
while (downloadMatcher.find()) {
|
||||
urls.add(downloadMatcher.group(1));
|
||||
// Release-Seiten-URL fuer die Ingame-Meldung
|
||||
String foundUrl = "https://git.viper.ipv64.net/M_Viper/StatusAPI/releases";
|
||||
Matcher urlM = HTML_URL_PATTERN.matcher(body);
|
||||
if (urlM.find()) {
|
||||
foundUrl = urlM.group(1).trim();
|
||||
}
|
||||
|
||||
int pairs = Math.min(names.size(), urls.size());
|
||||
for (int i = 0; i < pairs; i++) {
|
||||
String name = names.get(i).trim();
|
||||
String url = urls.get(i);
|
||||
if ("StatusAPI.jar".equalsIgnoreCase(name)) {
|
||||
foundUrl = url;
|
||||
break; // Erste (also neueste) passende JAR nehmen
|
||||
}
|
||||
// prerelease-Flag aus dem ersten Release-Block lesen
|
||||
boolean foundPreRelease = false;
|
||||
Matcher preM = PRERELEASE_PATTERN.matcher(body);
|
||||
if (preM.find()) {
|
||||
foundPreRelease = Boolean.parseBoolean(preM.group(1).trim());
|
||||
}
|
||||
|
||||
if (foundUrl == null) {
|
||||
plugin.getLogger().warning("Keine StatusAPI.jar im neuesten Release gefunden.");
|
||||
return;
|
||||
}
|
||||
latestVersion = foundVersion;
|
||||
latestUrl = foundUrl;
|
||||
latestVersion = foundVersion;
|
||||
latestUrl = foundUrl;
|
||||
latestPreRelease = foundPreRelease;
|
||||
|
||||
} catch (Exception e) {
|
||||
plugin.getLogger().log(Level.SEVERE, "Fehler beim Update-Check: " + e.getMessage(), e);
|
||||
@@ -118,9 +103,15 @@ public class UpdateChecker {
|
||||
return latestUrl != null ? latestUrl : "";
|
||||
}
|
||||
|
||||
public boolean isLatestPreRelease() {
|
||||
return latestPreRelease;
|
||||
}
|
||||
|
||||
public boolean isUpdateAvailable(String currentVer) {
|
||||
String lv = getLatestVersion();
|
||||
if (lv.isEmpty()) return false;
|
||||
// Nur melden wenn latest STRIKT groesser als current ist.
|
||||
// Laeuft eine Dev-Version (current > latest), kein Hinweis.
|
||||
return compareVersions(lv, currentVer) > 0;
|
||||
}
|
||||
|
||||
|
||||
+127
-6
@@ -3,6 +3,7 @@ package net.viper.status.modules.AutoMessage;
|
||||
import net.md_5.bungee.api.ChatColor;
|
||||
import net.md_5.bungee.api.CommandSender;
|
||||
import net.md_5.bungee.api.ProxyServer;
|
||||
import net.md_5.bungee.api.chat.BaseComponent;
|
||||
import net.md_5.bungee.api.chat.TextComponent;
|
||||
import net.md_5.bungee.api.plugin.Command;
|
||||
import net.md_5.bungee.api.plugin.Plugin;
|
||||
@@ -13,6 +14,7 @@ import java.io.File;
|
||||
import java.io.IOException;
|
||||
import java.nio.charset.StandardCharsets;
|
||||
import java.nio.file.Files;
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
import java.util.Properties;
|
||||
import java.util.concurrent.TimeUnit;
|
||||
@@ -145,12 +147,13 @@ public class AutoMessageModule implements Module {
|
||||
if (idx >= messages.size()) idx = 0;
|
||||
|
||||
String raw = messages.get(idx);
|
||||
String prefixPart = prefix.isEmpty() ? "" : ChatColor.translateAlternateColorCodes('&', prefix) + " ";
|
||||
// Fix: §-Codes direkt \u00fcbersetzen (messages.txt nutzt §-Codes)
|
||||
String text = prefixPart + ChatColor.translateAlternateColorCodes('&',
|
||||
raw.replace("\u00a7", "&").replace("§", "&"));
|
||||
String prefixPart = prefix.isEmpty() ? "" : ChatColor.translateAlternateColorCodes('&', applyGradients(prefix)) + " ";
|
||||
// Gradient zuerst auflösen, dann §/&-Codes übersetzen
|
||||
String normalized = raw.replace("\u00a7", "&").replace("§", "&");
|
||||
String text = prefixPart + ChatColor.translateAlternateColorCodes('&', applyGradients(normalized));
|
||||
|
||||
ProxyServer.getInstance().broadcast(new TextComponent(text));
|
||||
BaseComponent[] components = TextComponent.fromLegacyText(text);
|
||||
ProxyServer.getInstance().broadcast(components);
|
||||
}, intervalSeconds, intervalSeconds, TimeUnit.SECONDS).getId();
|
||||
}
|
||||
|
||||
@@ -160,4 +163,122 @@ public class AutoMessageModule implements Module {
|
||||
taskId = -1;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// ── Gradient-Support (%gradient:FARBE1:FARBE2:...:TEXT%) ─────────────────
|
||||
|
||||
private String applyGradients(String input) {
|
||||
if (input == null || !input.contains("%gradient:")) return input;
|
||||
StringBuilder result = new StringBuilder();
|
||||
int i = 0;
|
||||
while (i < input.length()) {
|
||||
int start = input.indexOf("%gradient:", i);
|
||||
if (start < 0) { result.append(input.substring(i)); break; }
|
||||
result.append(input, i, start);
|
||||
int end = input.indexOf("%", start + 10);
|
||||
if (end < 0) { result.append(input.substring(start)); break; }
|
||||
String inner = input.substring(start + 10, end);
|
||||
List<int[]> stops = new ArrayList<>();
|
||||
int colonIdx = 0;
|
||||
while (colonIdx < inner.length()) {
|
||||
int nextColon = inner.indexOf(':', colonIdx);
|
||||
if (nextColon < 0) break;
|
||||
String candidate = inner.substring(colonIdx, nextColon);
|
||||
int[] rgb = parseGradientColor(candidate);
|
||||
if (rgb != null) { stops.add(rgb); colonIdx = nextColon + 1; }
|
||||
else break;
|
||||
}
|
||||
if (stops.size() < 2) { result.append(input, start, end + 1); i = end + 1; continue; }
|
||||
String text = inner.substring(colonIdx);
|
||||
String plain = ChatColor.stripColor(ChatColor.translateAlternateColorCodes('&', text));
|
||||
boolean bold = text.contains("&l") || text.contains("\u00A7l");
|
||||
boolean italic = text.contains("&o") || text.contains("\u00A7o");
|
||||
boolean underline = text.contains("&n") || text.contains("\u00A7n");
|
||||
boolean strike = text.contains("&m") || text.contains("\u00A7m");
|
||||
String fmt = (bold ? "\u00A7l" : "") + (italic ? "\u00A7o" : "")
|
||||
+ (underline ? "\u00A7n" : "") + (strike ? "\u00A7m" : "");
|
||||
int visLen = 0;
|
||||
for (char ch : plain.toCharArray()) if (ch != ' ') visLen++;
|
||||
if (visLen == 0) visLen = 1;
|
||||
int charIdx = 0;
|
||||
int[] lastRgb = stops.get(0);
|
||||
for (char ch : plain.toCharArray()) {
|
||||
if (ch == ' ') {
|
||||
result.append('\u00A7').append('x');
|
||||
result.append('\u00A7').append(String.format("%02X", lastRgb[0]).charAt(0));
|
||||
result.append('\u00A7').append(String.format("%02X", lastRgb[0]).charAt(1));
|
||||
result.append('\u00A7').append(String.format("%02X", lastRgb[1]).charAt(0));
|
||||
result.append('\u00A7').append(String.format("%02X", lastRgb[1]).charAt(1));
|
||||
result.append('\u00A7').append(String.format("%02X", lastRgb[2]).charAt(0));
|
||||
result.append('\u00A7').append(String.format("%02X", lastRgb[2]).charAt(1));
|
||||
result.append(fmt).append(ch);
|
||||
continue;
|
||||
}
|
||||
float pos = visLen <= 1 ? 0f : (float) charIdx / (visLen - 1);
|
||||
lastRgb = interpolateGradient(stops, pos);
|
||||
result.append('\u00A7').append('x');
|
||||
result.append('\u00A7').append(String.format("%02X", lastRgb[0]).charAt(0));
|
||||
result.append('\u00A7').append(String.format("%02X", lastRgb[0]).charAt(1));
|
||||
result.append('\u00A7').append(String.format("%02X", lastRgb[1]).charAt(0));
|
||||
result.append('\u00A7').append(String.format("%02X", lastRgb[1]).charAt(1));
|
||||
result.append('\u00A7').append(String.format("%02X", lastRgb[2]).charAt(0));
|
||||
result.append('\u00A7').append(String.format("%02X", lastRgb[2]).charAt(1));
|
||||
result.append(fmt).append(ch);
|
||||
charIdx++;
|
||||
}
|
||||
// §r nach dem Gradient-Block, damit nachfolgende §8/§7 etc. wieder greifen
|
||||
result.append('\u00A7').append('r');
|
||||
i = end + 1;
|
||||
}
|
||||
return result.toString();
|
||||
}
|
||||
|
||||
private int[] parseGradientColor(String s) {
|
||||
s = s.trim();
|
||||
if (s.startsWith("&#")) s = s.substring(1);
|
||||
if (s.startsWith("#") && s.length() == 7) {
|
||||
try {
|
||||
return new int[]{
|
||||
Integer.parseInt(s.substring(1,3),16),
|
||||
Integer.parseInt(s.substring(3,5),16),
|
||||
Integer.parseInt(s.substring(5,7),16)
|
||||
};
|
||||
} catch (Exception ignored) {}
|
||||
}
|
||||
if (s.startsWith("&") && s.length() == 2) return mcColorToRgb(s.charAt(1));
|
||||
return null;
|
||||
}
|
||||
|
||||
private int[] interpolateGradient(List<int[]> stops, float pos) {
|
||||
if (stops.size() == 1) return stops.get(0);
|
||||
float scaled = pos * (stops.size() - 1);
|
||||
int i0 = Math.min((int) scaled, stops.size() - 2);
|
||||
float t = scaled - i0;
|
||||
return new int[]{
|
||||
(int)(stops.get(i0)[0] * (1-t) + stops.get(i0+1)[0] * t),
|
||||
(int)(stops.get(i0)[1] * (1-t) + stops.get(i0+1)[1] * t),
|
||||
(int)(stops.get(i0)[2] * (1-t) + stops.get(i0+1)[2] * t)
|
||||
};
|
||||
}
|
||||
|
||||
private static int[] mcColorToRgb(char code) {
|
||||
switch (Character.toLowerCase(code)) {
|
||||
case '0': return new int[]{ 0, 0, 0};
|
||||
case '1': return new int[]{ 0, 0, 170};
|
||||
case '2': return new int[]{ 0, 170, 0};
|
||||
case '3': return new int[]{ 0, 170, 170};
|
||||
case '4': return new int[]{170, 0, 0};
|
||||
case '5': return new int[]{170, 0, 170};
|
||||
case '6': return new int[]{255, 170, 0};
|
||||
case '7': return new int[]{170, 170, 170};
|
||||
case '8': return new int[]{ 85, 85, 85};
|
||||
case '9': return new int[]{ 85, 85, 255};
|
||||
case 'a': return new int[]{ 85, 255, 85};
|
||||
case 'b': return new int[]{ 85, 255, 255};
|
||||
case 'c': return new int[]{255, 85, 85};
|
||||
case 'd': return new int[]{255, 85, 255};
|
||||
case 'e': return new int[]{255, 255, 85};
|
||||
case 'f': return new int[]{255, 255, 255};
|
||||
default: return null;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -20,6 +20,7 @@ import net.viper.status.module.Module;
|
||||
import java.util.*;
|
||||
import java.util.concurrent.ConcurrentHashMap;
|
||||
import java.util.concurrent.TimeUnit;
|
||||
import java.util.logging.Level;
|
||||
|
||||
/**
|
||||
* AfkModule – /afk Befehl + automatische AFK-Erkennung nach Inaktivit\u00e4t.
|
||||
@@ -39,6 +40,7 @@ public class AfkModule implements Module, Listener {
|
||||
|
||||
private Plugin plugin;
|
||||
private ScheduledTask idleCheckTask;
|
||||
private Command afkCommand;
|
||||
private static AfkModule INSTANCE;
|
||||
private java.lang.reflect.Method sendPkt;
|
||||
|
||||
@@ -91,17 +93,19 @@ public class AfkModule implements Module, Listener {
|
||||
if (!enabled) { plugin.getLogger().info("[AfkModule] Deaktiviert."); return; }
|
||||
|
||||
ProxyServer.getInstance().getPluginManager().registerListener(plugin, this);
|
||||
ProxyServer.getInstance().getPluginManager().registerCommand(plugin,
|
||||
new Command("afk") {
|
||||
@Override
|
||||
public void execute(CommandSender sender, String[] args) {
|
||||
if (!(sender instanceof ProxiedPlayer)) {
|
||||
sender.sendMessage(plain("&cNur f\u00fcr Spieler."));
|
||||
return;
|
||||
}
|
||||
toggleAfk((ProxiedPlayer) sender);
|
||||
// Plugin-Message-Channel registrieren (BungeeCord → Spigot)
|
||||
ProxyServer.getInstance().registerChannel("statusapi:afk");
|
||||
afkCommand = new Command("afk") {
|
||||
@Override
|
||||
public void execute(CommandSender sender, String[] args) {
|
||||
if (!(sender instanceof ProxiedPlayer)) {
|
||||
sender.sendMessage(plain("&cNur f\u00fcr Spieler."));
|
||||
return;
|
||||
}
|
||||
});
|
||||
toggleAfk((ProxiedPlayer) sender);
|
||||
}
|
||||
};
|
||||
ProxyServer.getInstance().getPluginManager().registerCommand(plugin, afkCommand);
|
||||
|
||||
if (idleEnabled) {
|
||||
idleCheckTask = ProxyServer.getInstance().getScheduler().schedule(
|
||||
@@ -119,9 +123,28 @@ public class AfkModule implements Module, Listener {
|
||||
titleTasks.clear();
|
||||
activeTitleEntry.clear();
|
||||
activePair.clear();
|
||||
|
||||
// Die Spigot-Seite (StatusAPIBridge) fuehrt eine eigene afkState-Map und
|
||||
// setzt daraus den [AFK]-Nametag. Wird hier nur lokal geleert, bleibt der
|
||||
// Prefix nach einem Reload ueber dem Kopf haengen, weil die Bridge nie ein
|
||||
// Update auf false bekommt. Darum vor dem Leeren explizit zuruecksetzen.
|
||||
for (UUID id : new ArrayList<>(StatusAPI.playerAfk.keySet())) {
|
||||
ProxiedPlayer p = ProxyServer.getInstance().getPlayer(id);
|
||||
if (p != null) { try { clearTitle(p); } catch (Exception ignored) {} }
|
||||
try { broadcastAfkState(id, false); } catch (Exception ignored) {}
|
||||
}
|
||||
StatusAPI.playerAfk.clear();
|
||||
lastActivity.clear();
|
||||
INSTANCE = null;
|
||||
|
||||
// Listener und Command wieder abmelden, damit eine neue Instanz (Reload)
|
||||
// sauber registrieren kann und keine Doppel-Registrierung entsteht.
|
||||
try { ProxyServer.getInstance().getPluginManager().unregisterListener(this); } catch (Exception ignored) {}
|
||||
if (afkCommand != null) {
|
||||
try { ProxyServer.getInstance().getPluginManager().unregisterCommand(afkCommand); } catch (Exception ignored) {}
|
||||
afkCommand = null;
|
||||
}
|
||||
try { ProxyServer.getInstance().unregisterChannel("statusapi:afk"); } catch (Exception ignored) {}
|
||||
}
|
||||
|
||||
// ── Events ───────────────────────────────────────────────────────────────
|
||||
@@ -131,7 +154,9 @@ public class AfkModule implements Module, Listener {
|
||||
if (!(e.getSender() instanceof ProxiedPlayer)) return;
|
||||
ProxiedPlayer p = (ProxiedPlayer) e.getSender();
|
||||
recordActivity(p.getUniqueId());
|
||||
if (!e.getMessage().toLowerCase().startsWith("/afk") && isAfk(p.getUniqueId()))
|
||||
String msg = e.getMessage();
|
||||
if (msg == null) return;
|
||||
if (!msg.toLowerCase(Locale.ROOT).startsWith("/afk") && isAfk(p.getUniqueId()))
|
||||
setAfk(p, false);
|
||||
}
|
||||
|
||||
@@ -146,7 +171,21 @@ public class AfkModule implements Module, Listener {
|
||||
|
||||
// ── Public API ───────────────────────────────────────────────────────────
|
||||
|
||||
/**
|
||||
* Setzt den AFK-Status aus einer externen Quelle (POST /player/afk der Bridge).
|
||||
*
|
||||
* Laeuft ueber denselben Weg wie /afk und die Idle-Erkennung, sonst waere der
|
||||
* Status zwar gesetzt, aber ohne Title und ohne Broadcast an die Spigot-Server –
|
||||
* der [AFK]-Nametag wuerde dann nicht mitziehen.
|
||||
*/
|
||||
public static void setBridgeAfk(UUID uuid, boolean afk) {
|
||||
AfkModule inst = INSTANCE;
|
||||
ProxiedPlayer p = ProxyServer.getInstance().getPlayer(uuid);
|
||||
if (inst != null && p != null) {
|
||||
inst.setAfk(p, afk);
|
||||
return;
|
||||
}
|
||||
// Spieler nicht auf dem Proxy erreichbar: nur den Status nachfuehren.
|
||||
if (afk) {
|
||||
StatusAPI.playerAfk.put(uuid, true);
|
||||
} else {
|
||||
@@ -184,30 +223,66 @@ public class AfkModule implements Module, Listener {
|
||||
|
||||
private void setAfk(ProxiedPlayer p, boolean afk) {
|
||||
UUID id = p.getUniqueId();
|
||||
if (isAfk(id) == afk) return;
|
||||
// Zustandswechsel atomar: setAfk wird sowohl vom Scheduler (checkIdle) als
|
||||
// auch von den HTTP-Threads der Bridge (Bewegung, Compass) aufgerufen. Ein
|
||||
// getrenntes isAfk()-Vorabpruefen liess beide Threads durchlaufen – Folge
|
||||
// waren doppelte Title-Tasks und doppelte Broadcasts.
|
||||
if (afk) {
|
||||
StatusAPI.playerAfk.put(id, true);
|
||||
if (StatusAPI.playerAfk.putIfAbsent(id, Boolean.TRUE) != null) return;
|
||||
startTitleTask(p);
|
||||
} else {
|
||||
StatusAPI.playerAfk.remove(id);
|
||||
if (StatusAPI.playerAfk.remove(id) == null) return;
|
||||
lastActivity.put(id, System.currentTimeMillis());
|
||||
TitlePair pair = activePair.get(id);
|
||||
TitlePair pair = activePair.remove(id);
|
||||
stopTitleTask(id);
|
||||
clearTitle(p);
|
||||
if (pair != null) sendTitleEntry(p, pair.unset);
|
||||
}
|
||||
// AFK-Status an alle Spigot-Server broadcasten damit die Bridge
|
||||
// den Nametag-Prefix sofort aktualisieren kann.
|
||||
broadcastAfkState(id, afk);
|
||||
}
|
||||
|
||||
/**
|
||||
* Schickt eine Plugin-Message (Channel "statusapi:afk") an alle Spigot-Server,
|
||||
* auf denen der betroffene Spieler ODER irgendein anderer Spieler eingeloggt ist.
|
||||
* Format: "UUID:true" / "UUID:false"
|
||||
*/
|
||||
private void broadcastAfkState(UUID uuid, boolean afk) {
|
||||
byte[] payload = (uuid.toString() + ":" + afk).getBytes(java.nio.charset.StandardCharsets.UTF_8);
|
||||
Set<net.md_5.bungee.api.config.ServerInfo> reached = new java.util.HashSet<>();
|
||||
// Zuerst: Server des betroffenen Spielers direkt ansprechen
|
||||
ProxiedPlayer self = ProxyServer.getInstance().getPlayer(uuid);
|
||||
if (self != null && self.isConnected() && self.getServer() != null) {
|
||||
net.md_5.bungee.api.config.ServerInfo srv = self.getServer().getInfo();
|
||||
if (srv != null) { srv.sendData("statusapi:afk", payload); reached.add(srv); }
|
||||
}
|
||||
// Dann: alle anderen Online-Spieler – deren Server brauchen das Update
|
||||
// damit der Prefix über dem AFK-Spieler-Kopf auch bei Anderen stimmt.
|
||||
for (ProxiedPlayer other : ProxyServer.getInstance().getPlayers()) {
|
||||
if (!other.isConnected() || other.getServer() == null) continue;
|
||||
net.md_5.bungee.api.config.ServerInfo srv = other.getServer().getInfo();
|
||||
if (srv != null && !reached.contains(srv)) {
|
||||
srv.sendData("statusapi:afk", payload);
|
||||
reached.add(srv);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private void checkIdle() {
|
||||
long now = System.currentTimeMillis();
|
||||
long thresholdMs = idleSeconds * 1000L;
|
||||
for (ProxiedPlayer p : ProxyServer.getInstance().getPlayers()) {
|
||||
if (!p.isConnected()) continue;
|
||||
UUID id = p.getUniqueId();
|
||||
if (p.hasPermission(bypassPerm) || isAfk(id)) continue;
|
||||
Long last = lastActivity.get(id);
|
||||
if (last == null) { lastActivity.put(id, now); continue; }
|
||||
if (now - last >= thresholdMs) setAfk(p, true);
|
||||
try {
|
||||
if (!p.isConnected()) continue;
|
||||
UUID id = p.getUniqueId();
|
||||
if (p.hasPermission(bypassPerm) || isAfk(id)) continue;
|
||||
Long last = lastActivity.get(id);
|
||||
if (last == null) { lastActivity.put(id, now); continue; }
|
||||
if (now - last >= thresholdMs) setAfk(p, true);
|
||||
} catch (Exception ex) {
|
||||
plugin.getLogger().log(Level.FINE, "[AfkModule] Fehler in checkIdle fuer Spieler.", ex);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -224,17 +299,43 @@ public class AfkModule implements Module, Listener {
|
||||
TitlePair pair = titlePairs.get(random.nextInt(titlePairs.size()));
|
||||
activePair.put(id, pair);
|
||||
activeTitleEntry.put(id, pair.set);
|
||||
sendTitleEntry(p, pair.set);
|
||||
if (!sendTitleEntry(p, pair.set, true)) { // firstSend=true → TitleTimes mitsenden
|
||||
stopTitleTask(id);
|
||||
return;
|
||||
}
|
||||
|
||||
long intervalMs = Math.max(500, (titleStay - 20) * 50L);
|
||||
// Kurz vor Ablauf der Standzeit erneut senden, damit der Title nie ausblendet.
|
||||
// Fruehere Rechnung (titleStay - 20) * 50 lief bei kleinem afk.title.stay ins
|
||||
// Leere: unter 30 Ticks war das Intervall laenger als die Standzeit selbst und
|
||||
// der Title verschwand zwischen zwei Ticks.
|
||||
long stayMs = Math.max(1L, titleStay) * 50L;
|
||||
long intervalMs = Math.max(500L, stayMs - 1000L);
|
||||
ScheduledTask task = ProxyServer.getInstance().getScheduler().schedule(plugin, () -> {
|
||||
ProxiedPlayer online = ProxyServer.getInstance().getPlayer(id);
|
||||
if (online == null || !online.isConnected() || !isAfk(id)) {
|
||||
try {
|
||||
ProxiedPlayer online = ProxyServer.getInstance().getPlayer(id);
|
||||
if (online == null || !online.isConnected() || !isAfk(id)) {
|
||||
stopTitleTask(id);
|
||||
return;
|
||||
}
|
||||
String[] current = activeTitleEntry.get(id);
|
||||
if (current == null) return;
|
||||
// firstSend=false → kein TitleTimes → kein Blinken beim Repeat
|
||||
if (!sendTitleEntry(online, current, false)) {
|
||||
stopTitleTask(id);
|
||||
return;
|
||||
}
|
||||
// Der Spieler kann sich waehrend des Sendens bewegt haben – setAfk(false)
|
||||
// laeuft auf dem HTTP-Thread der Bridge. Dann klebt jetzt wieder der
|
||||
// AFK-Title auf dem Bildschirm, obwohl der Spieler zurueck ist.
|
||||
if (!isAfk(id)) {
|
||||
clearTitle(online);
|
||||
sendTitleEntry(online, pair.unset);
|
||||
stopTitleTask(id);
|
||||
}
|
||||
} catch (Exception ex) {
|
||||
plugin.getLogger().log(Level.FINE, "[AfkModule] Fehler im Title-Repeat-Task.", ex);
|
||||
stopTitleTask(id);
|
||||
return;
|
||||
}
|
||||
String[] current = activeTitleEntry.get(id);
|
||||
if (current != null) sendTitleEntry(online, current);
|
||||
}, intervalMs, intervalMs, TimeUnit.MILLISECONDS);
|
||||
titleTasks.put(id, task);
|
||||
}
|
||||
@@ -243,10 +344,15 @@ public class AfkModule implements Module, Listener {
|
||||
ScheduledTask old = titleTasks.remove(id);
|
||||
if (old != null) old.cancel();
|
||||
activeTitleEntry.remove(id);
|
||||
// activePair bleibt bis setAfk(false) es ausliest, danach:
|
||||
// BUGFIX: activePair hier NICHT entfernen wenn der Spieler noch AFK ist –
|
||||
// setAfk(false) liest activePair noch aus (für den Unset-Title) und entfernt es selbst.
|
||||
// Wenn der Spieler aber NICHT mehr AFK ist (z.B. Exception im Task), muss activePair
|
||||
// hier bereinigt werden, sonst Memory Leak.
|
||||
if (!Boolean.TRUE.equals(StatusAPI.playerAfk.get(id))) {
|
||||
activePair.remove(id);
|
||||
}
|
||||
}
|
||||
|
||||
/** Entfernt den Title sofort vom Bildschirm. */
|
||||
/** Entfernt den Title sofort vom Bildschirm via ClearTitles-Packet. */
|
||||
private void clearTitle(ProxiedPlayer p) {
|
||||
try {
|
||||
@@ -259,20 +365,37 @@ public class AfkModule implements Module, Listener {
|
||||
* Sendet Title + Subtitle als raw Packets (wie ScoreboardModule) –
|
||||
* dadurch werden Hex-Farben korrekt \u00fcbertragen, ohne durch TextComponent.fromArray() zu laufen.
|
||||
*/
|
||||
private void sendTitleEntry(ProxiedPlayer p, String[] entry) {
|
||||
String titleRaw = ChatColor.translateAlternateColorCodes('&', applyGradients(entry[0]));
|
||||
String subtitleRaw = entry.length > 1 ? ChatColor.translateAlternateColorCodes('&', applyGradients(entry[1])) : "";
|
||||
/**
|
||||
* Sendet Title + Subtitle an den Spieler.
|
||||
* @param firstSend true = TitleTimes-Packet mitsenden (erster Aufruf).
|
||||
* false = nur Title/Subtitle refreshen, KEIN TitleTimes →
|
||||
* verhindert das Blinken (fade-in/-out) bei jedem Repeat-Tick.
|
||||
*/
|
||||
private boolean sendTitleEntry(ProxiedPlayer p, String[] entry, boolean firstSend) {
|
||||
if (p == null || !p.isConnected() || entry == null || entry.length == 0) return false;
|
||||
|
||||
String titleRaw = ChatColor.translateAlternateColorCodes('&', applyGradients(
|
||||
entry[0] == null ? "" : entry[0]
|
||||
));
|
||||
String subtitleRaw = entry.length > 1
|
||||
? ChatColor.translateAlternateColorCodes('&', applyGradients(entry[1] == null ? "" : entry[1]))
|
||||
: "";
|
||||
|
||||
try {
|
||||
if (sendPkt == null) throw new IllegalStateException("sendPkt not initialized");
|
||||
|
||||
// Times zuerst senden
|
||||
net.md_5.bungee.protocol.packet.TitleTimes times = new net.md_5.bungee.protocol.packet.TitleTimes();
|
||||
times.setFadeIn(titleFadeIn);
|
||||
times.setStay(titleStay);
|
||||
times.setFadeOut(titleFadeOut);
|
||||
sendPkt.invoke(p, times);
|
||||
// TitleTimes NUR beim ersten Senden schicken.
|
||||
// Wird das Packet bei jedem Repeat-Tick gesendet, startet der Client
|
||||
// jedes Mal eine neue fade-in-Animation → sichtbares Blinken.
|
||||
if (firstSend) {
|
||||
net.md_5.bungee.protocol.packet.TitleTimes times = new net.md_5.bungee.protocol.packet.TitleTimes();
|
||||
times.setFadeIn(titleFadeIn);
|
||||
times.setStay(titleStay);
|
||||
times.setFadeOut(titleFadeOut);
|
||||
sendPkt.invoke(p, times);
|
||||
}
|
||||
|
||||
// Title-Packet (Action = TITLE, ordinal 0)
|
||||
// Title-Packet
|
||||
net.md_5.bungee.protocol.packet.Title titlePkt = new net.md_5.bungee.protocol.packet.Title();
|
||||
titlePkt.setAction(net.md_5.bungee.protocol.packet.Title.Action.TITLE);
|
||||
titlePkt.setText(mergeComponents(buildComponents(titleRaw)));
|
||||
@@ -283,20 +406,37 @@ public class AfkModule implements Module, Listener {
|
||||
subPkt.setText(mergeComponents(buildComponents(subtitleRaw)));
|
||||
sendPkt.invoke(p, subPkt);
|
||||
|
||||
return true;
|
||||
|
||||
} catch (Exception e) {
|
||||
// Fallback auf Title-API
|
||||
try {
|
||||
Title title = ProxyServer.getInstance().createTitle();
|
||||
title.title(buildComponents(titleRaw));
|
||||
title.subTitle(buildComponents(subtitleRaw));
|
||||
title.fadeIn(titleFadeIn);
|
||||
title.stay(titleStay);
|
||||
title.fadeOut(titleFadeOut);
|
||||
if (firstSend) {
|
||||
title.fadeIn(titleFadeIn);
|
||||
title.stay(titleStay);
|
||||
title.fadeOut(titleFadeOut);
|
||||
} else {
|
||||
// Kein Fade → bleibt stabil stehen ohne Blink-Effekt
|
||||
title.fadeIn(0);
|
||||
title.stay(titleStay);
|
||||
title.fadeOut(0);
|
||||
}
|
||||
p.sendTitle(title);
|
||||
} catch (Exception ignored) {}
|
||||
return true;
|
||||
} catch (Exception ignored) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/** Overload für Aufrufe ohne firstSend-Flag (z.B. unset-Title beim Zurückkommen). */
|
||||
private boolean sendTitleEntry(ProxiedPlayer p, String[] entry) {
|
||||
return sendTitleEntry(p, entry, true);
|
||||
}
|
||||
|
||||
/** Fasst BaseComponent[] in eine TextComponent zusammen (f\u00fcr Title/Subtitle setText). */
|
||||
private static net.md_5.bungee.api.chat.BaseComponent mergeComponents(BaseComponent[] parts) {
|
||||
TextComponent root = new TextComponent("");
|
||||
@@ -304,11 +444,6 @@ public class AfkModule implements Module, Listener {
|
||||
return root;
|
||||
}
|
||||
|
||||
/** Fasst ein BaseComponent[]-Array in eine einzelne TextComponent zusammen. */
|
||||
private static BaseComponent wrap(BaseComponent[] parts) {
|
||||
return mergeComponents(parts);
|
||||
}
|
||||
|
||||
// ── Gradient-Verarbeitung ─────────────────────────────────────────────────
|
||||
// Identische Implementierung wie im ScoreboardModule – verarbeitet
|
||||
// %gradient:FARBE1:FARBE2:...:TEXT% mit beliebig vielen Farb-Stopps.
|
||||
|
||||
@@ -246,28 +246,73 @@ public class AntiBotModule implements Module, Listener {
|
||||
}
|
||||
}
|
||||
|
||||
// FIX Netzwerk-Timeout: VPN-Check asynchron ausführen damit der BungeeCord-
|
||||
// Netzwerk-Thread nicht blockiert wird. Ein blockierender HTTP-Request (bis zu
|
||||
// vpnTimeoutMs = 2500 ms) im PreLoginEvent-Handler verhindert, dass KeepAlive-
|
||||
// Pakete an bereits eingeloggte Spieler verarbeitet werden → Disconnect.
|
||||
if (vpnCheckEnabled) {
|
||||
VpnCheckResult info = getVpnInfo(ip, now);
|
||||
if (info != null) {
|
||||
boolean shouldBlock = (vpnBlockProxy && info.proxy) || (vpnBlockHosting && info.hosting);
|
||||
if (shouldBlock) {
|
||||
logSecurityEvent("vpn_detected", ip, event.getConnection(), "proxy=" + info.proxy + ", hosting=" + info.hosting);
|
||||
if (learningModeEnabled) {
|
||||
if (vpnBlockProxy && info.proxy) addLearningScore(ip, now, learningVpnProxyPoints, "vpn-proxy", false);
|
||||
if (vpnBlockHosting && info.hosting) addLearningScore(ip, now, learningVpnHostingPoints, "vpn-hosting", false);
|
||||
int current = getLearningScore(ip, now);
|
||||
if (current >= learningScoreThreshold) {
|
||||
// Cache-Treffer: sofort prüfen, kein async nötig
|
||||
VpnCacheEntry cached = vpnCache.get(ip);
|
||||
if (cached != null && cached.expiresAt > now) {
|
||||
VpnCheckResult info = cached.result;
|
||||
if (info != null) {
|
||||
boolean shouldBlock = (vpnBlockProxy && info.proxy) || (vpnBlockHosting && info.hosting);
|
||||
if (shouldBlock) {
|
||||
logSecurityEvent("vpn_detected", ip, event.getConnection(), "proxy=" + info.proxy + ", hosting=" + info.hosting);
|
||||
if (learningModeEnabled) {
|
||||
if (vpnBlockProxy && info.proxy) addLearningScore(ip, now, learningVpnProxyPoints, "vpn-proxy", false);
|
||||
if (vpnBlockHosting && info.hosting) addLearningScore(ip, now, learningVpnHostingPoints, "vpn-hosting", false);
|
||||
int current = getLearningScore(ip, now);
|
||||
if (current >= learningScoreThreshold) {
|
||||
blockIp(ip, now);
|
||||
logSecurityEvent("learning_threshold_block", ip, event.getConnection(), "reason=vpn, score=" + current);
|
||||
recordLearningEvent("BLOCK " + ip + " reason=vpn score=" + current);
|
||||
blockEvent(event);
|
||||
}
|
||||
} else {
|
||||
blockIp(ip, now);
|
||||
logSecurityEvent("learning_threshold_block", ip, event.getConnection(), "reason=vpn, score=" + current);
|
||||
recordLearningEvent("BLOCK " + ip + " reason=vpn score=" + current);
|
||||
logSecurityEvent("vpn_block", ip, event.getConnection(), "mode=direct, proxy=" + info.proxy + ", hosting=" + info.hosting);
|
||||
blockEvent(event);
|
||||
}
|
||||
} else {
|
||||
blockIp(ip, now);
|
||||
logSecurityEvent("vpn_block", ip, event.getConnection(), "mode=direct, proxy=" + info.proxy + ", hosting=" + info.hosting);
|
||||
blockEvent(event);
|
||||
}
|
||||
}
|
||||
} else {
|
||||
// Kein Cache-Treffer → HTTP-Request async ausführen
|
||||
event.registerIntent(plugin);
|
||||
final long nowFinal = now;
|
||||
ProxyServer.getInstance().getScheduler().runAsync(plugin, () -> {
|
||||
try {
|
||||
VpnCheckResult info = requestIpApi(ip);
|
||||
if (info != null) {
|
||||
VpnCacheEntry entry = new VpnCacheEntry();
|
||||
entry.result = info;
|
||||
entry.expiresAt = nowFinal + Math.max(1, vpnCacheMinutes) * 60_000L;
|
||||
vpnCache.put(ip, entry);
|
||||
|
||||
boolean shouldBlock = (vpnBlockProxy && info.proxy) || (vpnBlockHosting && info.hosting);
|
||||
if (shouldBlock) {
|
||||
logSecurityEvent("vpn_detected", ip, event.getConnection(), "proxy=" + info.proxy + ", hosting=" + info.hosting);
|
||||
if (learningModeEnabled) {
|
||||
if (vpnBlockProxy && info.proxy) addLearningScore(ip, nowFinal, learningVpnProxyPoints, "vpn-proxy", false);
|
||||
if (vpnBlockHosting && info.hosting) addLearningScore(ip, nowFinal, learningVpnHostingPoints, "vpn-hosting", false);
|
||||
int current = getLearningScore(ip, nowFinal);
|
||||
if (current >= learningScoreThreshold) {
|
||||
blockIp(ip, nowFinal);
|
||||
logSecurityEvent("learning_threshold_block", ip, event.getConnection(), "reason=vpn, score=" + current);
|
||||
recordLearningEvent("BLOCK " + ip + " reason=vpn score=" + current);
|
||||
blockEvent(event);
|
||||
}
|
||||
} else {
|
||||
blockIp(ip, nowFinal);
|
||||
logSecurityEvent("vpn_block", ip, event.getConnection(), "mode=direct, proxy=" + info.proxy + ", hosting=" + info.hosting);
|
||||
blockEvent(event);
|
||||
}
|
||||
}
|
||||
}
|
||||
} finally {
|
||||
event.completeIntent(plugin);
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -723,19 +768,6 @@ public class AntiBotModule implements Module, Listener {
|
||||
try { return Integer.parseInt(s == null ? "" : s.trim()); } catch (Exception ignored) { return fallback; }
|
||||
}
|
||||
|
||||
private VpnCheckResult getVpnInfo(String ip, long now) {
|
||||
VpnCacheEntry cached = vpnCache.get(ip);
|
||||
if (cached != null && cached.expiresAt > now) return cached.result;
|
||||
VpnCheckResult fresh = requestIpApi(ip);
|
||||
if (fresh != null) {
|
||||
VpnCacheEntry entry = new VpnCacheEntry();
|
||||
entry.result = fresh;
|
||||
entry.expiresAt = now + Math.max(1, vpnCacheMinutes) * 60_000L;
|
||||
vpnCache.put(ip, entry);
|
||||
}
|
||||
return fresh;
|
||||
}
|
||||
|
||||
private VpnCheckResult requestIpApi(String ip) {
|
||||
HttpURLConnection conn = null;
|
||||
try {
|
||||
|
||||
@@ -22,8 +22,11 @@ public class ChatLogger {
|
||||
private final int retentionDays;
|
||||
private final AtomicInteger counter = new AtomicInteger(0);
|
||||
|
||||
private static final SimpleDateFormat DATE_FMT = new SimpleDateFormat("yyyy-MM-dd");
|
||||
private static final SimpleDateFormat TIME_FMT = new SimpleDateFormat("HH:mm:ss");
|
||||
// ThreadLocal: SimpleDateFormat ist NICHT thread-sicher – jeder Thread bekommt seine eigene Instanz.
|
||||
private static final ThreadLocal<SimpleDateFormat> DATE_FMT =
|
||||
ThreadLocal.withInitial(() -> new SimpleDateFormat("yyyy-MM-dd"));
|
||||
private static final ThreadLocal<SimpleDateFormat> TIME_FMT =
|
||||
ThreadLocal.withInitial(() -> new SimpleDateFormat("HH:mm:ss"));
|
||||
|
||||
public ChatLogger(File dataFolder, Logger logger, int retentionDays) {
|
||||
this.logDir = new File(dataFolder, "chatlogs");
|
||||
@@ -58,8 +61,8 @@ public class ChatLogger {
|
||||
* @param message Nachrichtentext (Rohtext, ohne Farbcodes)
|
||||
*/
|
||||
public void log(String msgId, String server, String channel, String player, String message) {
|
||||
String date = DATE_FMT.format(new Date());
|
||||
String time = TIME_FMT.format(new Date());
|
||||
String date = DATE_FMT.get().format(new Date());
|
||||
String time = TIME_FMT.get().format(new Date());
|
||||
|
||||
// Minecraft-Farbcodes aus dem Log entfernen
|
||||
String cleanMsg = stripColor(message);
|
||||
@@ -119,7 +122,7 @@ public class ChatLogger {
|
||||
* @return Liste der Logzeilen (\u00e4lteste zuerst)
|
||||
*/
|
||||
public List<String> readLastLines(String playerFilter, int maxLines) {
|
||||
String date = DATE_FMT.format(new Date());
|
||||
String date = DATE_FMT.get().format(new Date());
|
||||
File logFile = new File(logDir, "chatlog_" + date + ".log");
|
||||
if (!logFile.exists()) return Collections.emptyList();
|
||||
|
||||
|
||||
@@ -31,7 +31,9 @@ public class ReportManager {
|
||||
/** Z\u00e4hler f\u00fcr Report-IDs. Wird beim Laden synchronisiert. */
|
||||
private final AtomicInteger idCounter = new AtomicInteger(0);
|
||||
|
||||
private static final SimpleDateFormat DATE_FMT = new SimpleDateFormat("dd.MM.yyyy HH:mm:ss");
|
||||
// ThreadLocal: SimpleDateFormat ist NICHT thread-sicher.
|
||||
private static final ThreadLocal<SimpleDateFormat> DATE_FMT =
|
||||
ThreadLocal.withInitial(() -> new SimpleDateFormat("dd.MM.yyyy HH:mm:ss"));
|
||||
|
||||
// ===== Report-Datenklasse =====
|
||||
|
||||
@@ -48,7 +50,7 @@ public class ReportManager {
|
||||
public String closedBy; // Name des schlie\u00dfenden Admins (oder leer)
|
||||
|
||||
public String getFormattedTime() {
|
||||
return DATE_FMT.format(new Date(timestamp));
|
||||
return DATE_FMT.get().format(new Date(timestamp));
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
+100
-35
@@ -91,7 +91,11 @@ public class ScoreboardModule implements Module, Listener {
|
||||
private boolean nametagEnabled = true;
|
||||
|
||||
// Spieler, f\u00fcr die bereits ein Nametag-Team gesetzt wurde (Teamname = "afk_" + player.getName() abgek\u00fcrzt)
|
||||
private final Set<UUID> nametagCreated = ConcurrentHashMap.newKeySet();
|
||||
// Pro Target: welche Viewer haben bereits ein CREATE-Packet bekommen.
|
||||
// Fix: verhindert REMOVE ohne CREATE → "Player is either on another team" Crash.
|
||||
private final ConcurrentHashMap<UUID, Set<UUID>> nametagViewers = new ConcurrentHashMap<>();
|
||||
// Dirty-Flag-Cache: letzter gesendeter Prefix pro Spieler (UUID → prefixStr)
|
||||
private final ConcurrentHashMap<UUID, String> nametagLastPrefix = new ConcurrentHashMap<>();
|
||||
private int updateInterval = 500; // Millisekunden
|
||||
private int tickerSpeed = 1;
|
||||
private boolean rainbowEnabled = true;
|
||||
@@ -130,8 +134,10 @@ public class ScoreboardModule implements Module, Listener {
|
||||
private ScheduledTask updateTask;
|
||||
private ScheduledTask titleTask;
|
||||
private ScheduledTask newsTask;
|
||||
private java.text.SimpleDateFormat sdf;
|
||||
private java.text.SimpleDateFormat sdfDate;
|
||||
// DateTimeFormatter ist thread-sicher (im Gegensatz zu SimpleDateFormat)
|
||||
private java.time.format.DateTimeFormatter sdf;
|
||||
private java.time.format.DateTimeFormatter sdfDate;
|
||||
private java.time.ZoneId sdfZone = java.time.ZoneId.systemDefault();
|
||||
private DecimalFormat df;
|
||||
private Method sendPkt;
|
||||
private boolean ready = false;
|
||||
@@ -210,6 +216,7 @@ public class ScoreboardModule implements Module, Listener {
|
||||
}
|
||||
created.clear(); createdAdmin.clear(); createdSupporter.clear(); tickerPos.clear(); rainbowIdx.clear();
|
||||
hiddenPlayers.clear(); forceAdminView.clear(); forcePlayerView.clear(); forceSupporterView.clear(); newsPos.clear();
|
||||
nametagViewers.clear(); nametagLastPrefix.clear();
|
||||
}
|
||||
|
||||
@EventHandler
|
||||
@@ -229,7 +236,11 @@ public class ScoreboardModule implements Module, Listener {
|
||||
if (nametagEnabled) {
|
||||
ProxyServer.getInstance().getScheduler().schedule(plugin, () -> {
|
||||
// Alle bestehenden Spieler → neuer Spieler bekommt ihre Nametags
|
||||
nametagCreated.remove(id); // Reset damit CREATE statt UPDATE gesendet wird
|
||||
// Viewer-Eintrag des neuen Spielers aus allen anderen Target-Sets löschen
|
||||
// damit sie beim nächsten updateNametag() CREATE (nicht UPDATE) bekommen.
|
||||
nametagViewers.remove(id);
|
||||
for (Set<UUID> vs : nametagViewers.values()) { vs.remove(id); }
|
||||
nametagLastPrefix.remove(id);
|
||||
for (ProxiedPlayer existing : ProxyServer.getInstance().getPlayers()) {
|
||||
if (existing.isConnected()) updateNametag(existing);
|
||||
}
|
||||
@@ -258,8 +269,12 @@ public class ScoreboardModule implements Module, Listener {
|
||||
playerX.remove(id); playerY.remove(id); playerZ.remove(id);
|
||||
playerWorld.remove(id); playerGamemode.remove(id);
|
||||
playerExp.remove(id); playerFood.remove(id); playerSpeed.remove(id);
|
||||
joinTimes.remove(id); hiddenPlayers.remove(id); forceAdminView.remove(id); forcePlayerView.remove(id); newsPos.remove(id);
|
||||
joinTimes.remove(id); hiddenPlayers.remove(id); newsPos.remove(id);
|
||||
ticketMyOpen.remove(id); // Memory-Leak-Fix: Ticket-Daten beim Logout bereinigen
|
||||
// Nametag-Team f\u00fcr diesen Spieler bei allen anderen entfernen
|
||||
nametagLastPrefix.remove(id);
|
||||
// Viewer-Einträge dieses Spielers aus allen anderen Target-Sets entfernen
|
||||
for (Set<UUID> viewerSet : nametagViewers.values()) { viewerSet.remove(id); }
|
||||
if (nametagEnabled) removeNametag(e.getPlayer());
|
||||
}
|
||||
|
||||
@@ -342,6 +357,9 @@ public class ScoreboardModule implements Module, Listener {
|
||||
&& (p.hasPermission(adminPermission) || forceAdminView.contains(id));
|
||||
boolean isSupporter = !isAdmin && (forceSupporterView.contains(id) || (!forcePlayerView.contains(id) && p.hasPermission(supporterPermission)));
|
||||
Set<UUID> activeCreated = isAdmin ? createdAdmin : isSupporter ? createdSupporter : created;
|
||||
// BUG-FIX: UPDATE_TITLE (mode=2) darf nur gesendet werden wenn das Objective
|
||||
// bereits mit CREATE (mode=0) angelegt wurde. Sonst: "Fehler im Netzwerkprotokoll".
|
||||
if (!activeCreated.contains(id)) continue;
|
||||
try {
|
||||
int rIdx = (rainbowIdx.getOrDefault(id, 0) + 1) % 10000;
|
||||
rainbowIdx.put(id, rIdx);
|
||||
@@ -384,28 +402,42 @@ public class ScoreboardModule implements Module, Listener {
|
||||
|
||||
/**
|
||||
* Sendet ein Team-Packet an alle online Spieler, das den Prefix
|
||||
* \u00fcber dem Kopf des 'target'-Spielers setzt.
|
||||
* AFK-Spieler bekommen §7[AFK] §r als Prefix, alle anderen ihren LuckPerms-Prefix.
|
||||
* über dem Kopf des 'target'-Spielers setzt.
|
||||
*
|
||||
* FIX: Pro-Viewer-Tracking via nametagViewers.
|
||||
* Jeder Viewer bekommt beim ersten Mal CREATE (mode=0), danach UPDATE (mode=2).
|
||||
* Nur wenn sich der Prefix geändert hat werden überhaupt Packets gesendet (Dirty-Flag).
|
||||
* removeNametag() sendet REMOVE nur an Viewer die CREATE bekommen haben → kein Crash.
|
||||
*/
|
||||
private void updateNametag(ProxiedPlayer target) {
|
||||
if (!ready || !target.isConnected()) return;
|
||||
try {
|
||||
boolean isAfk = Boolean.TRUE.equals(net.viper.status.StatusAPI.playerAfk.get(target.getUniqueId()));
|
||||
UUID id = target.getUniqueId();
|
||||
boolean isAfk = Boolean.TRUE.equals(net.viper.status.StatusAPI.playerAfk.get(id));
|
||||
String lpPrefix = getLpPrefix(target);
|
||||
|
||||
// Teamname: "nt_" + erste 13 Zeichen des Playernamens (max 16 Zeichen insgesamt)
|
||||
String teamName = "nt_" + target.getName().substring(0, Math.min(13, target.getName().length()));
|
||||
|
||||
String teamName = nametagTeamName(target);
|
||||
String prefixStr = isAfk ? "§7[AFK] §r" : (lpPrefix.isEmpty() ? "" : lpPrefix + "§r ");
|
||||
|
||||
// Packet an alle Online-Spieler senden (damit alle den ge\u00e4nderten Prefix sehen)
|
||||
String lastPrefix = nametagLastPrefix.get(id);
|
||||
boolean prefixChanged = !prefixStr.equals(lastPrefix);
|
||||
|
||||
// Viewer-Set für diesen Target holen (oder neu anlegen)
|
||||
Set<UUID> viewers = nametagViewers.computeIfAbsent(id, k -> ConcurrentHashMap.newKeySet());
|
||||
|
||||
// Nur senden wenn Prefix sich geändert hat ODER es neue Viewer gibt
|
||||
for (ProxiedPlayer viewer : ProxyServer.getInstance().getPlayers()) {
|
||||
if (!viewer.isConnected()) continue;
|
||||
UUID vid = viewer.getUniqueId();
|
||||
boolean viewerIsNew = !viewers.contains(vid);
|
||||
|
||||
// Kein Update nötig wenn Prefix gleich und Viewer schon CREATE hatte
|
||||
if (!prefixChanged && !viewerIsNew) continue;
|
||||
|
||||
try {
|
||||
Team team = new Team();
|
||||
team.setName(teamName);
|
||||
boolean firstTime = !nametagCreated.contains(target.getUniqueId());
|
||||
team.setMode(firstTime ? (byte) 0 : (byte) 2); // 0=CREATE, 2=UPDATE
|
||||
team.setMode(viewerIsNew ? (byte) 0 : (byte) 2); // 0=CREATE, 2=UPDATE
|
||||
net.md_5.bungee.api.chat.TextComponent pfxComp =
|
||||
new net.md_5.bungee.api.chat.TextComponent("");
|
||||
for (net.md_5.bungee.api.chat.BaseComponent bc : buildComponents(c(prefixStr)))
|
||||
@@ -417,31 +449,64 @@ public class ScoreboardModule implements Module, Listener {
|
||||
team.setCollisionRule(Either.right(CollisionRule.ALWAYS));
|
||||
team.setColor(Optional.of(21)); // RESET
|
||||
team.setFriendlyFire((byte) 3);
|
||||
if (firstTime) team.setPlayers(new String[]{ target.getName() });
|
||||
if (viewerIsNew) team.setPlayers(new String[]{ target.getName() });
|
||||
sendPkt.invoke(viewer, team);
|
||||
|
||||
// Eintrag zurueckholen: StatusAPIBridge setzt serverseitig ein
|
||||
// eigenes Nametag-Team (vnt...). Sobald das den Spieler aufnimmt,
|
||||
// faellt er clientseitig aus unserem Team - ein UPDATE (mode=2)
|
||||
// traegt keine Eintraege und liefe danach dauerhaft ins Leere.
|
||||
// ADD_PLAYER (mode=3) holt ihn bei jeder Prefix-Aenderung zurueck.
|
||||
if (!viewerIsNew) {
|
||||
Team claim = new Team();
|
||||
claim.setName(teamName);
|
||||
claim.setMode((byte) 3); // ADD_PLAYER
|
||||
claim.setPlayers(new String[]{ target.getName() });
|
||||
sendPkt.invoke(viewer, claim);
|
||||
}
|
||||
viewers.add(vid); // Viewer als "hat CREATE" markieren
|
||||
} catch (Exception ignored) {}
|
||||
}
|
||||
nametagCreated.add(target.getUniqueId());
|
||||
nametagLastPrefix.put(id, prefixStr);
|
||||
} catch (Exception e) {
|
||||
plugin.getLogger().warning("[ScoreboardModule] Nametag-Fehler f\u00fcr " + target.getName() + ": " + e.getMessage());
|
||||
plugin.getLogger().warning("[ScoreboardModule] Nametag-Fehler für " + target.getName() + ": " + e.getMessage());
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Team-Name fuer den Nametag eines Spielers.
|
||||
* Basiert auf der UUID statt auf dem Namen: bei namensbasierten Teams
|
||||
* (erste 13 Zeichen) landeten Spieler mit gleichem Namensanfang – etwa
|
||||
* "ViperNetworkA" und "ViperNetworkB" – im selben Team und haben sich
|
||||
* gegenseitig den Prefix ueberschrieben. Minecraft-Limit: 16 Zeichen.
|
||||
*/
|
||||
private static String nametagTeamName(ProxiedPlayer target) {
|
||||
return "nt" + target.getUniqueId().toString().replace("-", "").substring(0, 13);
|
||||
}
|
||||
|
||||
/**
|
||||
* Entfernt das Nametag-Team beim Disconnect sauber vom Client aller Spieler.
|
||||
*/
|
||||
private void removeNametag(ProxiedPlayer target) {
|
||||
String teamName = "nt_" + target.getName().substring(0, Math.min(13, target.getName().length()));
|
||||
for (ProxiedPlayer viewer : ProxyServer.getInstance().getPlayers()) {
|
||||
if (!viewer.isConnected() || viewer.getUniqueId().equals(target.getUniqueId())) continue;
|
||||
try {
|
||||
Team team = new Team();
|
||||
team.setName(teamName);
|
||||
team.setMode((byte) 1); // REMOVE
|
||||
sendPkt.invoke(viewer, team);
|
||||
} catch (Exception ignored) {}
|
||||
UUID id = target.getUniqueId();
|
||||
String teamName = nametagTeamName(target);
|
||||
// REMOVE nur an Viewer senden die auch CREATE erhalten haben (per-Viewer-Tracking).
|
||||
// Sonst: "Player is either on another team or not on any team" → Client-Crash.
|
||||
Set<UUID> viewers = nametagViewers.remove(id);
|
||||
if (viewers != null) {
|
||||
for (UUID vid : viewers) {
|
||||
ProxiedPlayer viewer = ProxyServer.getInstance().getPlayer(vid);
|
||||
if (viewer == null || !viewer.isConnected()) continue;
|
||||
if (vid.equals(id)) continue; // Den Target selbst überspringen
|
||||
try {
|
||||
Team team = new Team();
|
||||
team.setName(teamName);
|
||||
team.setMode((byte) 1); // REMOVE
|
||||
sendPkt.invoke(viewer, team);
|
||||
} catch (Exception ignored) {}
|
||||
}
|
||||
}
|
||||
nametagCreated.remove(target.getUniqueId());
|
||||
nametagLastPrefix.remove(id);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -520,8 +585,8 @@ public class ScoreboardModule implements Module, Listener {
|
||||
String maxpl = rawLimit > 0 ? String.valueOf(rawLimit) : "∞";
|
||||
String tps = isAdmin ? getTps(id) : "";
|
||||
String ram = isAdmin ? getRam() : "";
|
||||
String time = sdf.format(new Date());
|
||||
String date = sdfDate.format(new Date());
|
||||
String time = sdf.format(java.time.LocalDateTime.now(sdfZone));
|
||||
String date = sdfDate.format(java.time.LocalDateTime.now(sdfZone));
|
||||
String playtime = formatPlaytime(id);
|
||||
// Neue Placeholders
|
||||
String xCoord = String.valueOf(playerX.getOrDefault(id, 0));
|
||||
@@ -1842,13 +1907,13 @@ public class ScoreboardModule implements Module, Listener {
|
||||
decimalSeparator = g.apply("scoreboard.money_decimal_separator",",");
|
||||
separator = g.apply("scoreboard.separator", "&8&m--------------------");
|
||||
try {
|
||||
sdf = new java.text.SimpleDateFormat(timeFormat);
|
||||
sdf.setTimeZone(java.util.TimeZone.getTimeZone(timeZone));
|
||||
sdfDate = new java.text.SimpleDateFormat(dateFormat);
|
||||
sdfDate.setTimeZone(java.util.TimeZone.getTimeZone(timeZone));
|
||||
sdfZone = java.time.ZoneId.of(timeZone);
|
||||
sdf = java.time.format.DateTimeFormatter.ofPattern(timeFormat).withZone(sdfZone);
|
||||
sdfDate = java.time.format.DateTimeFormatter.ofPattern(dateFormat).withZone(sdfZone);
|
||||
} catch (Exception e) {
|
||||
sdf = new java.text.SimpleDateFormat("HH:mm");
|
||||
sdfDate = new java.text.SimpleDateFormat("dd.MM.yyyy");
|
||||
sdfZone = java.time.ZoneId.systemDefault();
|
||||
sdf = java.time.format.DateTimeFormatter.ofPattern("HH:mm").withZone(sdfZone);
|
||||
sdfDate = java.time.format.DateTimeFormatter.ofPattern("dd.MM.yyyy").withZone(sdfZone);
|
||||
}
|
||||
try {
|
||||
df = new DecimalFormat(moneyFormat);
|
||||
|
||||
@@ -1,6 +1,8 @@
|
||||
package net.viper.status.stats;
|
||||
|
||||
import java.io.*;
|
||||
import java.nio.file.Files;
|
||||
import java.nio.file.StandardCopyOption;
|
||||
|
||||
/**
|
||||
* Fix #9: save() und load() sind jetzt synchronized um Race Conditions
|
||||
@@ -17,12 +19,21 @@ public class StatsStorage {
|
||||
|
||||
public void save(StatsManager manager) {
|
||||
synchronized (fileLock) {
|
||||
try (BufferedWriter bw = new BufferedWriter(new FileWriter(file))) {
|
||||
// Atomares Schreiben: erst in .tmp, dann umbenennen.
|
||||
// Verhindert korrupte stats.dat bei Server-Absturz während des Schreibvorgangs.
|
||||
File tmp = new File(file.getParentFile(), "stats.dat.tmp");
|
||||
try (BufferedWriter bw = new BufferedWriter(new FileWriter(tmp))) {
|
||||
for (PlayerStats ps : manager.all()) {
|
||||
bw.write(ps.toLine());
|
||||
bw.newLine();
|
||||
}
|
||||
bw.flush();
|
||||
} catch (IOException e) {
|
||||
e.printStackTrace();
|
||||
return; // Nicht umbenennen bei Fehler
|
||||
}
|
||||
try {
|
||||
Files.move(tmp.toPath(), file.toPath(), StandardCopyOption.REPLACE_EXISTING, StandardCopyOption.ATOMIC_MOVE);
|
||||
} catch (IOException e) {
|
||||
e.printStackTrace();
|
||||
}
|
||||
|
||||
@@ -1,18 +1,52 @@
|
||||
§8[§2Viper-Netzwerk§8] §7Der Server läuft 24/7 – also keine Hektik beim Spielen :)
|
||||
§8[§2Viper-Netzwerk§8] §7Dies ist ein privater Server – hier zählt der Zusammenhalt.
|
||||
§8[§dTipp§8] §7Wenn du denkst, du bist sicher… schau nochmal nach. Creeper machen keine Geräusche beim Tippen.
|
||||
§8[§2Viper-Netzwerk§8] §7Wähle einen Server, leg los – der Rest ergibt sich. Oder explodiert.
|
||||
§8[§2Viper-Netzwerk§8] §7Mehr Server. Mehr Blöcke. Mehr Unfälle. Willkommen!
|
||||
§8[§dTipp§8] §7Halte eine Spitzhacke mit Glück bereit. Man weiß nie, wann das nächste Erz kommt.
|
||||
§8[§dTipp§8] §7Mit §e/home§7 kannst du dich jederzeit nach Hause teleportieren.
|
||||
§8[§2Viper-Netzwerk§8] §7Das wichtigste Plugin? Du selbst. Spiel fair, sei kreativ!
|
||||
§8[§2Viper-Netzwerk§8] §7Redstone ist keine Magie – aber fast.
|
||||
§8[§dTipp§8] §7Schilde sind cool. Besonders wenn Skelette zielen.
|
||||
§8[§2Viper-Netzwerk§8] §7Wenn du in Lava fällst, bist du nicht der Erste. Nur der Nächste.
|
||||
§8[§dTipp§8] §7Villager sind nicht dumm – nur sehr… eigen.
|
||||
§8[§2Viper-Netzwerk§8] §7Bau groß, bau sicher – oder bau eine Treppe zur Nachbarschaftsklage.
|
||||
§8[§2Viper-Netzwerk§8] §7Gras wächst. Spieler auch. Gib jedem eine Chance!
|
||||
§8[§2Viper-Netzwerk§8] §7Ein Creeper ist keine Begrüßung. Es sei denn, du willst es spannend machen.
|
||||
§8[§dTipp§8] §7Ein voller Magen ist halbe Miete. Farmen lohnt sich!
|
||||
§8[§2Viper-Netzwerk§8] §7Wir haben keine Probleme – nur Redstone-Schaltungen mit Charakter.
|
||||
§8[§dTipp§8] §7Markiere dein Grundstück mit §e/p claim§7, bevor es jemand anderes tut!
|
||||
§8[%gradient:&b:&f:&b:&lViper-Network%§8] §7Der Server läuft 24/7 – also keine Hektik beim Spielen :)
|
||||
§8[%gradient:&b:&f:&b:&lViper-Network%§8] §7Dies ist ein privater Server – hier zählt der Zusammenhalt.
|
||||
§8[%gradient:&b:&f:&b:&lTipp%§8] §7Wenn du denkst, du bist sicher… schau nochmal nach. Creeper machen keine Geräusche beim Tippen.
|
||||
§8[%gradient:&b:&f:&b:&lViper-Network%§8] §7Wähle einen Server, leg los – der Rest ergibt sich. Oder explodiert.
|
||||
§8[%gradient:&b:&f:&b:&lViper-Network%§8] §7Mehr Server. Mehr Blöcke. Mehr Unfälle. Willkommen!
|
||||
§8[%gradient:&b:&f:&b:&lTipp%§8] §7Halte eine Spitzhacke mit Glück bereit. Man weiß nie, wann das nächste Erz kommt.
|
||||
§8[%gradient:&b:&f:&b:&lTipp%§8] §7Dein Bett ist dein Ankerpunkt – platziere es weise!
|
||||
§8[%gradient:&b:&f:&b:&lViper-Network%§8] §7Das wichtigste Plugin? Du selbst. Spiel fair, sei kreativ!
|
||||
§8[%gradient:&b:&f:&b:&lViper-Network%§8] §7Redstone ist keine Magie – aber fast.
|
||||
§8[%gradient:&b:&f:&b:&lTipp%§8] §7Schilde sind cool. Besonders wenn Skelette zielen.
|
||||
§8[%gradient:&b:&f:&b:&lViper-Network%§8] §7Wenn du in Lava fällst, bist du nicht der Erste. Nur der Nächste.
|
||||
§8[%gradient:&b:&f:&b:&lTipp%§8] §7Villager sind nicht dumm – nur sehr… eigen.
|
||||
§8[%gradient:&b:&f:&b:&lViper-Network%§8] §7Bau groß, bau sicher – oder bau eine Treppe zur Nachbarschaftsklage.
|
||||
§8[%gradient:&b:&f:&b:&lViper-Network%§8] §7Gras wächst. Spieler auch. Gib jedem eine Chance!
|
||||
§8[%gradient:&b:&f:&b:&lViper-Network%§8] §7Ein Creeper ist keine Begrüßung. Es sei denn, du willst es spannend machen.
|
||||
§8[%gradient:&b:&f:&b:&lTipp%§8] §7Ein voller Magen ist halbe Miete. Farmen lohnt sich!
|
||||
§8[%gradient:&b:&f:&b:&lViper-Network%§8] §7Wir haben keine Probleme – nur Redstone-Schaltungen mit Charakter.
|
||||
§8[%gradient:&b:&f:&b:&lTipp%§8] §7Markiere dein Grundstück frühzeitig, bevor es jemand anderes tut!
|
||||
§8[%gradient:&b:&f:&b:&lLobby%§8] §7Hier beginnt alles – such dir deinen Server aus und leg los!
|
||||
§8[%gradient:&b:&f:&b:&lLobby%§8] §7Die Lobby ist kein Ziel, sondern der Start. Oder ein Treffpunkt. Oder beides.
|
||||
§8[%gradient:&b:&f:&b:&lLobby%§8] §7Noch unentschlossen? Einfach mal reinschauen – auf allen Servern!
|
||||
§8[%gradient:&b:&f:&b:&lCitybuild%§8] §7Auf Citybuild baust du deine eigene Stadt – Block für Block.
|
||||
§8[%gradient:&b:&f:&b:&lCitybuild%§8] §7Grundstück sichern nicht vergessen – sonst baut jemand anderes drauf!
|
||||
§8[%gradient:&b:&f:&b:&lCitybuild%§8] §7Gute Nachbarn bauen gute Städte. Oder zumindest hübschere.
|
||||
§8[%gradient:&b:&f:&b:&lCitybuild%§8] §7Je größer die Stadt, desto mehr Redstone geht schief. Viel Erfolg!
|
||||
§8[%gradient:&b:&f:&b:&lTipp%§8] §7Auf Citybuild kannst du dein Grundstück mit Freunden teilen – baut zusammen mehr!
|
||||
§8[%gradient:&b:&f:&b:&lFreebuild%§8] §7Auf Freebuild gibt es keine Grenzen – außer deiner Fantasie.
|
||||
§8[%gradient:&b:&f:&b:&lFreebuild%§8] §7Bauen ohne Regeln. Na ja, fast. Sei trotzdem nett zu deinen Nachbarn.
|
||||
§8[%gradient:&b:&f:&b:&lFreebuild%§8] §7Riesige Projekte, verrückte Ideen – Freebuild macht's möglich.
|
||||
§8[%gradient:&b:&f:&b:&lTipp%§8] §7Auf Freebuild gilt: Erst messen, dann bauen. Oder erst bauen und dann bereuen.
|
||||
§8[%gradient:&b:&f:&b:&lSurvival%§8] §7Auf Survival zählt jeder Block – den du abbaust und den, der auf dich fällt.
|
||||
§8[%gradient:&b:&f:&b:&lSurvival%§8] §7Tag 1: Holz hacken. Tag 2: Höhle finden. Tag 3: Creeper ärgert dich. Klassiker.
|
||||
§8[%gradient:&b:&f:&b:&lSurvival%§8] §7Die Nether-Festung wartet. Bring Feuerresistenz mit – und Mut.
|
||||
§8[%gradient:&b:&f:&b:&lSurvival%§8] §7Ein Bett in der Nähe spart lange Laufwege nach dem Tod. Spar dir den Fußmarsch!
|
||||
§8[%gradient:&b:&f:&b:&lTipp%§8] §7Auf Survival lohnt sich ein Außenposten – Ressourcen gibt's nie genug.
|
||||
§8[%gradient:&b:&f:&b:&lTipp%§8] §7Verzaubere deine Ausrüstung so früh wie möglich. Schutzverzauberung rettet Leben!
|
||||
§8[%gradient:&b:&f:&b:&lSkyblock%§8] §7Auf Skyblock fängst du mit einer Insel an. Was du draus machst, liegt bei dir.
|
||||
§8[%gradient:&b:&f:&b:&lSkyblock%§8] §7Wasser + Lava = Kobblestone. Das Fundament jeder Skyblock-Karriere.
|
||||
§8[%gradient:&b:&f:&b:&lSkyblock%§8] §7Deine Insel, deine Regeln – aber Creeper kennen keine Regeln. Und keine Ränder.
|
||||
§8[%gradient:&b:&f:&b:&lSkyblock%§8] §7Tipp vom Profi: Nie rückwärts auf einer Skyblock-Insel laufen.
|
||||
§8[%gradient:&b:&f:&b:&lTipp%§8] §7Auf Skyblock ist ein Kompostierer Gold wert – Essen ist alles!
|
||||
§8[%gradient:&b:&f:&b:&lTipp%§8] §7Baue deine Skyblock-Insel nach außen aus, bevor du in die Höhe gehst.
|
||||
§8[%gradient:&b:&f:&b:&lMinigames%§8] §7Auf Minigames zählt Schnelligkeit, Köpfchen – und manchmal einfach Glück.
|
||||
§8[%gradient:&b:&f:&b:&lMinigames%§8] §7Gewonnen oder verloren – beim nächsten Spiel ist alles wieder offen!
|
||||
§8[%gradient:&b:&f:&b:&lMinigames%§8] §7Minigames sind der perfekte Ort, um Freunde zu finden. Oder Rivalen.
|
||||
§8[%gradient:&b:&f:&b:&lMinigames%§8] §7Kurze Runden, viel Spaß – einfach Minigames starten und loslegen!
|
||||
§8[%gradient:&b:&f:&b:&lViper-Network%§8] §7Sechs Server, ein Netzwerk – such dir deinen Lieblingsplatz!
|
||||
§8[%gradient:&b:&f:&b:&lViper-Network%§8] §7Egal ob Bauen, Überleben oder Kämpfen – hier ist für jeden was dabei.
|
||||
§8[%gradient:&b:&f:&b:&lViper-Network%§8] §7Neue Spieler sind immer willkommen. Zeig ihnen, wie es geht!
|
||||
§8[%gradient:&b:&f:&b:&lViper-Network%§8] §7Respekt kostet nichts – macht aber den Server für alle besser.
|
||||
§8[%gradient:&b:&f:&b:&lViper-Network%§8] §7Probleme oder Fragen? Wende dich ans Team – wir helfen gerne!
|
||||
§8[%gradient:&b:&f:&b:&lTipp%§8] §7Du kannst zwischen allen Servern wechseln – einfach den Kompass nutzen!
|
||||
@@ -1,6 +1,6 @@
|
||||
name: StatusAPI
|
||||
main: net.viper.status.StatusAPI
|
||||
version: 4.1.4
|
||||
version: 4.1.5
|
||||
author: M_Viper
|
||||
description: StatusAPI für BungeeCord inkl. Update-Checker, Modul-System und ChatModule
|
||||
# Mindestanforderung: Minecraft 1.20 / BungeeCord mit PlayerChatEvent-Unterstützung
|
||||
|
||||
+31
-9
@@ -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 -> {
|
||||
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");
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user