Upload folder via GUI - src
This commit is contained in:
@@ -56,10 +56,13 @@ public class StatusAPI extends Plugin implements Runnable {
|
|||||||
// Kontostand pro Spieler (UUID -> Balance), wird von StatusAPIBridge gepusht
|
// Kontostand pro Spieler (UUID -> Balance), wird von StatusAPIBridge gepusht
|
||||||
public static final ConcurrentHashMap<UUID, Double> playerBalances = new ConcurrentHashMap<>();
|
public static final ConcurrentHashMap<UUID, Double> playerBalances = new ConcurrentHashMap<>();
|
||||||
|
|
||||||
// PlaceholderAPI-Werte pro Spieler (UUID -> (placeholder -> aufgelöster Wert))
|
// PlaceholderAPI-Werte pro Spieler (UUID -> (placeholder -> aufgel\u00f6ster Wert))
|
||||||
public static final ConcurrentHashMap<UUID, Map<String, String>> playerPapi = new ConcurrentHashMap<>();
|
public static final ConcurrentHashMap<UUID, Map<String, String>> playerPapi = new ConcurrentHashMap<>();
|
||||||
|
|
||||||
/** Alle %token%-Tokens aus den Config-Dateien – als JSON-Array für GET /papi/tokens */
|
// AFK-Status pro Spieler (UUID -> true wenn AFK), wird von StatusAPIBridge gepusht
|
||||||
|
public static final ConcurrentHashMap<UUID, Boolean> playerAfk = new ConcurrentHashMap<>();
|
||||||
|
|
||||||
|
/** Alle %token%-Tokens aus den Config-Dateien – als JSON-Array f\u00fcr GET /papi/tokens */
|
||||||
public static volatile String papiTokensJson = "[]";
|
public static volatile String papiTokensJson = "[]";
|
||||||
|
|
||||||
// Debug-Modus (aus verify.properties)
|
// Debug-Modus (aus verify.properties)
|
||||||
@@ -102,7 +105,7 @@ public class StatusAPI extends Plugin implements Runnable {
|
|||||||
try {
|
try {
|
||||||
port = Integer.parseInt(portStr);
|
port = Integer.parseInt(portStr);
|
||||||
} catch (NumberFormatException e) {
|
} catch (NumberFormatException e) {
|
||||||
getLogger().warning("Ungültiger Port in verify.properties, nutze Standard-Port 9191.");
|
getLogger().warning("Ung\u00fcltiger Port in verify.properties, nutze Standard-Port 9191.");
|
||||||
port = 9191;
|
port = 9191;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -112,7 +115,8 @@ public class StatusAPI extends Plugin implements Runnable {
|
|||||||
moduleManager = new ModuleManager();
|
moduleManager = new ModuleManager();
|
||||||
|
|
||||||
// Module in korrekter Reihenfolge registrieren
|
// Module in korrekter Reihenfolge registrieren
|
||||||
// VanishModule MUSS vor ChatModule registriert werden (VanishProvider-Abhängigkeit)
|
// VanishModule MUSS vor ChatModule registriert werden (VanishProvider-Abh\u00e4ngigkeit)
|
||||||
|
moduleManager.registerModule(new net.viper.status.modules.afk.AfkModule());
|
||||||
moduleManager.registerModule(new StatsModule());
|
moduleManager.registerModule(new StatsModule());
|
||||||
moduleManager.registerModule(new HelpModule());
|
moduleManager.registerModule(new HelpModule());
|
||||||
moduleManager.registerModule(new VerifyModule());
|
moduleManager.registerModule(new VerifyModule());
|
||||||
@@ -280,7 +284,7 @@ public class StatusAPI extends Plugin implements Runnable {
|
|||||||
if (updateChecker.isUpdateAvailable(currentVersion)) {
|
if (updateChecker.isUpdateAvailable(currentVersion)) {
|
||||||
String newVersion = updateChecker.getLatestVersion();
|
String newVersion = updateChecker.getLatestVersion();
|
||||||
getLogger().warning("----------------------------------------");
|
getLogger().warning("----------------------------------------");
|
||||||
getLogger().warning("Neue Version verfügbar: " + newVersion);
|
getLogger().warning("Neue Version verf\u00fcgbar: " + newVersion);
|
||||||
getLogger().warning("Download: " + updateChecker.getLatestUrl());
|
getLogger().warning("Download: " + updateChecker.getLatestUrl());
|
||||||
getLogger().warning("----------------------------------------");
|
getLogger().warning("----------------------------------------");
|
||||||
}
|
}
|
||||||
@@ -308,7 +312,7 @@ public class StatusAPI extends Plugin implements Runnable {
|
|||||||
Socket clientSocket = localServerSocket.accept();
|
Socket clientSocket = localServerSocket.accept();
|
||||||
submitConnection(clientSocket);
|
submitConnection(clientSocket);
|
||||||
} catch (SocketTimeoutException ignored) {
|
} catch (SocketTimeoutException ignored) {
|
||||||
// Poll-Schleife für Interrupt/Shutdown
|
// Poll-Schleife f\u00fcr Interrupt/Shutdown
|
||||||
} catch (IOException e) {
|
} catch (IOException e) {
|
||||||
if (!shuttingDown) {
|
if (!shuttingDown) {
|
||||||
getLogger().warning("Fehler beim Akzeptieren einer Verbindung: " + e.getMessage());
|
getLogger().warning("Fehler beim Akzeptieren einer Verbindung: " + e.getMessage());
|
||||||
@@ -578,7 +582,7 @@ public class StatusAPI extends Plugin implements Runnable {
|
|||||||
playerMap.put("kills", ps.kills);
|
playerMap.put("kills", ps.kills);
|
||||||
playerMap.put("deaths", ps.deaths);
|
playerMap.put("deaths", ps.deaths);
|
||||||
playerMap.put("online", ProxyServer.getInstance().getPlayer(ps.uuid) != null);
|
playerMap.put("online", ProxyServer.getInstance().getPlayer(ps.uuid) != null);
|
||||||
// Balance direkt aus MySQL (serverübergreifend)
|
// Balance direkt aus MySQL (server\u00fcbergreifend)
|
||||||
double playerBalance = playerBalances.getOrDefault(ps.uuid, 0.0);
|
double playerBalance = playerBalances.getOrDefault(ps.uuid, 0.0);
|
||||||
Map<String, Object> economy = new LinkedHashMap<>();
|
Map<String, Object> economy = new LinkedHashMap<>();
|
||||||
economy.put("balance", playerBalance);
|
economy.put("balance", playerBalance);
|
||||||
@@ -603,7 +607,7 @@ public class StatusAPI extends Plugin implements Runnable {
|
|||||||
// Kein Cache – UUID und Balance kommen direkt aus der DB
|
// Kein Cache – UUID und Balance kommen direkt aus der DB
|
||||||
if ("GET".equalsIgnoreCase(method) && "/economy/player".equalsIgnoreCase(pathOnly)) {
|
if ("GET".equalsIgnoreCase(method) && "/economy/player".equalsIgnoreCase(pathOnly)) {
|
||||||
Map<String, String> qp = parseQueryParams(path);
|
Map<String, String> qp = parseQueryParams(path);
|
||||||
// UUID auflösen aus Query-Param
|
// UUID aufl\u00f6sen aus Query-Param
|
||||||
UUID ecoUuid = null;
|
UUID ecoUuid = null;
|
||||||
String ecoName = null;
|
String ecoName = null;
|
||||||
String uuidParam = qp.get("uuid");
|
String uuidParam = qp.get("uuid");
|
||||||
@@ -616,7 +620,7 @@ public class StatusAPI extends Plugin implements Runnable {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
if (ecoName == null) ecoName = uuidParam;
|
if (ecoName == null) ecoName = uuidParam;
|
||||||
// Balance aus playerBalances Map lesen (befüllt von StatusAPIBridge via NexEco)
|
// Balance aus playerBalances Map lesen (bef\u00fcllt von StatusAPIBridge via NexEco)
|
||||||
double directBalance = playerBalances.getOrDefault(ecoUuid, 0.0);
|
double directBalance = playerBalances.getOrDefault(ecoUuid, 0.0);
|
||||||
Map<String, Object> payload = new LinkedHashMap<>();
|
Map<String, Object> payload = new LinkedHashMap<>();
|
||||||
payload.put("success", true);
|
payload.put("success", true);
|
||||||
@@ -630,11 +634,11 @@ public class StatusAPI extends Plugin implements Runnable {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// POST /economy/update
|
// POST /economy/update
|
||||||
// Empfängt Balance-Updates von StatusAPIBridge (Vault/NexEco → HTTP)
|
// Empf\u00e4ngt Balance-Updates von StatusAPIBridge (Vault/NexEco → HTTP)
|
||||||
// Schreibt NUR in playerBalances für Tablist/Scoreboard – KEINE DB-Schreiboperationen
|
// Schreibt NUR in playerBalances f\u00fcr Tablist/Scoreboard – KEINE DB-Schreiboperationen
|
||||||
if ("POST".equalsIgnoreCase(method) && "/economy/update".equalsIgnoreCase(pathOnly)) {
|
if ("POST".equalsIgnoreCase(method) && "/economy/update".equalsIgnoreCase(pathOnly)) {
|
||||||
String body = readBody(in, headers);
|
String body = readBody(in, headers);
|
||||||
// UUID auflösen
|
// UUID aufl\u00f6sen
|
||||||
UUID ecoUpdUuid = null;
|
UUID ecoUpdUuid = null;
|
||||||
String uuidBody = extractJsonString(body, "uuid");
|
String uuidBody = extractJsonString(body, "uuid");
|
||||||
if (uuidBody != null && !uuidBody.isEmpty()) {
|
if (uuidBody != null && !uuidBody.isEmpty()) {
|
||||||
@@ -644,8 +648,8 @@ public class StatusAPI extends Plugin implements Runnable {
|
|||||||
sendHttpResponse(out, "{\"success\":false,\"error\":\"player_not_found\"}", 404);
|
sendHttpResponse(out, "{\"success\":false,\"error\":\"player_not_found\"}", 404);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
// Balance NUR in playerBalances Map speichern (für Tablist/Scoreboard)
|
// Balance NUR in playerBalances Map speichern (f\u00fcr Tablist/Scoreboard)
|
||||||
// Die echte DB-Verwaltung macht ausschließlich NexEco
|
// Die echte DB-Verwaltung macht ausschlie\u00dflich NexEco
|
||||||
String balStr = extractJsonString(body, "balance");
|
String balStr = extractJsonString(body, "balance");
|
||||||
if (balStr != null && !balStr.isEmpty()) {
|
if (balStr != null && !balStr.isEmpty()) {
|
||||||
try {
|
try {
|
||||||
@@ -733,7 +737,7 @@ public class StatusAPI extends Plugin implements Runnable {
|
|||||||
// Playtime auch updaten
|
// Playtime auch updaten
|
||||||
String playtimeStr = extractJsonString(body, "playtime");
|
String playtimeStr = extractJsonString(body, "playtime");
|
||||||
synchronized (psUpd) {
|
synchronized (psUpd) {
|
||||||
// HÖCHSTER WERT gewinnt – mehrere Unterserver können unterschiedliche Werte haben
|
// H\u00d6CHSTER WERT gewinnt – mehrere Unterserver k\u00f6nnen unterschiedliche Werte haben
|
||||||
try { if (killsStr != null && !killsStr.isEmpty()) {
|
try { if (killsStr != null && !killsStr.isEmpty()) {
|
||||||
int v = Integer.parseInt(killsStr.trim());
|
int v = Integer.parseInt(killsStr.trim());
|
||||||
if (v > psUpd.kills) psUpd.kills = v;
|
if (v > psUpd.kills) psUpd.kills = v;
|
||||||
@@ -775,6 +779,13 @@ public class StatusAPI extends Plugin implements Runnable {
|
|||||||
if (uuidStr != null && !uuidStr.isEmpty() && compassStr != null && !compassStr.isEmpty()) {
|
if (uuidStr != null && !uuidStr.isEmpty() && compassStr != null && !compassStr.isEmpty()) {
|
||||||
try {
|
try {
|
||||||
UUID cUuid = UUID.fromString(uuidStr.trim());
|
UUID cUuid = UUID.fromString(uuidStr.trim());
|
||||||
|
// Yaw-\u00c4nderung = Mausbewegung → AFK aufheben
|
||||||
|
String oldCompass = net.viper.status.modules.scoreboard.ScoreboardModule.playerCompass.get(cUuid);
|
||||||
|
if (oldCompass != null && !oldCompass.equals(compassStr.trim())
|
||||||
|
&& Boolean.TRUE.equals(playerAfk.get(cUuid))) {
|
||||||
|
net.viper.status.modules.afk.AfkModule.unAfkByMovement(cUuid);
|
||||||
|
}
|
||||||
|
net.viper.status.modules.afk.AfkModule.recordActivity(cUuid);
|
||||||
net.viper.status.modules.scoreboard.ScoreboardModule.playerCompass.put(cUuid, compassStr.trim());
|
net.viper.status.modules.scoreboard.ScoreboardModule.playerCompass.put(cUuid, compassStr.trim());
|
||||||
} catch (Exception ignored) {}
|
} catch (Exception ignored) {}
|
||||||
}
|
}
|
||||||
@@ -812,6 +823,22 @@ public class StatusAPI extends Plugin implements Runnable {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// POST /player/afk – AFK-Status eines Spielers setzen (von StatusAPIBridge)
|
||||||
|
if ("POST".equalsIgnoreCase(method) && "/player/afk".equalsIgnoreCase(pathOnly)) {
|
||||||
|
String body = readBody(in, headers);
|
||||||
|
String uuidStr = extractJsonString(body, "uuid");
|
||||||
|
String afkStr = extractJsonString(body, "afk");
|
||||||
|
if (uuidStr != null && !uuidStr.isEmpty() && afkStr != null) {
|
||||||
|
try {
|
||||||
|
UUID uid = UUID.fromString(uuidStr.trim());
|
||||||
|
boolean isAfk = Boolean.parseBoolean(afkStr.trim());
|
||||||
|
net.viper.status.modules.afk.AfkModule.setBridgeAfk(uid, isAfk);
|
||||||
|
} catch (IllegalArgumentException ignored) {}
|
||||||
|
}
|
||||||
|
sendHttpResponse(out, "{\"success\":true}", 200);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
// POST /ticket/update – TicketSystem Daten (von StatusAPIBridge)
|
// POST /ticket/update – TicketSystem Daten (von StatusAPIBridge)
|
||||||
if ("POST".equalsIgnoreCase(method) && "/ticket/update".equalsIgnoreCase(pathOnly)) {
|
if ("POST".equalsIgnoreCase(method) && "/ticket/update".equalsIgnoreCase(pathOnly)) {
|
||||||
String body = readBody(in, headers);
|
String body = readBody(in, headers);
|
||||||
@@ -853,6 +880,23 @@ public class StatusAPI extends Plugin implements Runnable {
|
|||||||
String fdS = extractJsonString(body, "food");
|
String fdS = extractJsonString(body, "food");
|
||||||
String spS = extractJsonString(body, "speed");
|
String spS = extractJsonString(body, "speed");
|
||||||
String wld = extractJsonString(body, "world");
|
String wld = extractJsonString(body, "world");
|
||||||
|
// Bewegungserkennung: Koordinaten mit letzten Werten vergleichen
|
||||||
|
if (xS != null && yS != null && zS != null) {
|
||||||
|
int newX = (int) Double.parseDouble(xS);
|
||||||
|
int newY = (int) Double.parseDouble(yS);
|
||||||
|
int newZ = (int) Double.parseDouble(zS);
|
||||||
|
Integer oldX = net.viper.status.modules.scoreboard.ScoreboardModule.playerX.get(uid);
|
||||||
|
Integer oldY = net.viper.status.modules.scoreboard.ScoreboardModule.playerY.get(uid);
|
||||||
|
Integer oldZ = net.viper.status.modules.scoreboard.ScoreboardModule.playerZ.get(uid);
|
||||||
|
if (oldX != null && (newX != oldX || newY != oldY || newZ != oldZ)) {
|
||||||
|
net.viper.status.modules.afk.AfkModule.recordActivity(uid);
|
||||||
|
// AFK aufheben wenn Spieler sich bewegt hat
|
||||||
|
if (Boolean.TRUE.equals(playerAfk.get(uid))) {
|
||||||
|
ProxiedPlayer mover = ProxyServer.getInstance().getPlayer(uid);
|
||||||
|
if (mover != null) net.viper.status.modules.afk.AfkModule.unAfkByMovement(uid);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
if (xS != null) net.viper.status.modules.scoreboard.ScoreboardModule.playerX.put(uid, (int)Double.parseDouble(xS));
|
if (xS != null) net.viper.status.modules.scoreboard.ScoreboardModule.playerX.put(uid, (int)Double.parseDouble(xS));
|
||||||
if (yS != null) net.viper.status.modules.scoreboard.ScoreboardModule.playerY.put(uid, (int)Double.parseDouble(yS));
|
if (yS != null) net.viper.status.modules.scoreboard.ScoreboardModule.playerY.put(uid, (int)Double.parseDouble(yS));
|
||||||
if (zS != null) net.viper.status.modules.scoreboard.ScoreboardModule.playerZ.put(uid, (int)Double.parseDouble(zS));
|
if (zS != null) net.viper.status.modules.scoreboard.ScoreboardModule.playerZ.put(uid, (int)Double.parseDouble(zS));
|
||||||
@@ -873,7 +917,7 @@ public class StatusAPI extends Plugin implements Runnable {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
// POST /player/papi – empfängt von StatusAPIBridge aufgelöste PAPI-Werte
|
// POST /player/papi – empf\u00e4ngt von StatusAPIBridge aufgel\u00f6ste PAPI-Werte
|
||||||
if ("POST".equalsIgnoreCase(method) && "/player/papi".equalsIgnoreCase(pathOnly)) {
|
if ("POST".equalsIgnoreCase(method) && "/player/papi".equalsIgnoreCase(pathOnly)) {
|
||||||
String body = readBody(in, headers);
|
String body = readBody(in, headers);
|
||||||
String uuidStr = extractJsonString(body, "uuid");
|
String uuidStr = extractJsonString(body, "uuid");
|
||||||
@@ -1011,7 +1055,7 @@ public class StatusAPI extends Plugin implements Runnable {
|
|||||||
playerInfo.put("deaths", ps.deaths);
|
playerInfo.put("deaths", ps.deaths);
|
||||||
playerInfo.put("first_seen", ps.firstSeen);
|
playerInfo.put("first_seen", ps.firstSeen);
|
||||||
playerInfo.put("last_seen", ps.lastSeen);
|
playerInfo.put("last_seen", ps.lastSeen);
|
||||||
// Balance direkt aus MySQL (serverübergreifend)
|
// Balance direkt aus MySQL (server\u00fcbergreifend)
|
||||||
double statusBalance = playerBalances.getOrDefault(p.getUniqueId(), 0.0);
|
double statusBalance = playerBalances.getOrDefault(p.getUniqueId(), 0.0);
|
||||||
Map<String, Object> eco = new LinkedHashMap<>();
|
Map<String, Object> eco = new LinkedHashMap<>();
|
||||||
eco.put("balance", statusBalance);
|
eco.put("balance", statusBalance);
|
||||||
@@ -1298,7 +1342,7 @@ public class StatusAPI extends Plugin implements Runnable {
|
|||||||
|
|
||||||
// ── PAPI-Token-Erkennung ──────────────────────────────────────────────────
|
// ── PAPI-Token-Erkennung ──────────────────────────────────────────────────
|
||||||
|
|
||||||
/** Alle Tokens die StatusAPI selbst auflöst – werden nicht an PAPI weitergegeben */
|
/** Alle Tokens die StatusAPI selbst aufl\u00f6st – werden nicht an PAPI weitergegeben */
|
||||||
private static final Set<String> NATIVE_TOKENS = new HashSet<>(Arrays.asList(
|
private static final Set<String> NATIVE_TOKENS = new HashSet<>(Arrays.asList(
|
||||||
"player", "rank", "money", "server", "compass", "health", "hearts", "ping",
|
"player", "rank", "money", "server", "compass", "health", "hearts", "ping",
|
||||||
"online", "maxplayers", "tps", "ram", "time", "playtime", "x", "y", "z",
|
"online", "maxplayers", "tps", "ram", "time", "playtime", "x", "y", "z",
|
||||||
@@ -1310,8 +1354,8 @@ public class StatusAPI extends Plugin implements Runnable {
|
|||||||
|
|
||||||
/**
|
/**
|
||||||
* Scannt alle .properties-Dateien im Plugin-Ordner nach %token%-Mustern,
|
* Scannt alle .properties-Dateien im Plugin-Ordner nach %token%-Mustern,
|
||||||
* filtert nativ unterstützte Tokens heraus und veröffentlicht den Rest
|
* filtert nativ unterst\u00fctzte Tokens heraus und ver\u00f6ffentlicht den Rest
|
||||||
* als JSON-Array unter GET /papi/tokens für StatusAPIBridge.
|
* als JSON-Array unter GET /papi/tokens f\u00fcr StatusAPIBridge.
|
||||||
*/
|
*/
|
||||||
public void scanAndPublishPapiTokens() {
|
public void scanAndPublishPapiTokens() {
|
||||||
Set<String> tokens = new LinkedHashSet<>();
|
Set<String> tokens = new LinkedHashSet<>();
|
||||||
@@ -1378,8 +1422,8 @@ public class StatusAPI extends Plugin implements Runnable {
|
|||||||
// ── Reload ────────────────────────────────────────────────────────────────
|
// ── Reload ────────────────────────────────────────────────────────────────
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Lädt Scoreboard und Tablist neu (Config + Tasks), ohne den HTTP-Server zu berühren.
|
* L\u00e4dt Scoreboard und Tablist neu (Config + Tasks), ohne den HTTP-Server zu ber\u00fchren.
|
||||||
* Alle anderen Module (Chat, AntiBot, etc.) bleiben unberührt.
|
* Alle anderen Module (Chat, AntiBot, etc.) bleiben unber\u00fchrt.
|
||||||
*/
|
*/
|
||||||
public void reloadModules() {
|
public void reloadModules() {
|
||||||
getLogger().info("[StatusAPI] Reload von Scoreboard und Tablist...");
|
getLogger().info("[StatusAPI] Reload von Scoreboard und Tablist...");
|
||||||
@@ -1432,7 +1476,7 @@ public class StatusAPI extends Plugin implements Runnable {
|
|||||||
if (isAdmin) {
|
if (isAdmin) {
|
||||||
send(sender, "&e/statusapi reload &7– Scoreboard & Tablist neu laden");
|
send(sender, "&e/statusapi reload &7– Scoreboard & Tablist neu laden");
|
||||||
} else {
|
} else {
|
||||||
send(sender, "&7Keine weiteren Unterbefehle verfügbar.");
|
send(sender, "&7Keine weiteren Unterbefehle verf\u00fcgbar.");
|
||||||
}
|
}
|
||||||
send(sender, "&8&m──────────────────────────────────────────");
|
send(sender, "&8&m──────────────────────────────────────────");
|
||||||
return;
|
return;
|
||||||
|
|||||||
@@ -16,7 +16,7 @@ public class UpdateChecker {
|
|||||||
private final String currentVersion;
|
private final String currentVersion;
|
||||||
private final int intervalHours;
|
private final int intervalHours;
|
||||||
|
|
||||||
// Neue Domain und korrekter API-Pfad für Releases
|
// 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 final String apiUrl = "https://git.viper.ipv64.net/api/v1/repos/M_Viper/StatusAPI/releases";
|
||||||
|
|
||||||
private volatile String latestVersion = "";
|
private volatile String latestVersion = "";
|
||||||
@@ -55,7 +55,7 @@ public class UpdateChecker {
|
|||||||
|
|
||||||
String body = sb.toString();
|
String body = sb.toString();
|
||||||
|
|
||||||
// Neu: Da die API ein JSON-Array von Releases zurückgibt, nehmen wir das erste (neueste) Release
|
// Neu: Da die API ein JSON-Array von Releases zur\u00fcckgibt, nehmen wir das erste (neueste) Release
|
||||||
// Wir suchen den ersten Block mit tag_name
|
// Wir suchen den ersten Block mit tag_name
|
||||||
String foundVersion = null;
|
String foundVersion = null;
|
||||||
Matcher tagM = TAG_NAME_PATTERN.matcher(body);
|
Matcher tagM = TAG_NAME_PATTERN.matcher(body);
|
||||||
|
|||||||
@@ -3,7 +3,7 @@ package net.viper.status.module;
|
|||||||
import net.md_5.bungee.api.plugin.Plugin;
|
import net.md_5.bungee.api.plugin.Plugin;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Interface für alle zukünftigen Erweiterungen.
|
* Interface f\u00fcr alle zuk\u00fcnftigen Erweiterungen.
|
||||||
*/
|
*/
|
||||||
public interface Module {
|
public interface Module {
|
||||||
|
|
||||||
|
|||||||
@@ -8,7 +8,7 @@ import java.util.Map;
|
|||||||
/**
|
/**
|
||||||
* Verwaltet alle geladenen Module.
|
* Verwaltet alle geladenen Module.
|
||||||
* Verwendet LinkedHashMap um die Registrierungsreihenfolge zu erhalten,
|
* Verwendet LinkedHashMap um die Registrierungsreihenfolge zu erhalten,
|
||||||
* damit Abhängigkeiten (z.B. VanishModule → ChatModule) korrekt aufgelöst werden.
|
* damit Abh\u00e4ngigkeiten (z.B. VanishModule → ChatModule) korrekt aufgel\u00f6st werden.
|
||||||
*/
|
*/
|
||||||
public class ModuleManager {
|
public class ModuleManager {
|
||||||
|
|
||||||
@@ -41,14 +41,14 @@ public class ModuleManager {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Ermöglicht anderen Komponenten (wie dem WebServer) Zugriff auf spezifische Module.
|
* Erm\u00f6glicht anderen Komponenten (wie dem WebServer) Zugriff auf spezifische Module.
|
||||||
*/
|
*/
|
||||||
public Module getModule(String name) {
|
public Module getModule(String name) {
|
||||||
return modules.get(name.toLowerCase());
|
return modules.get(name.toLowerCase());
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Ersetzt ein bestehendes Modul durch eine neue Instanz (für Reload).
|
* Ersetzt ein bestehendes Modul durch eine neue Instanz (f\u00fcr Reload).
|
||||||
* Das alte Modul muss bereits deaktiviert worden sein.
|
* Das alte Modul muss bereits deaktiviert worden sein.
|
||||||
*/
|
*/
|
||||||
public void replaceModule(String name, Module newModule) {
|
public void replaceModule(String name, Module newModule) {
|
||||||
|
|||||||
@@ -23,10 +23,10 @@ import java.util.concurrent.atomic.AtomicInteger;
|
|||||||
*
|
*
|
||||||
* Fix #5:
|
* Fix #5:
|
||||||
* - Nachrichten werden bei jedem Zyklus frisch aus der Datei gelesen,
|
* - Nachrichten werden bei jedem Zyklus frisch aus der Datei gelesen,
|
||||||
* damit Änderungen an messages.txt sofort wirken ohne Neustart.
|
* damit \u00c4nderungen an messages.txt sofort wirken ohne Neustart.
|
||||||
* - Neuer Befehl /automessage reload (Permission: statusapi.automessage)
|
* - Neuer Befehl /automessage reload (Permission: statusapi.automessage)
|
||||||
* lädt die Konfiguration neu und setzt den Zähler zurück.
|
* l\u00e4dt die Konfiguration neu und setzt den Z\u00e4hler zur\u00fcck.
|
||||||
* - TextComponent.fromLegacy() → ChatColor.translateAlternateColorCodes für §-Codes.
|
* - TextComponent.fromLegacy() → ChatColor.translateAlternateColorCodes f\u00fcr §-Codes.
|
||||||
*/
|
*/
|
||||||
public class AutoMessageModule implements Module {
|
public class AutoMessageModule implements Module {
|
||||||
|
|
||||||
@@ -34,7 +34,7 @@ public class AutoMessageModule implements Module {
|
|||||||
private StatusAPI api;
|
private StatusAPI api;
|
||||||
private final AtomicInteger currentIndex = new AtomicInteger(0);
|
private final AtomicInteger currentIndex = new AtomicInteger(0);
|
||||||
|
|
||||||
// Konfiguration (für Reload zugänglich)
|
// Konfiguration (f\u00fcr Reload zug\u00e4nglich)
|
||||||
private volatile boolean enabled = false;
|
private volatile boolean enabled = false;
|
||||||
private volatile int intervalSeconds = 300;
|
private volatile int intervalSeconds = 300;
|
||||||
private volatile String fileName = "messages.txt";
|
private volatile String fileName = "messages.txt";
|
||||||
@@ -82,7 +82,7 @@ public class AutoMessageModule implements Module {
|
|||||||
try {
|
try {
|
||||||
Files.write(target.toPath(),
|
Files.write(target.toPath(),
|
||||||
("# AutoMessage – eine Nachricht pro Zeile\n" +
|
("# AutoMessage – eine Nachricht pro Zeile\n" +
|
||||||
"# Farben mit & oder §-Codes, z.B. &aGrüner Text\n" +
|
"# Farben mit & oder §-Codes, z.B. &aGr\u00fcner Text\n" +
|
||||||
"# Kommentarzeilen (# ...) und Leerzeilen werden ignoriert\n").getBytes(StandardCharsets.UTF_8));
|
"# Kommentarzeilen (# ...) und Leerzeilen werden ignoriert\n").getBytes(StandardCharsets.UTF_8));
|
||||||
api.getLogger().info("[AutoMessage] " + fileName + " wurde als leere Vorlage erstellt.");
|
api.getLogger().info("[AutoMessage] " + fileName + " wurde als leere Vorlage erstellt.");
|
||||||
} catch (IOException e) {
|
} catch (IOException e) {
|
||||||
@@ -95,7 +95,7 @@ public class AutoMessageModule implements Module {
|
|||||||
enabled = Boolean.parseBoolean(props.getProperty("automessage.enabled", "false"));
|
enabled = Boolean.parseBoolean(props.getProperty("automessage.enabled", "false"));
|
||||||
String rawInterval = props.getProperty("automessage.interval", "300");
|
String rawInterval = props.getProperty("automessage.interval", "300");
|
||||||
try { intervalSeconds = Integer.parseInt(rawInterval); }
|
try { intervalSeconds = Integer.parseInt(rawInterval); }
|
||||||
catch (NumberFormatException e) { api.getLogger().warning("Ungültiges Intervall für AutoMessage! Nutze Standard (300s)."); intervalSeconds = 300; }
|
catch (NumberFormatException e) { api.getLogger().warning("Ung\u00fcltiges Intervall f\u00fcr AutoMessage! Nutze Standard (300s)."); intervalSeconds = 300; }
|
||||||
fileName = props.getProperty("automessage.file", "messages.txt");
|
fileName = props.getProperty("automessage.file", "messages.txt");
|
||||||
prefix = props.getProperty("automessage.prefix", "");
|
prefix = props.getProperty("automessage.prefix", "");
|
||||||
}
|
}
|
||||||
@@ -129,7 +129,7 @@ public class AutoMessageModule implements Module {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Fix #5: Datei bei jedem Tick neu einlesen → Änderungen wirken sofort
|
// Fix #5: Datei bei jedem Tick neu einlesen → \u00c4nderungen wirken sofort
|
||||||
List<String> messages;
|
List<String> messages;
|
||||||
try {
|
try {
|
||||||
messages = Files.readAllLines(messageFile.toPath(), StandardCharsets.UTF_8);
|
messages = Files.readAllLines(messageFile.toPath(), StandardCharsets.UTF_8);
|
||||||
@@ -146,7 +146,7 @@ public class AutoMessageModule implements Module {
|
|||||||
|
|
||||||
String raw = messages.get(idx);
|
String raw = messages.get(idx);
|
||||||
String prefixPart = prefix.isEmpty() ? "" : ChatColor.translateAlternateColorCodes('&', prefix) + " ";
|
String prefixPart = prefix.isEmpty() ? "" : ChatColor.translateAlternateColorCodes('&', prefix) + " ";
|
||||||
// Fix: §-Codes direkt übersetzen (messages.txt nutzt §-Codes)
|
// Fix: §-Codes direkt \u00fcbersetzen (messages.txt nutzt §-Codes)
|
||||||
String text = prefixPart + ChatColor.translateAlternateColorCodes('&',
|
String text = prefixPart + ChatColor.translateAlternateColorCodes('&',
|
||||||
raw.replace("\u00a7", "&").replace("§", "&"));
|
raw.replace("\u00a7", "&").replace("§", "&"));
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,621 @@
|
|||||||
|
package net.viper.status.modules.afk;
|
||||||
|
|
||||||
|
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.Title;
|
||||||
|
import net.md_5.bungee.api.chat.BaseComponent;
|
||||||
|
import net.md_5.bungee.api.chat.TextComponent;
|
||||||
|
import net.md_5.bungee.api.connection.ProxiedPlayer;
|
||||||
|
import net.md_5.bungee.api.event.ChatEvent;
|
||||||
|
import net.md_5.bungee.api.event.PlayerDisconnectEvent;
|
||||||
|
import net.md_5.bungee.api.plugin.Command;
|
||||||
|
import net.md_5.bungee.api.plugin.Listener;
|
||||||
|
import net.md_5.bungee.api.plugin.Plugin;
|
||||||
|
import net.md_5.bungee.api.scheduler.ScheduledTask;
|
||||||
|
import net.md_5.bungee.event.EventHandler;
|
||||||
|
import net.viper.status.StatusAPI;
|
||||||
|
import net.viper.status.module.Module;
|
||||||
|
|
||||||
|
import java.util.*;
|
||||||
|
import java.util.concurrent.ConcurrentHashMap;
|
||||||
|
import java.util.concurrent.TimeUnit;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* AfkModule – /afk Befehl + automatische AFK-Erkennung nach Inaktivit\u00e4t.
|
||||||
|
*
|
||||||
|
* Unterst\u00fctzt %gradient:FARBE1:FARBE2:...:TEXT% in allen Title-Eintr\u00e4gen,
|
||||||
|
* genau wie Scoreboard und Tablist.
|
||||||
|
*
|
||||||
|
* Config: afk.properties (wird beim ersten Start automatisch erstellt)
|
||||||
|
*
|
||||||
|
* Title-Format: "Title-Zeile|Subtitle-Zeile" (Subtitle optional)
|
||||||
|
* Beispiel mit Gradient:
|
||||||
|
* afk.title.set.1=%gradient:&b:&f:&b:&l [AFK] %|&8Bewege dich um zur\u00fcckzukehren
|
||||||
|
*/
|
||||||
|
public class AfkModule implements Module, Listener {
|
||||||
|
|
||||||
|
private static final String CONFIG_FILE = "afk.properties";
|
||||||
|
|
||||||
|
private Plugin plugin;
|
||||||
|
private ScheduledTask idleCheckTask;
|
||||||
|
private static AfkModule INSTANCE;
|
||||||
|
private java.lang.reflect.Method sendPkt;
|
||||||
|
|
||||||
|
public static final ConcurrentHashMap<UUID, Long> lastActivity = new ConcurrentHashMap<>();
|
||||||
|
|
||||||
|
// Laufender Title-Repeat-Task pro AFK-Spieler
|
||||||
|
private final ConcurrentHashMap<UUID, ScheduledTask> titleTasks = new ConcurrentHashMap<>();
|
||||||
|
|
||||||
|
// Welcher Title-Eintrag diesem Spieler gerade angezeigt wird (bleibt gleich solange AFK)
|
||||||
|
private final ConcurrentHashMap<UUID, String[]> activeTitleEntry = new ConcurrentHashMap<>();
|
||||||
|
|
||||||
|
// Config
|
||||||
|
private boolean enabled = true;
|
||||||
|
private boolean idleEnabled = true;
|
||||||
|
private int idleSeconds = 300;
|
||||||
|
private String bypassPerm = "statusapi.afk.bypass";
|
||||||
|
private int titleFadeIn = 10;
|
||||||
|
private int titleStay = 60;
|
||||||
|
private int titleFadeOut = 20;
|
||||||
|
|
||||||
|
// Titel-Paar: set-Nachricht + passende unset-Nachricht
|
||||||
|
private static class TitlePair {
|
||||||
|
final String[] set; // { title, subtitle }
|
||||||
|
final String[] unset; // { title, subtitle }
|
||||||
|
TitlePair(String[] set, String[] unset) { this.set = set; this.unset = unset; }
|
||||||
|
}
|
||||||
|
|
||||||
|
private final List<TitlePair> titlePairs = new ArrayList<>();
|
||||||
|
|
||||||
|
// Welches Paar diesem Spieler gerade zugewiesen ist (bleibt f\u00fcr die gesamte AFK-Zeit gleich)
|
||||||
|
private final ConcurrentHashMap<UUID, TitlePair> activePair = new ConcurrentHashMap<>();
|
||||||
|
|
||||||
|
private final Random random = new Random();
|
||||||
|
|
||||||
|
@Override public String getName() { return "AfkModule"; }
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void onEnable(Plugin plugin) {
|
||||||
|
this.plugin = plugin;
|
||||||
|
INSTANCE = this;
|
||||||
|
try {
|
||||||
|
Class<?> uc = Class.forName("net.md_5.bungee.UserConnection");
|
||||||
|
sendPkt = uc.getMethod("sendPacketQueued", net.md_5.bungee.protocol.DefinedPacket.class);
|
||||||
|
sendPkt.setAccessible(true);
|
||||||
|
} catch (Exception e) {
|
||||||
|
plugin.getLogger().severe("[AfkModule] sendPacketQueued nicht gefunden: " + e.getMessage());
|
||||||
|
}
|
||||||
|
ensureConfigExists();
|
||||||
|
loadConfig();
|
||||||
|
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);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
if (idleEnabled) {
|
||||||
|
idleCheckTask = ProxyServer.getInstance().getScheduler().schedule(
|
||||||
|
plugin, this::checkIdle, 10L, 10L, TimeUnit.SECONDS);
|
||||||
|
}
|
||||||
|
plugin.getLogger().info("[AfkModule] Aktiviert. idle=" + idleEnabled
|
||||||
|
+ " idleSeconds=" + idleSeconds
|
||||||
|
+ " pairs=" + titlePairs.size());
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void onDisable(Plugin plugin) {
|
||||||
|
if (idleCheckTask != null) { idleCheckTask.cancel(); idleCheckTask = null; }
|
||||||
|
titleTasks.values().forEach(ScheduledTask::cancel);
|
||||||
|
titleTasks.clear();
|
||||||
|
activeTitleEntry.clear();
|
||||||
|
activePair.clear();
|
||||||
|
StatusAPI.playerAfk.clear();
|
||||||
|
lastActivity.clear();
|
||||||
|
INSTANCE = null;
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── Events ───────────────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
@EventHandler
|
||||||
|
public void onChat(ChatEvent e) {
|
||||||
|
if (!(e.getSender() instanceof ProxiedPlayer)) return;
|
||||||
|
ProxiedPlayer p = (ProxiedPlayer) e.getSender();
|
||||||
|
recordActivity(p.getUniqueId());
|
||||||
|
if (!e.getMessage().toLowerCase().startsWith("/afk") && isAfk(p.getUniqueId()))
|
||||||
|
setAfk(p, false);
|
||||||
|
}
|
||||||
|
|
||||||
|
@EventHandler
|
||||||
|
public void onDisconnect(PlayerDisconnectEvent e) {
|
||||||
|
UUID id = e.getPlayer().getUniqueId();
|
||||||
|
StatusAPI.playerAfk.remove(id);
|
||||||
|
lastActivity.remove(id);
|
||||||
|
stopTitleTask(id);
|
||||||
|
activePair.remove(id);
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── Public API ───────────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
public static void setBridgeAfk(UUID uuid, boolean afk) {
|
||||||
|
if (afk) {
|
||||||
|
StatusAPI.playerAfk.put(uuid, true);
|
||||||
|
} else {
|
||||||
|
StatusAPI.playerAfk.remove(uuid);
|
||||||
|
lastActivity.put(uuid, System.currentTimeMillis());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Wird von StatusAPI aufgerufen wenn die Bridge eine Koordinaten\u00e4nderung meldet.
|
||||||
|
* Hebt AFK sofort auf und zeigt den Unset-Title an.
|
||||||
|
*/
|
||||||
|
public static void unAfkByMovement(UUID uuid) {
|
||||||
|
AfkModule inst = INSTANCE;
|
||||||
|
if (inst == null) return;
|
||||||
|
if (!Boolean.TRUE.equals(StatusAPI.playerAfk.get(uuid))) return;
|
||||||
|
ProxiedPlayer p = ProxyServer.getInstance().getPlayer(uuid);
|
||||||
|
if (p == null) return;
|
||||||
|
inst.setAfk(p, false);
|
||||||
|
}
|
||||||
|
|
||||||
|
public static void recordActivity(UUID uuid) {
|
||||||
|
lastActivity.put(uuid, System.currentTimeMillis());
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── Interne Logik ────────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
private boolean isAfk(UUID uuid) {
|
||||||
|
return Boolean.TRUE.equals(StatusAPI.playerAfk.get(uuid));
|
||||||
|
}
|
||||||
|
|
||||||
|
private void toggleAfk(ProxiedPlayer p) {
|
||||||
|
setAfk(p, !isAfk(p.getUniqueId()));
|
||||||
|
}
|
||||||
|
|
||||||
|
private void setAfk(ProxiedPlayer p, boolean afk) {
|
||||||
|
UUID id = p.getUniqueId();
|
||||||
|
if (isAfk(id) == afk) return;
|
||||||
|
if (afk) {
|
||||||
|
StatusAPI.playerAfk.put(id, true);
|
||||||
|
startTitleTask(p);
|
||||||
|
} else {
|
||||||
|
StatusAPI.playerAfk.remove(id);
|
||||||
|
lastActivity.put(id, System.currentTimeMillis());
|
||||||
|
TitlePair pair = activePair.get(id);
|
||||||
|
stopTitleTask(id);
|
||||||
|
clearTitle(p);
|
||||||
|
if (pair != null) sendTitleEntry(p, pair.unset);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
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);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* W\u00e4hlt einen zuf\u00e4lligen AFK-Title und wiederholt ihn dauerhaft,
|
||||||
|
* solange der Spieler AFK ist – kein Ausblenden m\u00f6glich.
|
||||||
|
*/
|
||||||
|
private void startTitleTask(ProxiedPlayer p) {
|
||||||
|
if (titlePairs.isEmpty()) return;
|
||||||
|
UUID id = p.getUniqueId();
|
||||||
|
stopTitleTask(id);
|
||||||
|
|
||||||
|
// Zuf\u00e4lliges Paar w\u00e4hlen – bleibt f\u00fcr die gesamte AFK-Zeit gleich
|
||||||
|
TitlePair pair = titlePairs.get(random.nextInt(titlePairs.size()));
|
||||||
|
activePair.put(id, pair);
|
||||||
|
activeTitleEntry.put(id, pair.set);
|
||||||
|
sendTitleEntry(p, pair.set);
|
||||||
|
|
||||||
|
long intervalMs = Math.max(500, (titleStay - 20) * 50L);
|
||||||
|
ScheduledTask task = ProxyServer.getInstance().getScheduler().schedule(plugin, () -> {
|
||||||
|
ProxiedPlayer online = ProxyServer.getInstance().getPlayer(id);
|
||||||
|
if (online == null || !online.isConnected() || !isAfk(id)) {
|
||||||
|
stopTitleTask(id);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
String[] current = activeTitleEntry.get(id);
|
||||||
|
if (current != null) sendTitleEntry(online, current);
|
||||||
|
}, intervalMs, intervalMs, TimeUnit.MILLISECONDS);
|
||||||
|
titleTasks.put(id, task);
|
||||||
|
}
|
||||||
|
|
||||||
|
private void stopTitleTask(UUID id) {
|
||||||
|
ScheduledTask old = titleTasks.remove(id);
|
||||||
|
if (old != null) old.cancel();
|
||||||
|
activeTitleEntry.remove(id);
|
||||||
|
// activePair bleibt bis setAfk(false) es ausliest, danach:
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Entfernt den Title sofort vom Bildschirm. */
|
||||||
|
/** Entfernt den Title sofort vom Bildschirm via ClearTitles-Packet. */
|
||||||
|
private void clearTitle(ProxiedPlayer p) {
|
||||||
|
try {
|
||||||
|
net.md_5.bungee.protocol.packet.ClearTitles clear = new net.md_5.bungee.protocol.packet.ClearTitles();
|
||||||
|
if (sendPkt != null) sendPkt.invoke(p, clear);
|
||||||
|
} catch (Exception ignored) {}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 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])) : "";
|
||||||
|
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);
|
||||||
|
|
||||||
|
// Title-Packet (Action = TITLE, ordinal 0)
|
||||||
|
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)));
|
||||||
|
sendPkt.invoke(p, titlePkt);
|
||||||
|
|
||||||
|
// Subtitle-Packet
|
||||||
|
net.md_5.bungee.protocol.packet.Subtitle subPkt = new net.md_5.bungee.protocol.packet.Subtitle();
|
||||||
|
subPkt.setText(mergeComponents(buildComponents(subtitleRaw)));
|
||||||
|
sendPkt.invoke(p, subPkt);
|
||||||
|
|
||||||
|
} 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);
|
||||||
|
p.sendTitle(title);
|
||||||
|
} catch (Exception ignored) {}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 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("");
|
||||||
|
for (BaseComponent bc : parts) root.addExtra(bc);
|
||||||
|
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.
|
||||||
|
// Farben: #RRGGBB, &#RRGGBB oder &b, &f, &c usw.
|
||||||
|
// Formatcodes (&l, &o, &n, &m) im Text bleiben erhalten.
|
||||||
|
|
||||||
|
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); // Farbe f\u00fcr f\u00fchrende Leerzeichen
|
||||||
|
for (char ch : plain.toCharArray()) {
|
||||||
|
if (ch == ' ') {
|
||||||
|
// Leerzeichen mit der letzten aktiven Gradient-Farbe einf\u00e4rben
|
||||||
|
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++;
|
||||||
|
}
|
||||||
|
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;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── BaseComponent-Builder ─────────────────────────────────────────────────
|
||||||
|
// Wandelt einen vorverarbeiteten String (§x§R§R§G§G§B§B + §-Codes) in
|
||||||
|
// ein BaseComponent[]-Array um – identisch zu ScoreboardModule.buildComponents().
|
||||||
|
|
||||||
|
private static BaseComponent[] buildComponents(String text) {
|
||||||
|
if (text == null || text.isEmpty())
|
||||||
|
return new BaseComponent[]{ new TextComponent("") };
|
||||||
|
|
||||||
|
List<BaseComponent> parts = new ArrayList<>();
|
||||||
|
net.md_5.bungee.api.ChatColor currentColor = net.md_5.bungee.api.ChatColor.WHITE;
|
||||||
|
boolean bold = false, italic = false, underline = false, strike = false, magic = false;
|
||||||
|
int i = 0;
|
||||||
|
StringBuilder buf = new StringBuilder();
|
||||||
|
|
||||||
|
while (i < text.length()) {
|
||||||
|
char c = text.charAt(i);
|
||||||
|
// §x§R§R§G§G§B§B – RGB Hex (14 Zeichen)
|
||||||
|
if (c == '§' && i + 13 < text.length() && text.charAt(i+1) == 'x') {
|
||||||
|
if (buf.length() > 0) {
|
||||||
|
parts.add(makeComp(buf.toString(), currentColor, bold, italic, underline, strike, magic));
|
||||||
|
buf.setLength(0);
|
||||||
|
}
|
||||||
|
try {
|
||||||
|
String hex = "" + text.charAt(i+3) + text.charAt(i+5)
|
||||||
|
+ text.charAt(i+7) + text.charAt(i+9)
|
||||||
|
+ text.charAt(i+11) + text.charAt(i+13);
|
||||||
|
currentColor = net.md_5.bungee.api.ChatColor.of("#" + hex);
|
||||||
|
} catch (Exception ignored) {}
|
||||||
|
i += 14;
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
// §X – Farb-/Formatcodes
|
||||||
|
if (c == '§' && i + 1 < text.length()) {
|
||||||
|
char code = Character.toLowerCase(text.charAt(i+1));
|
||||||
|
if (buf.length() > 0) {
|
||||||
|
parts.add(makeComp(buf.toString(), currentColor, bold, italic, underline, strike, magic));
|
||||||
|
buf.setLength(0);
|
||||||
|
}
|
||||||
|
switch (code) {
|
||||||
|
case 'r': currentColor = net.md_5.bungee.api.ChatColor.WHITE;
|
||||||
|
bold=false; italic=false; underline=false; strike=false; magic=false; break;
|
||||||
|
case 'l': bold=true; break;
|
||||||
|
case 'o': italic=true; break;
|
||||||
|
case 'n': underline=true; break;
|
||||||
|
case 'm': strike=true; break;
|
||||||
|
case 'k': magic=true; break;
|
||||||
|
default:
|
||||||
|
net.md_5.bungee.api.ChatColor col = net.md_5.bungee.api.ChatColor.getByChar(code);
|
||||||
|
if (col != null) { currentColor=col; bold=false; italic=false; underline=false; strike=false; magic=false; }
|
||||||
|
}
|
||||||
|
i += 2;
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
buf.append(c);
|
||||||
|
i++;
|
||||||
|
}
|
||||||
|
if (buf.length() > 0)
|
||||||
|
parts.add(makeComp(buf.toString(), currentColor, bold, italic, underline, strike, magic));
|
||||||
|
if (parts.isEmpty())
|
||||||
|
return new BaseComponent[]{ new TextComponent("") };
|
||||||
|
return parts.toArray(new BaseComponent[0]);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static BaseComponent makeComp(String text,
|
||||||
|
net.md_5.bungee.api.ChatColor color,
|
||||||
|
boolean bold, boolean italic, boolean underline, boolean strike, boolean magic) {
|
||||||
|
TextComponent tc = new TextComponent(text);
|
||||||
|
tc.setColor(color);
|
||||||
|
tc.setBold(bold);
|
||||||
|
tc.setItalic(italic);
|
||||||
|
tc.setUnderlined(underline);
|
||||||
|
tc.setStrikethrough(strike);
|
||||||
|
tc.setObfuscated(magic);
|
||||||
|
return tc;
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── Config ───────────────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
private void loadConfig() {
|
||||||
|
java.io.File file = new java.io.File(plugin.getDataFolder(), CONFIG_FILE);
|
||||||
|
Map<String, String> map = new LinkedHashMap<>();
|
||||||
|
if (file.exists()) {
|
||||||
|
try (java.io.BufferedReader br = new java.io.BufferedReader(
|
||||||
|
new java.io.InputStreamReader(
|
||||||
|
new java.io.FileInputStream(file),
|
||||||
|
java.nio.charset.StandardCharsets.UTF_8))) {
|
||||||
|
String line;
|
||||||
|
while ((line = br.readLine()) != null) {
|
||||||
|
line = line.trim();
|
||||||
|
if (line.isEmpty() || line.startsWith("#")) continue;
|
||||||
|
int eq = line.indexOf('=');
|
||||||
|
if (eq < 1) continue;
|
||||||
|
map.put(line.substring(0, eq).trim(), line.substring(eq + 1));
|
||||||
|
}
|
||||||
|
} catch (Exception e) {
|
||||||
|
plugin.getLogger().warning("[AfkModule] Ladefehler: " + e.getMessage());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
enabled = Boolean.parseBoolean(map.getOrDefault("afk.enabled", "true"));
|
||||||
|
idleEnabled = Boolean.parseBoolean(map.getOrDefault("afk.idle_enabled", "true"));
|
||||||
|
idleSeconds = parseInt(map.getOrDefault("afk.idle_seconds", "300"), 300);
|
||||||
|
bypassPerm = map.getOrDefault("afk.permission.bypass", "statusapi.afk.bypass");
|
||||||
|
titleFadeIn = parseInt(map.getOrDefault("afk.title.fade_in", "10"), 10);
|
||||||
|
titleStay = parseInt(map.getOrDefault("afk.title.stay", "100"), 100);
|
||||||
|
titleFadeOut = parseInt(map.getOrDefault("afk.title.fade_out", "10"), 10);
|
||||||
|
|
||||||
|
titlePairs.clear();
|
||||||
|
for (int i = 1; i <= 20; i++) {
|
||||||
|
String raw = map.get("afk.title.pair." + i);
|
||||||
|
if (raw == null || raw.isEmpty()) continue;
|
||||||
|
// Format: setTitle|setSubtitle||unsetTitle|unsetSubtitle
|
||||||
|
int sep = raw.indexOf("||");
|
||||||
|
if (sep < 0) continue;
|
||||||
|
String[] set = splitTitleLine(raw.substring(0, sep));
|
||||||
|
String[] unset = splitTitleLine(raw.substring(sep + 2));
|
||||||
|
titlePairs.add(new TitlePair(set, unset));
|
||||||
|
}
|
||||||
|
if (titlePairs.isEmpty()) {
|
||||||
|
titlePairs.add(new TitlePair(
|
||||||
|
new String[]{"%gradient:&b:&f:&b:&l [AFK] %", "&8Bewege dich um zur\u00fcckzukehren"},
|
||||||
|
new String[]{"&aWillkommen zur\u00fcck!", ""}
|
||||||
|
));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private String[] splitTitleLine(String raw) {
|
||||||
|
int pipe = raw.indexOf('|');
|
||||||
|
if (pipe < 0) return new String[]{ raw, "" };
|
||||||
|
return new String[]{ raw.substring(0, pipe), raw.substring(pipe + 1) };
|
||||||
|
}
|
||||||
|
|
||||||
|
private void ensureConfigExists() {
|
||||||
|
java.io.File file = new java.io.File(plugin.getDataFolder(), CONFIG_FILE);
|
||||||
|
if (file.exists()) return;
|
||||||
|
try (java.io.OutputStreamWriter w = new java.io.OutputStreamWriter(
|
||||||
|
new java.io.FileOutputStream(file), java.nio.charset.StandardCharsets.UTF_8)) {
|
||||||
|
w.write(
|
||||||
|
"# =====================================================\n" +
|
||||||
|
"# AfkModule – /afk Befehl & automatische AFK-Erkennung\n" +
|
||||||
|
"# =====================================================\n\n" +
|
||||||
|
"afk.enabled=true\n\n" +
|
||||||
|
"# Automatisch AFK setzen nach X Sekunden ohne Aktivitaet\n" +
|
||||||
|
"afk.idle_enabled=true\n" +
|
||||||
|
"afk.idle_seconds=300\n\n" +
|
||||||
|
"# Berechtigung zum Umgehen des auto-AFK\n" +
|
||||||
|
"afk.permission.bypass=statusapi.afk.bypass\n\n" +
|
||||||
|
"# ── Title-Anzeigezeiten (in Ticks, 20 Ticks = 1 Sekunde) ──\n" +
|
||||||
|
"afk.title.fade_in=10\n" +
|
||||||
|
"afk.title.stay=100\n" +
|
||||||
|
"afk.title.fade_out=10\n\n" +
|
||||||
|
"# ── Nachrichten-Paare ─────────────────────────────────────\n" +
|
||||||
|
"# Format: setTitle|setSubtitle||unsetTitle|unsetSubtitle\n" +
|
||||||
|
"# | trennt Title und Subtitle innerhalb einer Seite\n" +
|
||||||
|
"# || trennt AFK-Nachricht (links) von R\u00fcckkehr-Nachricht (rechts)\n" +
|
||||||
|
"# Gradient: %gradient:FARBE1:FARBE2:...:TEXT%\n" +
|
||||||
|
"# Farben: &b, &f, &a usw. oder #RRGGBB\n" +
|
||||||
|
"# Es wird zufaellig ein Paar gewaehlt (max. 20 Paare).\n" +
|
||||||
|
"# Die R\u00fcckkehr-Nachricht passt thematisch zur AFK-Nachricht.\n\n" +
|
||||||
|
"afk.title.pair.1=%gradient:&b:&f:&b:&l [AFK] %|&8Wahrscheinlich auf dem Klo...||%gradient:&a:&f:&a:&l Willkommen zur\u00fcck! %|&7War das Klo sauber?\n" +
|
||||||
|
"afk.title.pair.2=%gradient:&b:&f:&b:&l [AFK] %|&8Hat den Stecker gezogen||%gradient:&a:&f:&a:&l Wieder eingesteckt! %|&7Der Strom ist zur\u00fcck\n" +
|
||||||
|
"afk.title.pair.3=%gradient:&b:&f:&b:&l [AFK] %|&8Schaut seit 10min die Decke an||%gradient:&a:&f:&a:&l Na endlich! %|&7Die Decke hat dich freigegeben\n" +
|
||||||
|
"afk.title.pair.4=%gradient:&b:&f:&b:&l [AFK] %|&8Vom K\u00fchlschrank verschluckt worden||%gradient:&a:&f:&a:&l Er lebt! %|&7Der Snack war es wert, oder?\n" +
|
||||||
|
"afk.title.pair.5=%gradient:&b:&f:&b:&l [AFK] %|&8Loading... Spieler nicht gefunden||%gradient:&a:&f:&a:&l Error behoben! %|&7Spieler erfolgreich neugestartet\n" +
|
||||||
|
"afk.title.pair.6=%gradient:&b:&f:&b:&l [AFK] %|&8Vermutlich beim Snackholen||%gradient:&a:&f:&a:&l Snack erfolgreich geholt! %|&7Weiter gehts!\n" +
|
||||||
|
"afk.title.pair.7=%gradient:&b:&f:&b:&l [AFK] %|&8Eingeschlafen. Bitte nicht wecken.||%gradient:&a:&f:&a:&l Aufgewacht! %|&7Der Wecker hat funktioniert\n" +
|
||||||
|
"afk.title.pair.8=%gradient:&b:&f:&b:&l [AFK] %|&8Hat die Realit\u00e4t betreten||%gradient:&a:&f:&a:&l Zur\u00fcck aus der Realit\u00e4t! %|&7Und? War's schlimm?\n" +
|
||||||
|
"afk.title.pair.9=%gradient:&b:&f:&b:&l [AFK] %|&8Gehirn: AFK. K\u00f6rper: noch da.||%gradient:&a:&f:&a:&l Gehirn wieder eingeschaltet! %|&7Willkommen zur\u00fcck\n" +
|
||||||
|
"afk.title.pair.10=%gradient:&b:&f:&b:&l [AFK] %|&8Spricht mit echten Menschen. Seltsam.||%gradient:&a:&f:&a:&l Zur\u00fcck zu den Pixeln! %|&7Echte Menschen sind \u00fcbersch\u00e4tzt\n" +
|
||||||
|
"afk.title.pair.11=%gradient:&b:&f:&b:&l [AFK] %|&8Sucht den Einschalter f\u00fcrs Leben||%gradient:&a:&f:&a:&l Einschalter gefunden! %|&7Das Leben l\u00e4uft wieder\n" +
|
||||||
|
"afk.title.pair.12=%gradient:&b:&f:&b:&l [AFK] %|&8Mom hat gerufen. RIP.||%gradient:&a:&f:&a:&l Mom ist wieder weg. %|&7Puh. Knapp entkommen.\n" +
|
||||||
|
"afk.title.pair.13=%gradient:&b:&f:&b:&l [AFK] %|&8Error 404: Spieler nicht gefunden||%gradient:&a:&f:&a:&l Spieler wieder online! %|&7404 behoben\n" +
|
||||||
|
"afk.title.pair.14=%gradient:&b:&f:&b:&l [AFK] %|&8Tut so als w\u00e4re er besch\u00e4ftigt||%gradient:&a:&f:&a:&l Aufgeflogen! %|&7War eh nicht \u00fcberzeugend\n" +
|
||||||
|
"afk.title.pair.15=%gradient:&b:&f:&b:&l [AFK] %|&8Kaffeepause. Die wichtigste Pause.||%gradient:&a:&f:&a:&l Koffein erfolgreich zugef\u00fchrt! %|&7Jetzt wieder einsatzbereit\n"
|
||||||
|
);
|
||||||
|
} catch (Exception e) {
|
||||||
|
plugin.getLogger().warning("[AfkModule] Konnte Config nicht erstellen: " + e.getMessage());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── Hilfsmethoden ────────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
private static TextComponent plain(String text) {
|
||||||
|
return new TextComponent(ChatColor.translateAlternateColorCodes('&', text));
|
||||||
|
}
|
||||||
|
|
||||||
|
private static int parseInt(String s, int def) {
|
||||||
|
try { return Integer.parseInt(s.trim()); } catch (Exception e) { return def; }
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -46,7 +46,7 @@ import java.util.concurrent.atomic.AtomicInteger;
|
|||||||
import java.util.concurrent.atomic.AtomicLong;
|
import java.util.concurrent.atomic.AtomicLong;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Eigenständiger AntiBot/Attack-Guard.
|
* Eigenst\u00e4ndiger AntiBot/Attack-Guard.
|
||||||
*
|
*
|
||||||
* Fixes:
|
* Fixes:
|
||||||
* - cleanupExpired() nutzt jetzt removeIf() statt Iteration + remove() (Bug #3)
|
* - cleanupExpired() nutzt jetzt removeIf() statt Iteration + remove() (Bug #3)
|
||||||
@@ -330,7 +330,7 @@ public class AntiBotModule implements Module, Listener {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Sperrt eine IP für die konfigurierte Block-Dauer (antibot.ip.block_seconds).
|
* Sperrt eine IP f\u00fcr die konfigurierte Block-Dauer (antibot.ip.block_seconds).
|
||||||
* Kann von anderen Modulen aufgerufen werden (z. B. MultiAccountGuard).
|
* Kann von anderen Modulen aufgerufen werden (z. B. MultiAccountGuard).
|
||||||
* @param ip Die zu sperrende IP-Adresse
|
* @param ip Die zu sperrende IP-Adresse
|
||||||
* @param durationSeconds Sperrdauer in Sekunden (0 = antibot-Standard verwenden)
|
* @param durationSeconds Sperrdauer in Sekunden (0 = antibot-Standard verwenden)
|
||||||
|
|||||||
@@ -31,7 +31,7 @@ import java.util.concurrent.TimeUnit;
|
|||||||
* Fixes:
|
* Fixes:
|
||||||
* - loadSchedules(): ID-Split nutzt jetzt indexOf/lastIndexOf statt split("\\.") mit length==2-Check.
|
* - loadSchedules(): ID-Split nutzt jetzt indexOf/lastIndexOf statt split("\\.") mit length==2-Check.
|
||||||
* Damit werden auch clientScheduleIds die Punkte enthalten korrekt geladen. (Bug #2)
|
* Damit werden auch clientScheduleIds die Punkte enthalten korrekt geladen. (Bug #2)
|
||||||
* - handleBroadcast(): &-Farbcodes werden jetzt auch in der Nachricht selbst übersetzt. (Bug #3)
|
* - handleBroadcast(): &-Farbcodes werden jetzt auch in der Nachricht selbst \u00fcbersetzt. (Bug #3)
|
||||||
* - handleBroadcast(): Literal \n in der Nachricht wird als echter Zeilenumbruch gerendert. (Bug #4)
|
* - handleBroadcast(): Literal \n in der Nachricht wird als echter Zeilenumbruch gerendert. (Bug #4)
|
||||||
* - handleBroadcast(): URLs (http/https) werden als anklickbare TextComponents eingebettet. (Bug #5)
|
* - handleBroadcast(): URLs (http/https) werden als anklickbare TextComponents eingebettet. (Bug #5)
|
||||||
*/
|
*/
|
||||||
@@ -129,7 +129,7 @@ public class BroadcastModule implements Module, Listener {
|
|||||||
finalPrefix = prefixColorCode + usedPrefix + ChatColor.RESET;
|
finalPrefix = prefixColorCode + usedPrefix + ChatColor.RESET;
|
||||||
}
|
}
|
||||||
|
|
||||||
// FIX #1: &-Farbcodes auch in der Nachricht selbst übersetzen
|
// FIX #1: &-Farbcodes auch in der Nachricht selbst \u00fcbersetzen
|
||||||
String translatedMessage = ChatColor.translateAlternateColorCodes('&', message);
|
String translatedMessage = ChatColor.translateAlternateColorCodes('&', message);
|
||||||
String coloredMessage = (messageColorCode.isEmpty() ? "" : messageColorCode) + translatedMessage;
|
String coloredMessage = (messageColorCode.isEmpty() ? "" : messageColorCode) + translatedMessage;
|
||||||
|
|
||||||
@@ -150,17 +150,17 @@ public class BroadcastModule implements Module, Listener {
|
|||||||
for (ProxiedPlayer p : plugin.getProxy().getPlayers()) {
|
for (ProxiedPlayer p : plugin.getProxy().getPlayers()) {
|
||||||
try { p.sendMessage(components); sent++; } catch (Throwable ignored) {}
|
try { p.sendMessage(components); sent++; } catch (Throwable ignored) {}
|
||||||
}
|
}
|
||||||
StatusAPI.debugLog(plugin, "[BroadcastModule] Broadcast gesendet (Empfänger=" + sent + "): " + message);
|
StatusAPI.debugLog(plugin, "[BroadcastModule] Broadcast gesendet (Empf\u00e4nger=" + sent + "): " + message);
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Baut ein BaseComponent-Array aus einem formatierten String.
|
* Baut ein BaseComponent-Array aus einem formatierten String.
|
||||||
* URLs (http/https) werden als anklickbare TextComponents eingebettet.
|
* URLs (http/https) werden als anklickbare TextComponents eingebettet.
|
||||||
* Unterstützt auch echte Newlines (\n) als Zeilenumbruch.
|
* Unterst\u00fctzt auch echte Newlines (\n) als Zeilenumbruch.
|
||||||
*/
|
*/
|
||||||
private BaseComponent[] buildClickableComponents(String text) {
|
private BaseComponent[] buildClickableComponents(String text) {
|
||||||
// Regex für URLs
|
// Regex f\u00fcr URLs
|
||||||
java.util.regex.Pattern urlPattern = java.util.regex.Pattern.compile(
|
java.util.regex.Pattern urlPattern = java.util.regex.Pattern.compile(
|
||||||
"(https?://[^\\s\\n]+)", java.util.regex.Pattern.CASE_INSENSITIVE);
|
"(https?://[^\\s\\n]+)", java.util.regex.Pattern.CASE_INSENSITIVE);
|
||||||
|
|
||||||
|
|||||||
@@ -6,12 +6,12 @@ import java.util.concurrent.ConcurrentHashMap;
|
|||||||
import java.util.logging.Logger;
|
import java.util.logging.Logger;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Verwaltet die Verknüpfung von Minecraft-Accounts mit Discord/Telegram.
|
* Verwaltet die Verkn\u00fcpfung von Minecraft-Accounts mit Discord/Telegram.
|
||||||
*
|
*
|
||||||
* Ablauf:
|
* Ablauf:
|
||||||
* 1. Spieler tippt /linkdiscord oder /linktelegram → Token wird generiert
|
* 1. Spieler tippt /linkdiscord oder /linktelegram → Token wird generiert
|
||||||
* 2. Spieler schickt Token an den Bot
|
* 2. Spieler schickt Token an den Bot
|
||||||
* 3. Bot-Polling erkennt Token → Verknüpfung wird gespeichert
|
* 3. Bot-Polling erkennt Token → Verkn\u00fcpfung wird gespeichert
|
||||||
*
|
*
|
||||||
* Speicherformat (chat_links.dat):
|
* Speicherformat (chat_links.dat):
|
||||||
* minecraft:<uuid>|name:<spielername>|discord:<discord-user-id>|telegram:<telegram-user-id>
|
* minecraft:<uuid>|name:<spielername>|discord:<discord-user-id>|telegram:<telegram-user-id>
|
||||||
@@ -21,10 +21,10 @@ public class AccountLinkManager {
|
|||||||
private final File file;
|
private final File file;
|
||||||
private final Logger logger;
|
private final Logger logger;
|
||||||
|
|
||||||
// UUID → verknüpfte Accounts
|
// UUID → verkn\u00fcpfte Accounts
|
||||||
private final ConcurrentHashMap<UUID, LinkedAccount> links = new ConcurrentHashMap<>();
|
private final ConcurrentHashMap<UUID, LinkedAccount> links = new ConcurrentHashMap<>();
|
||||||
|
|
||||||
// Ausstehende Token: token → UUID (läuft nach 10 Min ab)
|
// Ausstehende Token: token → UUID (l\u00e4uft nach 10 Min ab)
|
||||||
private final ConcurrentHashMap<String, PendingToken> pendingTokens = new ConcurrentHashMap<>();
|
private final ConcurrentHashMap<String, PendingToken> pendingTokens = new ConcurrentHashMap<>();
|
||||||
|
|
||||||
public AccountLinkManager(File dataFolder, Logger logger) {
|
public AccountLinkManager(File dataFolder, Logger logger) {
|
||||||
@@ -37,10 +37,10 @@ public class AccountLinkManager {
|
|||||||
public static class LinkedAccount {
|
public static class LinkedAccount {
|
||||||
public UUID minecraftUUID;
|
public UUID minecraftUUID;
|
||||||
public String minecraftName;
|
public String minecraftName;
|
||||||
public String discordUserId = ""; // leer = nicht verknüpft
|
public String discordUserId = ""; // leer = nicht verkn\u00fcpft
|
||||||
public String telegramUserId = ""; // leer = nicht verknüpft
|
public String telegramUserId = ""; // leer = nicht verkn\u00fcpft
|
||||||
public String telegramUsername = ""; // @username für Anzeige
|
public String telegramUsername = ""; // @username f\u00fcr Anzeige
|
||||||
public String discordUsername = ""; // für Anzeige
|
public String discordUsername = ""; // f\u00fcr Anzeige
|
||||||
}
|
}
|
||||||
|
|
||||||
private static class PendingToken {
|
private static class PendingToken {
|
||||||
@@ -64,8 +64,8 @@ public class AccountLinkManager {
|
|||||||
// ===== Token-Generierung =====
|
// ===== Token-Generierung =====
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Generiert einen neuen Verknüpfungs-Token für einen Spieler.
|
* Generiert einen neuen Verkn\u00fcpfungs-Token f\u00fcr einen Spieler.
|
||||||
* Bestehende Token für denselben Spieler+Typ werden überschrieben.
|
* Bestehende Token f\u00fcr denselben Spieler+Typ werden \u00fcberschrieben.
|
||||||
*
|
*
|
||||||
* @param uuid UUID des Spielers
|
* @param uuid UUID des Spielers
|
||||||
* @param playerName Anzeigename
|
* @param playerName Anzeigename
|
||||||
@@ -73,7 +73,7 @@ public class AccountLinkManager {
|
|||||||
* @return 6-stelliger alphanumerischer Token (z.B. "A3F9K2")
|
* @return 6-stelliger alphanumerischer Token (z.B. "A3F9K2")
|
||||||
*/
|
*/
|
||||||
public String generateToken(UUID uuid, String playerName, String type) {
|
public String generateToken(UUID uuid, String playerName, String type) {
|
||||||
// Alte Token für diesen Spieler+Typ entfernen
|
// Alte Token f\u00fcr diesen Spieler+Typ entfernen
|
||||||
pendingTokens.entrySet().removeIf(e ->
|
pendingTokens.entrySet().removeIf(e ->
|
||||||
e.getValue().uuid.equals(uuid) && e.getValue().type.equals(type));
|
e.getValue().uuid.equals(uuid) && e.getValue().type.equals(type));
|
||||||
|
|
||||||
@@ -97,15 +97,15 @@ public class AccountLinkManager {
|
|||||||
return sb.toString();
|
return sb.toString();
|
||||||
}
|
}
|
||||||
|
|
||||||
// ===== Token einlösen =====
|
// ===== Token einl\u00f6sen =====
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Versucht einen Token einzulösen (aufgerufen wenn Bot eine Nachricht empfängt).
|
* Versucht einen Token einzul\u00f6sen (aufgerufen wenn Bot eine Nachricht empf\u00e4ngt).
|
||||||
*
|
*
|
||||||
* @param token Der eingesendete Token
|
* @param token Der eingesendete Token
|
||||||
* @param externalId Discord User-ID oder Telegram User-ID (als String)
|
* @param externalId Discord User-ID oder Telegram User-ID (als String)
|
||||||
* @param externalName Discord-Username oder Telegram-@username
|
* @param externalName Discord-Username oder Telegram-@username
|
||||||
* @return LinkedAccount wenn erfolgreich, null wenn Token ungültig/abgelaufen
|
* @return LinkedAccount wenn erfolgreich, null wenn Token ung\u00fcltig/abgelaufen
|
||||||
*/
|
*/
|
||||||
public LinkedAccount redeemToken(String token, String externalId, String externalName, String type) {
|
public LinkedAccount redeemToken(String token, String externalId, String externalName, String type) {
|
||||||
token = token.trim().toUpperCase();
|
token = token.trim().toUpperCase();
|
||||||
@@ -115,7 +115,7 @@ public class AccountLinkManager {
|
|||||||
pendingTokens.remove(token);
|
pendingTokens.remove(token);
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
// Typ muss übereinstimmen
|
// Typ muss \u00fcbereinstimmen
|
||||||
if (!pending.type.equals(type)) return null;
|
if (!pending.type.equals(type)) return null;
|
||||||
|
|
||||||
pendingTokens.remove(token);
|
pendingTokens.remove(token);
|
||||||
@@ -161,19 +161,19 @@ public class AccountLinkManager {
|
|||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
/** Gibt den Minecraft-Namen für eine Discord-User-ID zurück, oder null. */
|
/** Gibt den Minecraft-Namen f\u00fcr eine Discord-User-ID zur\u00fcck, oder null. */
|
||||||
public String getMinecraftNameByDiscordId(String discordUserId) {
|
public String getMinecraftNameByDiscordId(String discordUserId) {
|
||||||
LinkedAccount a = getByDiscordId(discordUserId);
|
LinkedAccount a = getByDiscordId(discordUserId);
|
||||||
return a != null ? a.minecraftName : null;
|
return a != null ? a.minecraftName : null;
|
||||||
}
|
}
|
||||||
|
|
||||||
/** Gibt den Minecraft-Namen für eine Telegram-User-ID zurück, oder null. */
|
/** Gibt den Minecraft-Namen f\u00fcr eine Telegram-User-ID zur\u00fcck, oder null. */
|
||||||
public String getMinecraftNameByTelegramId(String telegramUserId) {
|
public String getMinecraftNameByTelegramId(String telegramUserId) {
|
||||||
LinkedAccount a = getByTelegramId(telegramUserId);
|
LinkedAccount a = getByTelegramId(telegramUserId);
|
||||||
return a != null ? a.minecraftName : null;
|
return a != null ? a.minecraftName : null;
|
||||||
}
|
}
|
||||||
|
|
||||||
/** Prüft ob ein Token gerade aussteht (für Tab-Complete etc.). */
|
/** Pr\u00fcft ob ein Token gerade aussteht (f\u00fcr Tab-Complete etc.). */
|
||||||
public boolean hasPendingToken(UUID uuid, String type) {
|
public boolean hasPendingToken(UUID uuid, String type) {
|
||||||
for (PendingToken t : pendingTokens.values()) {
|
for (PendingToken t : pendingTokens.values()) {
|
||||||
if (t.uuid.equals(uuid) && t.type.equals(type) && !t.isExpired()) return true;
|
if (t.uuid.equals(uuid) && t.type.equals(type) && !t.isExpired()) return true;
|
||||||
@@ -181,7 +181,7 @@ public class AccountLinkManager {
|
|||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
// ===== Verknüpfung aufheben =====
|
// ===== Verkn\u00fcpfung aufheben =====
|
||||||
|
|
||||||
public boolean unlinkDiscord(UUID uuid) {
|
public boolean unlinkDiscord(UUID uuid) {
|
||||||
LinkedAccount a = links.get(uuid);
|
LinkedAccount a = links.get(uuid);
|
||||||
@@ -210,27 +210,27 @@ public class AccountLinkManager {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// ===== Convenience-Methoden für Bridges =====
|
// ===== Convenience-Methoden f\u00fcr Bridges =====
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Löst einen Telegram-Token ein.
|
* L\u00f6st einen Telegram-Token ein.
|
||||||
* Wrapper für redeemToken mit type="telegram".
|
* Wrapper f\u00fcr redeemToken mit type="telegram".
|
||||||
*/
|
*/
|
||||||
public LinkedAccount redeemTelegram(String token, String telegramUserId, String telegramUsername) {
|
public LinkedAccount redeemTelegram(String token, String telegramUserId, String telegramUsername) {
|
||||||
return redeemToken(token, telegramUserId, telegramUsername, "telegram");
|
return redeemToken(token, telegramUserId, telegramUsername, "telegram");
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Löst einen Discord-Token ein.
|
* L\u00f6st einen Discord-Token ein.
|
||||||
* Wrapper für redeemToken mit type="discord".
|
* Wrapper f\u00fcr redeemToken mit type="discord".
|
||||||
*/
|
*/
|
||||||
public LinkedAccount redeemDiscord(String token, String discordUserId, String discordUsername) {
|
public LinkedAccount redeemDiscord(String token, String discordUserId, String discordUsername) {
|
||||||
return redeemToken(token, discordUserId, discordUsername, "discord");
|
return redeemToken(token, discordUserId, discordUsername, "discord");
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Gibt den Anzeigenamen für einen Telegram-Nutzer zurück.
|
* Gibt den Anzeigenamen f\u00fcr einen Telegram-Nutzer zur\u00fcck.
|
||||||
* Wenn verknüpft: "MinecraftName (@telegram)", sonst: "@telegram"
|
* Wenn verkn\u00fcpft: "MinecraftName (@telegram)", sonst: "@telegram"
|
||||||
*/
|
*/
|
||||||
public String resolveTelegramName(String telegramUserId, String fallbackName) {
|
public String resolveTelegramName(String telegramUserId, String fallbackName) {
|
||||||
String mc = getMinecraftNameByTelegramId(telegramUserId);
|
String mc = getMinecraftNameByTelegramId(telegramUserId);
|
||||||
@@ -238,8 +238,8 @@ public class AccountLinkManager {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Gibt den Anzeigenamen für einen Discord-Nutzer zurück.
|
* Gibt den Anzeigenamen f\u00fcr einen Discord-Nutzer zur\u00fcck.
|
||||||
* Wenn verknüpft: Minecraft-Name, sonst: Discord-Username
|
* Wenn verkn\u00fcpft: Minecraft-Name, sonst: Discord-Username
|
||||||
*/
|
*/
|
||||||
public String resolveDiscordName(String discordUserId, String fallbackName) {
|
public String resolveDiscordName(String discordUserId, String fallbackName) {
|
||||||
String mc = getMinecraftNameByDiscordId(discordUserId);
|
String mc = getMinecraftNameByDiscordId(discordUserId);
|
||||||
@@ -247,7 +247,7 @@ public class AccountLinkManager {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Gibt das korrekte Format-String zurück abhängig ob Account verknüpft.
|
* Gibt das korrekte Format-String zur\u00fcck abh\u00e4ngig ob Account verkn\u00fcpft.
|
||||||
* linked=true → linkedFormat (mit {player}), false → unlinkedFormat (mit {user})
|
* linked=true → linkedFormat (mit {player}), false → unlinkedFormat (mit {user})
|
||||||
*/
|
*/
|
||||||
public boolean isLinkedTelegram(String telegramUserId) {
|
public boolean isLinkedTelegram(String telegramUserId) {
|
||||||
|
|||||||
@@ -35,7 +35,7 @@ public class BlockManager {
|
|||||||
save();
|
save();
|
||||||
}
|
}
|
||||||
|
|
||||||
/** Spieler `blocker` hebt den Block für `target` auf. */
|
/** Spieler `blocker` hebt den Block f\u00fcr `target` auf. */
|
||||||
public void unblock(UUID blocker, UUID target) {
|
public void unblock(UUID blocker, UUID target) {
|
||||||
Set<UUID> set = blocked.get(blocker);
|
Set<UUID> set = blocked.get(blocker);
|
||||||
if (set != null) {
|
if (set != null) {
|
||||||
@@ -46,7 +46,7 @@ public class BlockManager {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Prüft ob `blocker` den Spieler `target` blockiert hat.
|
* Pr\u00fcft ob `blocker` den Spieler `target` blockiert hat.
|
||||||
* Admins (isAdmin=true) sind niemals blockiert.
|
* Admins (isAdmin=true) sind niemals blockiert.
|
||||||
*/
|
*/
|
||||||
public boolean isBlocked(UUID blocker, UUID target) {
|
public boolean isBlocked(UUID blocker, UUID target) {
|
||||||
@@ -55,8 +55,8 @@ public class BlockManager {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Prüft ob eine Nachricht von `sender` an `receiver` zugestellt werden soll.
|
* Pr\u00fcft ob eine Nachricht von `sender` an `receiver` zugestellt werden soll.
|
||||||
* Gibt false zurück, wenn einer der beiden den anderen blockiert.
|
* Gibt false zur\u00fcck, wenn einer der beiden den anderen blockiert.
|
||||||
*/
|
*/
|
||||||
public boolean canReceive(UUID sender, UUID receiver) {
|
public boolean canReceive(UUID sender, UUID receiver) {
|
||||||
// receiver hat sender blockiert → keine Nachricht
|
// receiver hat sender blockiert → keine Nachricht
|
||||||
@@ -66,7 +66,7 @@ public class BlockManager {
|
|||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
/** Gibt alle UUIDs zurück, die `blocker` blockiert hat. */
|
/** Gibt alle UUIDs zur\u00fcck, die `blocker` blockiert hat. */
|
||||||
public Set<UUID> getBlockedBy(UUID blocker) {
|
public Set<UUID> getBlockedBy(UUID blocker) {
|
||||||
Set<UUID> set = blocked.get(blocker);
|
Set<UUID> set = blocked.get(blocker);
|
||||||
if (set == null) return Collections.emptySet();
|
if (set == null) return Collections.emptySet();
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
package net.viper.status.modules.chat;
|
package net.viper.status.modules.chat;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Repräsentiert einen Chat-Kanal mit allen zugehörigen Einstellungen.
|
* Repr\u00e4sentiert einen Chat-Kanal mit allen zugeh\u00f6rigen Einstellungen.
|
||||||
*/
|
*/
|
||||||
public class ChatChannel {
|
public class ChatChannel {
|
||||||
|
|
||||||
@@ -51,12 +51,12 @@ public class ChatChannel {
|
|||||||
public int getTelegramThreadId() { return telegramThreadId; }
|
public int getTelegramThreadId() { return telegramThreadId; }
|
||||||
public boolean isUseAdminBridge() { return useAdminBridge; }
|
public boolean isUseAdminBridge() { return useAdminBridge; }
|
||||||
|
|
||||||
/** Prüft ob der Kanal eine Permission erfordert. */
|
/** Pr\u00fcft ob der Kanal eine Permission erfordert. */
|
||||||
public boolean hasPermission() {
|
public boolean hasPermission() {
|
||||||
return permission != null && !permission.isEmpty();
|
return permission != null && !permission.isEmpty();
|
||||||
}
|
}
|
||||||
|
|
||||||
/** Gibt das formatierte Kanalprefix zurück, z.B. §a[G] */
|
/** Gibt das formatierte Kanalprefix zur\u00fcck, z.B. §a[G] */
|
||||||
public String getFormattedTag() {
|
public String getFormattedTag() {
|
||||||
return net.md_5.bungee.api.ChatColor.translateAlternateColorCodes('&',
|
return net.md_5.bungee.api.ChatColor.translateAlternateColorCodes('&',
|
||||||
color + "[" + symbol + "]");
|
color + "[" + symbol + "]");
|
||||||
|
|||||||
@@ -10,12 +10,12 @@ import java.nio.file.Files;
|
|||||||
import java.util.*;
|
import java.util.*;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Lädt und verwaltet die chat.yml Konfiguration.
|
* L\u00e4dt und verwaltet die chat.yml Konfiguration.
|
||||||
*
|
*
|
||||||
* Fix #8: Rate-Limit-Werte aus anti-spam werden nicht mehr durch nachfolgende
|
* Fix #8: Rate-Limit-Werte aus anti-spam werden nicht mehr durch nachfolgende
|
||||||
* Berechnungen überschrieben. Der rate-limit.chat-Block hat jetzt Vorrang.
|
* Berechnungen \u00fcberschrieben. Der rate-limit.chat-Block hat jetzt Vorrang.
|
||||||
* Die Reihenfolge ist: erst rate-limit.chat einlesen, dann ggf. durch anti-spam
|
* Die Reihenfolge ist: erst rate-limit.chat einlesen, dann ggf. durch anti-spam
|
||||||
* als Fallback ergänzen, nicht umgekehrt.
|
* als Fallback erg\u00e4nzen, nicht umgekehrt.
|
||||||
*/
|
*/
|
||||||
public class ChatConfig {
|
public class ChatConfig {
|
||||||
|
|
||||||
@@ -103,13 +103,13 @@ public class ChatConfig {
|
|||||||
config = new Configuration();
|
config = new Configuration();
|
||||||
}
|
}
|
||||||
parseConfig();
|
parseConfig();
|
||||||
plugin.getLogger().fine("[ChatModule] " + channels.size() + " Kanäle geladen.");
|
plugin.getLogger().fine("[ChatModule] " + channels.size() + " Kan\u00e4le geladen.");
|
||||||
}
|
}
|
||||||
|
|
||||||
private void parseConfig() {
|
private void parseConfig() {
|
||||||
defaultChannel = config.getString("default-channel", "global");
|
defaultChannel = config.getString("default-channel", "global");
|
||||||
|
|
||||||
// --- Kanäle ---
|
// --- Kan\u00e4le ---
|
||||||
channels.clear();
|
channels.clear();
|
||||||
Configuration chSection = config.getSection("channels");
|
Configuration chSection = config.getSection("channels");
|
||||||
if (chSection != null) {
|
if (chSection != null) {
|
||||||
@@ -209,10 +209,10 @@ public class ChatConfig {
|
|||||||
linkingEnabled = al == null || al.getBoolean("enabled", true);
|
linkingEnabled = al == null || al.getBoolean("enabled", true);
|
||||||
linkDiscordMessage = al != null ? al.getString("discord-link-message", "&aCode: &f{token}") : "&aCode: &f{token}";
|
linkDiscordMessage = al != null ? al.getString("discord-link-message", "&aCode: &f{token}") : "&aCode: &f{token}";
|
||||||
linkTelegramMessage = al != null ? al.getString("telegram-link-message", "&aCode: &f{token}") : "&aCode: &f{token}";
|
linkTelegramMessage = al != null ? al.getString("telegram-link-message", "&aCode: &f{token}") : "&aCode: &f{token}";
|
||||||
linkSuccessDiscord = al != null ? al.getString("success-discord", "&aDiscord verknüpft!") : "&aDiscord verknüpft!";
|
linkSuccessDiscord = al != null ? al.getString("success-discord", "&aDiscord verkn\u00fcpft!") : "&aDiscord verkn\u00fcpft!";
|
||||||
linkSuccessTelegram = al != null ? al.getString("success-telegram", "&aTelegram verknüpft!") : "&aTelegram verknüpft!";
|
linkSuccessTelegram = al != null ? al.getString("success-telegram", "&aTelegram verkn\u00fcpft!") : "&aTelegram verkn\u00fcpft!";
|
||||||
linkBotSuccessDiscord = al != null ? al.getString("bot-success-discord", "✅ Verknüpft: {player}") : "✅ Verknüpft: {player}";
|
linkBotSuccessDiscord = al != null ? al.getString("bot-success-discord", "✅ Verkn\u00fcpft: {player}") : "✅ Verkn\u00fcpft: {player}";
|
||||||
linkBotSuccessTelegram = al != null ? al.getString("bot-success-telegram", "✅ Verknüpft: {player}") : "✅ Verknüpft: {player}";
|
linkBotSuccessTelegram = al != null ? al.getString("bot-success-telegram", "✅ Verkn\u00fcpft: {player}") : "✅ Verkn\u00fcpft: {player}";
|
||||||
linkedDiscordFormat = al != null ? al.getString("linked-discord-format", "&9[&bDiscord&9] &f{player} &8(&7{user}&8)&8: &f{message}") : "&9[&bDiscord&9] &f{player} &8(&7{user}&8)&8: &f{message}";
|
linkedDiscordFormat = al != null ? al.getString("linked-discord-format", "&9[&bDiscord&9] &f{player} &8(&7{user}&8)&8: &f{message}") : "&9[&bDiscord&9] &f{player} &8(&7{user}&8)&8: &f{message}";
|
||||||
linkedTelegramFormat = al != null ? al.getString("linked-telegram-format", "&3[&bTelegram&3] &f{player} &8(&7{user}&8)&8: &f{message}") : "&3[&bTelegram&3] &f{player} &8(&7{user}&8)&8: &f{message}";
|
linkedTelegramFormat = al != null ? al.getString("linked-telegram-format", "&3[&bTelegram&3] &f{player} &8(&7{user}&8)&8: &f{message}") : "&3[&bTelegram&3] &f{player} &8(&7{user}&8)&8: &f{message}";
|
||||||
|
|
||||||
@@ -227,7 +227,7 @@ public class ChatConfig {
|
|||||||
filterConfig.spamMaxMessages = spam.getInt("max-messages", 3);
|
filterConfig.spamMaxMessages = spam.getInt("max-messages", 3);
|
||||||
filterConfig.spamMessage = spam.getString("message", "&cNicht so schnell!");
|
filterConfig.spamMessage = spam.getString("message", "&cNicht so schnell!");
|
||||||
// FIX #8: Fallback-Werte aus anti-spam werden NUR gesetzt wenn rate-limit.chat nicht
|
// FIX #8: Fallback-Werte aus anti-spam werden NUR gesetzt wenn rate-limit.chat nicht
|
||||||
// konfiguriert ist. Wir setzen die Werte hier als Vorbelegung und überschreiben sie
|
// konfiguriert ist. Wir setzen die Werte hier als Vorbelegung und \u00fcberschreiben sie
|
||||||
// unten mit dem rate-limit.chat-Block wenn vorhanden.
|
// unten mit dem rate-limit.chat-Block wenn vorhanden.
|
||||||
filterConfig.globalRateLimitWindowMs = Math.max(500L, filterConfig.spamCooldownMs);
|
filterConfig.globalRateLimitWindowMs = Math.max(500L, filterConfig.spamCooldownMs);
|
||||||
filterConfig.globalRateLimitMaxActions = Math.max(1, filterConfig.spamMaxMessages);
|
filterConfig.globalRateLimitMaxActions = Math.max(1, filterConfig.spamMaxMessages);
|
||||||
@@ -272,7 +272,7 @@ public class ChatConfig {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// --- Rate-Limit (FIX #8: dieser Block setzt die endgültigen Werte, hat Vorrang) ---
|
// --- Rate-Limit (FIX #8: dieser Block setzt die endg\u00fcltigen Werte, hat Vorrang) ---
|
||||||
pmRateLimitEnabled = true;
|
pmRateLimitEnabled = true;
|
||||||
pmRateLimitWindowMs = 5000L;
|
pmRateLimitWindowMs = 5000L;
|
||||||
pmRateLimitMaxActions = 4;
|
pmRateLimitMaxActions = 4;
|
||||||
@@ -283,7 +283,7 @@ public class ChatConfig {
|
|||||||
if (rl != null) {
|
if (rl != null) {
|
||||||
Configuration rlChat = rl.getSection("chat");
|
Configuration rlChat = rl.getSection("chat");
|
||||||
if (rlChat != null) {
|
if (rlChat != null) {
|
||||||
// FIX #8: rate-limit.chat überschreibt die anti-spam-Fallbacks vollständig
|
// FIX #8: rate-limit.chat \u00fcberschreibt die anti-spam-Fallbacks vollst\u00e4ndig
|
||||||
filterConfig.globalRateLimitEnabled = rlChat.getBoolean("enabled", true);
|
filterConfig.globalRateLimitEnabled = rlChat.getBoolean("enabled", true);
|
||||||
filterConfig.globalRateLimitWindowMs = rlChat.getLong("window-ms", filterConfig.globalRateLimitWindowMs);
|
filterConfig.globalRateLimitWindowMs = rlChat.getLong("window-ms", filterConfig.globalRateLimitWindowMs);
|
||||||
filterConfig.globalRateLimitMaxActions = rlChat.getInt("max-actions", filterConfig.globalRateLimitMaxActions);
|
filterConfig.globalRateLimitMaxActions = rlChat.getInt("max-actions", filterConfig.globalRateLimitMaxActions);
|
||||||
|
|||||||
@@ -9,11 +9,11 @@ import java.util.regex.Pattern;
|
|||||||
/**
|
/**
|
||||||
* Chat-Filter: Anti-Spam, Caps-Filter, Wort-Blacklist, Farbcode-Filter.
|
* Chat-Filter: Anti-Spam, Caps-Filter, Wort-Blacklist, Farbcode-Filter.
|
||||||
*
|
*
|
||||||
* Reihenfolge der Prüfungen in processChat():
|
* Reihenfolge der Pr\u00fcfungen in processChat():
|
||||||
* 1. Spam-Cooldown (zu schnell geschrieben?)
|
* 1. Spam-Cooldown (zu schnell geschrieben?)
|
||||||
* 2. Gleiche Nachricht wiederholt?
|
* 2. Gleiche Nachricht wiederholt?
|
||||||
* 3. Zu viele Großbuchstaben?
|
* 3. Zu viele Gro\u00dfbuchstaben?
|
||||||
* 4. Verbotene Wörter → ersetzen durch ****
|
* 4. Verbotene W\u00f6rter → ersetzen durch ****
|
||||||
* 5. Farbcodes (& Codes) → nur mit Permission erlaubt
|
* 5. Farbcodes (& Codes) → nur mit Permission erlaubt
|
||||||
*/
|
*/
|
||||||
public class ChatFilter {
|
public class ChatFilter {
|
||||||
@@ -21,10 +21,10 @@ public class ChatFilter {
|
|||||||
private final ChatFilterConfig cfg;
|
private final ChatFilterConfig cfg;
|
||||||
private final GlobalRateLimitFramework rateLimiter = GlobalRateLimitFramework.getInstance();
|
private final GlobalRateLimitFramework rateLimiter = GlobalRateLimitFramework.getInstance();
|
||||||
|
|
||||||
// UUID → letzte Nachricht (für Duplikat-Check)
|
// UUID → letzte Nachricht (f\u00fcr Duplikat-Check)
|
||||||
private final Map<UUID, String> lastMessageText = new ConcurrentHashMap<>();
|
private final Map<UUID, String> lastMessageText = new ConcurrentHashMap<>();
|
||||||
|
|
||||||
// Kompilierte Regex-Pattern für Blacklist-Wörter
|
// Kompilierte Regex-Pattern f\u00fcr Blacklist-W\u00f6rter
|
||||||
private final List<Pattern> blacklistPatterns = new ArrayList<>();
|
private final List<Pattern> blacklistPatterns = new ArrayList<>();
|
||||||
|
|
||||||
public ChatFilter(ChatFilterConfig cfg) {
|
public ChatFilter(ChatFilterConfig cfg) {
|
||||||
@@ -46,7 +46,7 @@ public class ChatFilter {
|
|||||||
public enum FilterResult {
|
public enum FilterResult {
|
||||||
ALLOWED, // Nachricht darf durch
|
ALLOWED, // Nachricht darf durch
|
||||||
BLOCKED, // Nachricht blockiert (Spam/Flood)
|
BLOCKED, // Nachricht blockiert (Spam/Flood)
|
||||||
MODIFIED // Nachricht wurde verändert (Wörter ersetzt / Caps reduziert)
|
MODIFIED // Nachricht wurde ver\u00e4ndert (W\u00f6rter ersetzt / Caps reduziert)
|
||||||
}
|
}
|
||||||
|
|
||||||
public static class FilterResponse {
|
public static class FilterResponse {
|
||||||
@@ -68,7 +68,7 @@ public class ChatFilter {
|
|||||||
*
|
*
|
||||||
* @param uuid UUID des sendenden Spielers
|
* @param uuid UUID des sendenden Spielers
|
||||||
* @param message Originalnachricht
|
* @param message Originalnachricht
|
||||||
* @param isAdmin true → Farbcodes und Caps-Filter überspringen
|
* @param isAdmin true → Farbcodes und Caps-Filter \u00fcberspringen
|
||||||
* @param hasColorPerm true → &-Farbcodes erlaubt
|
* @param hasColorPerm true → &-Farbcodes erlaubt
|
||||||
* @param hasFormatPerm true → &l, &o etc. erlaubt
|
* @param hasFormatPerm true → &l, &o etc. erlaubt
|
||||||
* @return FilterResponse mit Ergebnis und ggf. modifizierter Nachricht
|
* @return FilterResponse mit Ergebnis und ggf. modifizierter Nachricht
|
||||||
@@ -160,7 +160,7 @@ public class ChatFilter {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private String applyCapsFilter(String message) {
|
private String applyCapsFilter(String message) {
|
||||||
// Zähle Großbuchstaben
|
// Z\u00e4hle Gro\u00dfbuchstaben
|
||||||
int total = 0, upper = 0;
|
int total = 0, upper = 0;
|
||||||
for (char c : message.toCharArray()) {
|
for (char c : message.toCharArray()) {
|
||||||
if (Character.isLetter(c)) { total++; if (Character.isUpperCase(c)) upper++; }
|
if (Character.isLetter(c)) { total++; if (Character.isUpperCase(c)) upper++; }
|
||||||
@@ -192,9 +192,9 @@ public class ChatFilter {
|
|||||||
if (isFormat && hasFormatPerm) { sb.append(c); continue; }
|
if (isFormat && hasFormatPerm) { sb.append(c); continue; }
|
||||||
if (isHex && hasColorPerm) { sb.append(c); continue; }
|
if (isHex && hasColorPerm) { sb.append(c); continue; }
|
||||||
|
|
||||||
// Kein Recht → & und nächstes Zeichen überspringen
|
// Kein Recht → & und n\u00e4chstes Zeichen \u00fcberspringen
|
||||||
if (isColor || isFormat) { i++; continue; }
|
if (isColor || isFormat) { i++; continue; }
|
||||||
// Hex: &# + 6 Zeichen überspringen (i zeigt auf &, +1 = #, +2..+7 = RRGGBB)
|
// Hex: &# + 6 Zeichen \u00fcberspringen (i zeigt auf &, +1 = #, +2..+7 = RRGGBB)
|
||||||
if (isHex && i + 7 <= message.length()) { i += 7; continue; }
|
if (isHex && i + 7 <= message.length()) { i += 7; continue; }
|
||||||
}
|
}
|
||||||
sb.append(c);
|
sb.append(c);
|
||||||
@@ -215,7 +215,7 @@ public class ChatFilter {
|
|||||||
Pattern.compile("(?i)(https?://|www\\.)\\S+");
|
Pattern.compile("(?i)(https?://|www\\.)\\S+");
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Prüft ob die Nachricht Werbung enthält (IP, URL, fremde Domain).
|
* Pr\u00fcft ob die Nachricht Werbung enth\u00e4lt (IP, URL, fremde Domain).
|
||||||
* Domains auf der Whitelist werden ignoriert.
|
* Domains auf der Whitelist werden ignoriert.
|
||||||
*
|
*
|
||||||
* Erkennt:
|
* Erkennt:
|
||||||
@@ -314,15 +314,15 @@ public class ChatFilter {
|
|||||||
|
|
||||||
// Caps
|
// Caps
|
||||||
public boolean capsFilterEnabled = true;
|
public boolean capsFilterEnabled = true;
|
||||||
public int capsMinLength = 6; // Mindestlänge für Caps-Check
|
public int capsMinLength = 6; // Mindestl\u00e4nge f\u00fcr Caps-Check
|
||||||
public int capsMaxPercent = 70; // Max. % Großbuchstaben
|
public int capsMaxPercent = 70; // Max. % Gro\u00dfbuchstaben
|
||||||
|
|
||||||
// Anti-Werbung
|
// Anti-Werbung
|
||||||
public boolean antiAdEnabled = true;
|
public boolean antiAdEnabled = true;
|
||||||
public String antiAdMessage = "&cWerbung ist in diesem Chat nicht erlaubt!";
|
public String antiAdMessage = "&cWerbung ist in diesem Chat nicht erlaubt!";
|
||||||
// Domains/Substrings die NICHT geblockt werden (z.B. eigene Serveradresse)
|
// Domains/Substrings die NICHT geblockt werden (z.B. eigene Serveradresse)
|
||||||
public List<String> antiAdWhitelist = new ArrayList<>();
|
public List<String> antiAdWhitelist = new ArrayList<>();
|
||||||
// TLDs die als Werbung gewertet werden (leer = alle TLDs prüfen)
|
// TLDs die als Werbung gewertet werden (leer = alle TLDs pr\u00fcfen)
|
||||||
public List<String> antiAdBlockedTlds = new ArrayList<>(Arrays.asList(
|
public List<String> antiAdBlockedTlds = new ArrayList<>(Arrays.asList(
|
||||||
"net", "com", "de", "org", "gg", "io", "eu", "tv", "xyz",
|
"net", "com", "de", "org", "gg", "io", "eu", "tv", "xyz",
|
||||||
"info", "me", "cc", "co", "app", "online", "site", "fun"
|
"info", "me", "cc", "co", "app", "online", "site", "fun"
|
||||||
|
|||||||
@@ -12,7 +12,7 @@ import java.util.logging.Logger;
|
|||||||
* Verzeichnis: plugins/StatusAPI/chatlogs/chatlog_YYYY-MM-DD.log
|
* Verzeichnis: plugins/StatusAPI/chatlogs/chatlog_YYYY-MM-DD.log
|
||||||
* Format: [HH:mm:ss] [MSG-XXXXXX] [SERVER] [CHANNEL] Spieler: Nachricht
|
* Format: [HH:mm:ss] [MSG-XXXXXX] [SERVER] [CHANNEL] Spieler: Nachricht
|
||||||
*
|
*
|
||||||
* Alte Logs werden beim Start und täglich automatisch bereinigt.
|
* Alte Logs werden beim Start und t\u00e4glich automatisch bereinigt.
|
||||||
* Die Aufbewahrungsdauer ist in der chat.yml konfigurierbar (7 oder 14 Tage).
|
* Die Aufbewahrungsdauer ist in der chat.yml konfigurierbar (7 oder 14 Tage).
|
||||||
*/
|
*/
|
||||||
public class ChatLogger {
|
public class ChatLogger {
|
||||||
@@ -37,7 +37,7 @@ public class ChatLogger {
|
|||||||
|
|
||||||
/**
|
/**
|
||||||
* Generiert eine eindeutige Nachrichten-ID (z.B. MSG-A3F2B1).
|
* Generiert eine eindeutige Nachrichten-ID (z.B. MSG-A3F2B1).
|
||||||
* Kombiniert Zeitstempel + inkrementellen Zähler für Eindeutigkeit.
|
* Kombiniert Zeitstempel + inkrementellen Z\u00e4hler f\u00fcr Eindeutigkeit.
|
||||||
*/
|
*/
|
||||||
public String generateMessageId() {
|
public String generateMessageId() {
|
||||||
int seq = counter.incrementAndGet();
|
int seq = counter.incrementAndGet();
|
||||||
@@ -49,7 +49,7 @@ public class ChatLogger {
|
|||||||
// ===== Logging =====
|
// ===== Logging =====
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Loggt eine Nachricht und gibt die generierte Nachrichten-ID zurück.
|
* Loggt eine Nachricht und gibt die generierte Nachrichten-ID zur\u00fcck.
|
||||||
*
|
*
|
||||||
* @param msgId Vorher generierte ID (aus generateMessageId())
|
* @param msgId Vorher generierte ID (aus generateMessageId())
|
||||||
* @param server Servername des Absenders
|
* @param server Servername des Absenders
|
||||||
@@ -80,7 +80,7 @@ public class ChatLogger {
|
|||||||
// ===== Cleanup =====
|
// ===== Cleanup =====
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Löscht Log-Dateien, die älter als retentionDays Tage sind.
|
* L\u00f6scht Log-Dateien, die \u00e4lter als retentionDays Tage sind.
|
||||||
* Wird beim Start und kann manuell aufgerufen werden.
|
* Wird beim Start und kann manuell aufgerufen werden.
|
||||||
*/
|
*/
|
||||||
public void cleanup() {
|
public void cleanup() {
|
||||||
@@ -92,7 +92,7 @@ public class ChatLogger {
|
|||||||
for (File f : files) {
|
for (File f : files) {
|
||||||
if (f.lastModified() < cutoff) {
|
if (f.lastModified() < cutoff) {
|
||||||
if (f.delete()) {
|
if (f.delete()) {
|
||||||
logger.info("[ChatLogger] Altes Log gelöscht: " + f.getName());
|
logger.info("[ChatLogger] Altes Log gel\u00f6scht: " + f.getName());
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -112,11 +112,11 @@ public class ChatLogger {
|
|||||||
|
|
||||||
/**
|
/**
|
||||||
* Liest die letzten `maxLines` Zeilen aus dem heutigen Chatlog.
|
* Liest die letzten `maxLines` Zeilen aus dem heutigen Chatlog.
|
||||||
* Wenn ein Spielername angegeben ist, werden nur seine Zeilen zurückgegeben.
|
* Wenn ein Spielername angegeben ist, werden nur seine Zeilen zur\u00fcckgegeben.
|
||||||
*
|
*
|
||||||
* @param playerFilter Spielername (case-insensitiv) oder null für alle
|
* @param playerFilter Spielername (case-insensitiv) oder null f\u00fcr alle
|
||||||
* @param maxLines Maximale Anzahl zurückgegebener Zeilen
|
* @param maxLines Maximale Anzahl zur\u00fcckgegebener Zeilen
|
||||||
* @return Liste der Logzeilen (älteste zuerst)
|
* @return Liste der Logzeilen (\u00e4lteste zuerst)
|
||||||
*/
|
*/
|
||||||
public List<String> readLastLines(String playerFilter, int maxLines) {
|
public List<String> readLastLines(String playerFilter, int maxLines) {
|
||||||
String date = DATE_FMT.format(new Date());
|
String date = DATE_FMT.format(new Date());
|
||||||
@@ -145,7 +145,7 @@ public class ChatLogger {
|
|||||||
logger.warning("[ChatLogger] Fehler beim Lesen: " + e.getMessage());
|
logger.warning("[ChatLogger] Fehler beim Lesen: " + e.getMessage());
|
||||||
}
|
}
|
||||||
|
|
||||||
// Letzte maxLines zurückgeben
|
// Letzte maxLines zur\u00fcckgeben
|
||||||
if (allLines.size() <= maxLines) return allLines;
|
if (allLines.size() <= maxLines) return allLines;
|
||||||
return allLines.subList(allLines.size() - maxLines, allLines.size());
|
return allLines.subList(allLines.size() - maxLines, allLines.size());
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -23,25 +23,25 @@ import java.util.concurrent.TimeUnit;
|
|||||||
import java.util.logging.Logger;
|
import java.util.logging.Logger;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* ChatModule für StatusAPI (BungeeCord)
|
* ChatModule f\u00fcr StatusAPI (BungeeCord)
|
||||||
*
|
*
|
||||||
* Features:
|
* Features:
|
||||||
* ✅ Mehrere Kanäle mit Permissions
|
* ✅ Mehrere Kan\u00e4le mit Permissions
|
||||||
* ✅ Server-Erkennung im Chat-Format
|
* ✅ Server-Erkennung im Chat-Format
|
||||||
* ✅ /helpop für Spieler
|
* ✅ /helpop f\u00fcr Spieler
|
||||||
* ✅ Emoji-Unterstützung (:smile: → 😊)
|
* ✅ Emoji-Unterst\u00fctzung (:smile: → 😊)
|
||||||
* ✅ Admin-Mute / Spieler-eigener Chat-Mute
|
* ✅ Admin-Mute / Spieler-eigener Chat-Mute
|
||||||
* ✅ CMI & Plugin-kompatibel (kein Eingriff in SubServer-Befehle)
|
* ✅ CMI & Plugin-kompatibel (kein Eingriff in SubServer-Befehle)
|
||||||
* ✅ Privat-Nachrichten (/msg, /r)
|
* ✅ Privat-Nachrichten (/msg, /r)
|
||||||
* ✅ Spieler-Blocking (/ignore, /unignore)
|
* ✅ Spieler-Blocking (/ignore, /unignore)
|
||||||
* ✅ Discord & Telegram Integration
|
* ✅ Discord & Telegram Integration
|
||||||
* ✅ Admin-Bypass (kann nicht gemutet/geblockt werden)
|
* ✅ Admin-Bypass (kann nicht gemutet/geblockt werden)
|
||||||
* ✅ /broadcast für Admins
|
* ✅ /broadcast f\u00fcr Admins
|
||||||
* ✅ Secure-Chat kompatibel (1.19+ & Bedrock/Geyser)
|
* ✅ Secure-Chat kompatibel (1.19+ & Bedrock/Geyser)
|
||||||
* ✅ Chat-Log mit konfigurierbarer Aufbewahrung (7 / 14 Tage)
|
* ✅ Chat-Log mit konfigurierbarer Aufbewahrung (7 / 14 Tage)
|
||||||
* ✅ Nachrichten-IDs (klickbar → Zwischenablage)
|
* ✅ Nachrichten-IDs (klickbar → Zwischenablage)
|
||||||
* ✅ Report-System (/report, /reports, /reportclose)
|
* ✅ Report-System (/report, /reports, /reportclose)
|
||||||
* ✅ Admin-Benachrichtigung bei Reports (sofort oder verzögert nach Login)
|
* ✅ Admin-Benachrichtigung bei Reports (sofort oder verz\u00f6gert nach Login)
|
||||||
* ✅ Server-Farben pro Server (&-Codes und &#RRGGBB HEX)
|
* ✅ Server-Farben pro Server (&-Codes und &#RRGGBB HEX)
|
||||||
* ✅ Join / Leave Nachrichten (mit Vanish-Support)
|
* ✅ Join / Leave Nachrichten (mit Vanish-Support)
|
||||||
* ✅ BungeeCord-Vanish-Integration via VanishProvider
|
* ✅ BungeeCord-Vanish-Integration via VanishProvider
|
||||||
@@ -69,7 +69,7 @@ public class ChatModule implements Module, Listener {
|
|||||||
// UUIDs die ihren eigenen Chat-Empfang deaktiviert haben
|
// UUIDs die ihren eigenen Chat-Empfang deaktiviert haben
|
||||||
private final Set<UUID> selfChatMuted = Collections.newSetFromMap(new ConcurrentHashMap<>());
|
private final Set<UUID> selfChatMuted = Collections.newSetFromMap(new ConcurrentHashMap<>());
|
||||||
|
|
||||||
// UUIDs die Mentions für sich deaktiviert haben
|
// UUIDs die Mentions f\u00fcr sich deaktiviert haben
|
||||||
private final Set<UUID> mentionsDisabled = Collections.newSetFromMap(new ConcurrentHashMap<>());
|
private final Set<UUID> mentionsDisabled = Collections.newSetFromMap(new ConcurrentHashMap<>());
|
||||||
|
|
||||||
// HelpOp Cooldown: UUID → letzter Zeitstempel (Sekunden)
|
// HelpOp Cooldown: UUID → letzter Zeitstempel (Sekunden)
|
||||||
@@ -78,13 +78,13 @@ public class ChatModule implements Module, Listener {
|
|||||||
// Report-Cooldown: UUID → letzter Report-Zeitstempel (Sekunden)
|
// Report-Cooldown: UUID → letzter Report-Zeitstempel (Sekunden)
|
||||||
private final Map<UUID, Long> reportCooldowns = new ConcurrentHashMap<>();
|
private final Map<UUID, Long> reportCooldowns = new ConcurrentHashMap<>();
|
||||||
|
|
||||||
// Letzte Chatnachricht pro Spieler (für Report-Kontext): name.toLowerCase() → message
|
// Letzte Chatnachricht pro Spieler (f\u00fcr Report-Kontext): name.toLowerCase() → message
|
||||||
private final Map<String, String> lastChatMessages = new ConcurrentHashMap<>();
|
private final Map<String, String> lastChatMessages = new ConcurrentHashMap<>();
|
||||||
|
|
||||||
// UUIDs die gerade auf Plugin-Chat-Eingabe warten
|
// UUIDs die gerade auf Plugin-Chat-Eingabe warten
|
||||||
private final Set<UUID> awaitingInput = Collections.newSetFromMap(new ConcurrentHashMap<>());
|
private final Set<UUID> awaitingInput = Collections.newSetFromMap(new ConcurrentHashMap<>());
|
||||||
|
|
||||||
// Geyser-Präfix für Bedrock-Spieler (Standard: ".")
|
// Geyser-Pr\u00e4fix f\u00fcr Bedrock-Spieler (Standard: ".")
|
||||||
private static final String GEYSER_PREFIX = ".";
|
private static final String GEYSER_PREFIX = ".";
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
@@ -126,7 +126,7 @@ public class ChatModule implements Module, Listener {
|
|||||||
linkManager = new AccountLinkManager(plugin.getDataFolder(), logger);
|
linkManager = new AccountLinkManager(plugin.getDataFolder(), logger);
|
||||||
linkManager.load();
|
linkManager.load();
|
||||||
|
|
||||||
// Externe Brücken
|
// Externe Br\u00fccken
|
||||||
if (config.isDiscordEnabled() || config.isReportWebhookEnabled()) {
|
if (config.isDiscordEnabled() || config.isReportWebhookEnabled()) {
|
||||||
discordBridge = new DiscordBridge(plugin, config);
|
discordBridge = new DiscordBridge(plugin, config);
|
||||||
discordBridge.setLinkManager(linkManager);
|
discordBridge.setLinkManager(linkManager);
|
||||||
@@ -144,7 +144,7 @@ public class ChatModule implements Module, Listener {
|
|||||||
ProxyServer.getInstance().getPluginManager().registerListener(plugin, this);
|
ProxyServer.getInstance().getPluginManager().registerListener(plugin, this);
|
||||||
registerCommands();
|
registerCommands();
|
||||||
|
|
||||||
logger.fine("[ChatModule] Aktiviert – " + config.getChannels().size() + " Kanäle geladen.");
|
logger.fine("[ChatModule] Aktiviert – " + config.getChannels().size() + " Kan\u00e4le geladen.");
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
@@ -166,17 +166,17 @@ public class ChatModule implements Module, Listener {
|
|||||||
// CHAT-EVENTS (BungeeCord original, 1.20+)
|
// CHAT-EVENTS (BungeeCord original, 1.20+)
|
||||||
//
|
//
|
||||||
// Das Bypass-Problem mit Paper:
|
// Das Bypass-Problem mit Paper:
|
||||||
// Wenn BungeeCord eine signierte Nachricht unverändert durchlässt
|
// Wenn BungeeCord eine signierte Nachricht unver\u00e4ndert durchl\u00e4sst
|
||||||
// (kein setCancelled), prüft Paper die Signatur → ungültig → Fehler.
|
// (kein setCancelled), pr\u00fcft Paper die Signatur → ung\u00fcltig → Fehler.
|
||||||
// Wenn wir setCancelled(true) setzen und die Nachricht selbst senden,
|
// Wenn wir setCancelled(true) setzen und die Nachricht selbst senden,
|
||||||
// fehlt die Signatur → Paper lehnt sie ebenfalls ab.
|
// fehlt die Signatur → Paper lehnt sie ebenfalls ab.
|
||||||
//
|
//
|
||||||
// Lösung: In der paper-global.yml auf JEDEM Sub-Server:
|
// L\u00f6sung: In der paper-global.yml auf JEDEM Sub-Server:
|
||||||
// messages:
|
// messages:
|
||||||
// reject-chat-unsigned: false
|
// reject-chat-unsigned: false
|
||||||
// Das erlaubt unsignierte Nachrichten vom Proxy durch.
|
// Das erlaubt unsignierte Nachrichten vom Proxy durch.
|
||||||
// Alternativ: In spigot.yml → settings: bungeecord: true (bereits nötig)
|
// Alternativ: In spigot.yml → settings: bungeecord: true (bereits n\u00f6tig)
|
||||||
// kombiniert mit BungeeCord IP-Forwarding deaktiviert Paper die Prüfung.
|
// kombiniert mit BungeeCord IP-Forwarding deaktiviert Paper die Pr\u00fcfung.
|
||||||
// =========================================================
|
// =========================================================
|
||||||
|
|
||||||
@EventHandler(priority = EventPriority.HIGHEST)
|
@EventHandler(priority = EventPriority.HIGHEST)
|
||||||
@@ -215,7 +215,7 @@ public class ChatModule implements Module, Listener {
|
|||||||
if (channel == null) channel = config.getDefaultChannel();
|
if (channel == null) channel = config.getDefaultChannel();
|
||||||
|
|
||||||
if (channel.hasPermission() && !player.hasPermission(channel.getPermission())) {
|
if (channel.hasPermission() && !player.hasPermission(channel.getPermission())) {
|
||||||
player.sendMessage(color("&cDu hast keine Berechtigung für den Kanal &f" + channel.getName() + "&c."));
|
player.sendMessage(color("&cDu hast keine Berechtigung f\u00fcr den Kanal &f" + channel.getName() + "&c."));
|
||||||
channelId = config.getDefaultChannelId();
|
channelId = config.getDefaultChannelId();
|
||||||
channel = config.getDefaultChannel();
|
channel = config.getDefaultChannel();
|
||||||
playerChannels.put(uuid, channelId);
|
playerChannels.put(uuid, channelId);
|
||||||
@@ -281,7 +281,7 @@ public class ChatModule implements Module, Listener {
|
|||||||
msgId = null;
|
msgId = null;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Letzte Nachricht des Spielers speichern (für Report-Kontext)
|
// Letzte Nachricht des Spielers speichern (f\u00fcr Report-Kontext)
|
||||||
lastChatMessages.put(player.getName().toLowerCase(), finalMessage);
|
lastChatMessages.put(player.getName().toLowerCase(), finalMessage);
|
||||||
|
|
||||||
final ChatChannel finalChannel = channel;
|
final ChatChannel finalChannel = channel;
|
||||||
@@ -314,7 +314,7 @@ public class ChatModule implements Module, Listener {
|
|||||||
|
|
||||||
if (isMentioned) {
|
if (isMentioned) {
|
||||||
recipient.sendMessage(color(config.getMentionsNotifyPrefix()
|
recipient.sendMessage(color(config.getMentionsNotifyPrefix()
|
||||||
+ "&7" + finalSenderName + " &7hat dich erwähnt!"));
|
+ "&7" + finalSenderName + " &7hat dich erw\u00e4hnt!"));
|
||||||
sendMentionSound(recipient, config.getMentionsSound());
|
sendMentionSound(recipient, config.getMentionsSound());
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -353,7 +353,7 @@ public class ChatModule implements Module, Listener {
|
|||||||
broadcastJoinLeave(player, true);
|
broadcastJoinLeave(player, true);
|
||||||
}
|
}
|
||||||
|
|
||||||
// ── Offene Reports für Admins anzeigen ──
|
// ── Offene Reports f\u00fcr Admins anzeigen ──
|
||||||
if (reportManager != null
|
if (reportManager != null
|
||||||
&& (player.hasPermission(config.getAdminNotifyPermission())
|
&& (player.hasPermission(config.getAdminNotifyPermission())
|
||||||
|| player.hasPermission(config.getAdminBypassPermission()))) {
|
|| player.hasPermission(config.getAdminBypassPermission()))) {
|
||||||
@@ -361,7 +361,7 @@ public class ChatModule implements Module, Listener {
|
|||||||
if (count > 0) {
|
if (count > 0) {
|
||||||
player.sendMessage(color("&8▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬"));
|
player.sendMessage(color("&8▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬"));
|
||||||
player.sendMessage(color("&c&l⚑ OFFENE REPORTS &8| &f" + count + " ausstehend"));
|
player.sendMessage(color("&c&l⚑ OFFENE REPORTS &8| &f" + count + " ausstehend"));
|
||||||
player.sendMessage(color("&7Tippe &f/reports &7für eine Übersicht."));
|
player.sendMessage(color("&7Tippe &f/reports &7f\u00fcr eine \u00dcbersicht."));
|
||||||
player.sendMessage(color("&8▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬"));
|
player.sendMessage(color("&8▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬"));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -457,7 +457,7 @@ public class ChatModule implements Module, Listener {
|
|||||||
ProxyServer.getInstance().getConsole().sendMessage(color(
|
ProxyServer.getInstance().getConsole().sendMessage(color(
|
||||||
isVanished ? "&8" + logMsg : "&7" + logMsg));
|
isVanished ? "&8" + logMsg : "&7" + logMsg));
|
||||||
|
|
||||||
// Brücken (nur für nicht-vanished Spieler)
|
// Br\u00fccken (nur f\u00fcr nicht-vanished Spieler)
|
||||||
if (!isVanished) {
|
if (!isVanished) {
|
||||||
String cleanMsg = ChatColor.stripColor(ChatColor.translateAlternateColorCodes('&', normalMsg));
|
String cleanMsg = ChatColor.stripColor(ChatColor.translateAlternateColorCodes('&', normalMsg));
|
||||||
if (discordBridge != null && !config.getJoinLeaveDiscordWebhook().isEmpty()) {
|
if (discordBridge != null && !config.getJoinLeaveDiscordWebhook().isEmpty()) {
|
||||||
@@ -484,7 +484,7 @@ public class ChatModule implements Module, Listener {
|
|||||||
if (!(sender instanceof ProxiedPlayer)) { sender.sendMessage(color("&cNur Spieler!")); return; }
|
if (!(sender instanceof ProxiedPlayer)) { sender.sendMessage(color("&cNur Spieler!")); return; }
|
||||||
ProxiedPlayer p = (ProxiedPlayer) sender;
|
ProxiedPlayer p = (ProxiedPlayer) sender;
|
||||||
if (args.length == 0) {
|
if (args.length == 0) {
|
||||||
p.sendMessage(color("&8▸ &eVerfügbare Kanäle:"));
|
p.sendMessage(color("&8▸ &eVerf\u00fcgbare Kan\u00e4le:"));
|
||||||
for (ChatChannel ch : config.getChannels().values()) {
|
for (ChatChannel ch : config.getChannels().values()) {
|
||||||
boolean hasPerm = !ch.hasPermission() || p.hasPermission(ch.getPermission());
|
boolean hasPerm = !ch.hasPermission() || p.hasPermission(ch.getPermission());
|
||||||
String active = ch.getId().equals(playerChannels.getOrDefault(p.getUniqueId(), config.getDefaultChannelId())) ? " &a✔" : "";
|
String active = ch.getId().equals(playerChannels.getOrDefault(p.getUniqueId(), config.getDefaultChannelId())) ? " &a✔" : "";
|
||||||
@@ -499,7 +499,7 @@ public class ChatModule implements Module, Listener {
|
|||||||
ChatChannel ch = config.getChannel(target);
|
ChatChannel ch = config.getChannel(target);
|
||||||
if (ch == null) { p.sendMessage(color("&cKanal &f" + args[0] + " &cnicht gefunden.")); return; }
|
if (ch == null) { p.sendMessage(color("&cKanal &f" + args[0] + " &cnicht gefunden.")); return; }
|
||||||
if (ch.hasPermission() && !p.hasPermission(ch.getPermission())) {
|
if (ch.hasPermission() && !p.hasPermission(ch.getPermission())) {
|
||||||
p.sendMessage(color("&cDu hast keine Berechtigung für diesen Kanal.")); return;
|
p.sendMessage(color("&cDu hast keine Berechtigung f\u00fcr diesen Kanal.")); return;
|
||||||
}
|
}
|
||||||
playerChannels.put(p.getUniqueId(), ch.getId());
|
playerChannels.put(p.getUniqueId(), ch.getId());
|
||||||
p.sendMessage(color("&aKanal gewechselt: " + ch.getFormattedTag() + " &a" + ch.getName()));
|
p.sendMessage(color("&aKanal gewechselt: " + ch.getFormattedTag() + " &a" + ch.getName()));
|
||||||
@@ -519,7 +519,7 @@ public class ChatModule implements Module, Listener {
|
|||||||
Long last = helpopCooldowns.get(p.getUniqueId());
|
Long last = helpopCooldowns.get(p.getUniqueId());
|
||||||
if (last != null && (now - last) < config.getHelpopCooldown()) {
|
if (last != null && (now - last) < config.getHelpopCooldown()) {
|
||||||
long wait = config.getHelpopCooldown() - (now - last);
|
long wait = config.getHelpopCooldown() - (now - last);
|
||||||
p.sendMessage(color("&cBitte warte noch &f" + wait + "s &cvor dem nächsten HelpOp."));
|
p.sendMessage(color("&cBitte warte noch &f" + wait + "s &cvor dem n\u00e4chsten HelpOp."));
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
helpopCooldowns.put(p.getUniqueId(), now);
|
helpopCooldowns.put(p.getUniqueId(), now);
|
||||||
@@ -550,8 +550,8 @@ public class ChatModule implements Module, Listener {
|
|||||||
ProxyServer.getInstance().getPluginManager().registerCommand(plugin, helpop);
|
ProxyServer.getInstance().getPluginManager().registerCommand(plugin, helpop);
|
||||||
|
|
||||||
// /msg <spieler> <nachricht>
|
// /msg <spieler> <nachricht>
|
||||||
// Vanish: Vanished Spieler sind für normale Spieler nicht erreichbar.
|
// Vanish: Vanished Spieler sind f\u00fcr normale Spieler nicht erreichbar.
|
||||||
// Admins können vanished Spieler per PM kontaktieren.
|
// Admins k\u00f6nnen vanished Spieler per PM kontaktieren.
|
||||||
Command msgCmd = new Command("msg", null, "tell", "w", "whisper") {
|
Command msgCmd = new Command("msg", null, "tell", "w", "whisper") {
|
||||||
@Override
|
@Override
|
||||||
public void execute(CommandSender sender, String[] args) {
|
public void execute(CommandSender sender, String[] args) {
|
||||||
@@ -562,7 +562,7 @@ public class ChatModule implements Module, Listener {
|
|||||||
ProxiedPlayer from = (ProxiedPlayer) sender;
|
ProxiedPlayer from = (ProxiedPlayer) sender;
|
||||||
boolean fromIsAdmin = from.hasPermission(config.getAdminBypassPermission());
|
boolean fromIsAdmin = from.hasPermission(config.getAdminBypassPermission());
|
||||||
|
|
||||||
// Ziel suchen – vanished Spieler sind für Nicht-Admins "nicht gefunden"
|
// Ziel suchen – vanished Spieler sind f\u00fcr Nicht-Admins "nicht gefunden"
|
||||||
ProxiedPlayer to = findVisiblePlayer(args[0], fromIsAdmin);
|
ProxiedPlayer to = findVisiblePlayer(args[0], fromIsAdmin);
|
||||||
if (to == null || !to.isConnected()) {
|
if (to == null || !to.isConnected()) {
|
||||||
from.sendMessage(color("&cSpieler &f" + args[0] + " &cnicht gefunden.")); return;
|
from.sendMessage(color("&cSpieler &f" + args[0] + " &cnicht gefunden.")); return;
|
||||||
@@ -597,7 +597,7 @@ public class ChatModule implements Module, Listener {
|
|||||||
ProxiedPlayer target = ProxyServer.getInstance().getPlayer(args[0]);
|
ProxiedPlayer target = ProxyServer.getInstance().getPlayer(args[0]);
|
||||||
if (target == null) { p.sendMessage(color("&cSpieler nicht gefunden.")); return; }
|
if (target == null) { p.sendMessage(color("&cSpieler nicht gefunden.")); return; }
|
||||||
if (target.hasPermission(config.getAdminBypassPermission())) {
|
if (target.hasPermission(config.getAdminBypassPermission())) {
|
||||||
p.sendMessage(color("&cAdmins können nicht ignoriert werden.")); return;
|
p.sendMessage(color("&cAdmins k\u00f6nnen nicht ignoriert werden.")); return;
|
||||||
}
|
}
|
||||||
if (blockManager.isBlocked(p.getUniqueId(), target.getUniqueId())) {
|
if (blockManager.isBlocked(p.getUniqueId(), target.getUniqueId())) {
|
||||||
p.sendMessage(color("&cDu hast &f" + target.getName() + " &cbereits ignoriert.")); return;
|
p.sendMessage(color("&cDu hast &f" + target.getName() + " &cbereits ignoriert.")); return;
|
||||||
@@ -623,7 +623,7 @@ public class ChatModule implements Module, Listener {
|
|||||||
p.sendMessage(color("&cDu hast diesen Spieler nicht ignoriert.")); return;
|
p.sendMessage(color("&cDu hast diesen Spieler nicht ignoriert.")); return;
|
||||||
}
|
}
|
||||||
blockManager.unblock(p.getUniqueId(), target.getUniqueId());
|
blockManager.unblock(p.getUniqueId(), target.getUniqueId());
|
||||||
p.sendMessage(color("&aIgnore für &f" + args[0] + " &aaufgehoben."));
|
p.sendMessage(color("&aIgnore f\u00fcr &f" + args[0] + " &aaufgehoben."));
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
ProxyServer.getInstance().getPluginManager().registerCommand(plugin, unignoreCmd);
|
ProxyServer.getInstance().getPluginManager().registerCommand(plugin, unignoreCmd);
|
||||||
@@ -641,14 +641,14 @@ public class ChatModule implements Module, Listener {
|
|||||||
int duration = config.getDefaultMuteDuration();
|
int duration = config.getDefaultMuteDuration();
|
||||||
if (args.length >= 2) {
|
if (args.length >= 2) {
|
||||||
try { duration = Integer.parseInt(args[1]); }
|
try { duration = Integer.parseInt(args[1]); }
|
||||||
catch (NumberFormatException ex) { sender.sendMessage(color("&cUngültige Dauer. Bitte Zahl eingeben.")); return; }
|
catch (NumberFormatException ex) { sender.sendMessage(color("&cUng\u00fcltige Dauer. Bitte Zahl eingeben.")); return; }
|
||||||
}
|
}
|
||||||
muteManager.mute(target.getUniqueId(), duration);
|
muteManager.mute(target.getUniqueId(), duration);
|
||||||
String durationStr = duration <= 0 ? "permanent" : duration + " Minuten";
|
String durationStr = duration <= 0 ? "permanent" : duration + " Minuten";
|
||||||
target.sendMessage(color("&cDu wurdest für " + durationStr + " stummgeschaltet."));
|
target.sendMessage(color("&cDu wurdest f\u00fcr " + durationStr + " stummgeschaltet."));
|
||||||
sender.sendMessage(color("&a" + target.getName() + " wurde für " + durationStr + " gemutet."));
|
sender.sendMessage(color("&a" + target.getName() + " wurde f\u00fcr " + durationStr + " gemutet."));
|
||||||
notifyAdmins("&8[&cMute&8] &f" + (sender instanceof ProxiedPlayer ? sender.getName() : "Konsole")
|
notifyAdmins("&8[&cMute&8] &f" + (sender instanceof ProxiedPlayer ? sender.getName() : "Konsole")
|
||||||
+ " &7hat &f" + target.getName() + " &7für &f" + durationStr + " &7gemutet.");
|
+ " &7hat &f" + target.getName() + " &7f\u00fcr &f" + durationStr + " &7gemutet.");
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
ProxyServer.getInstance().getPluginManager().registerCommand(plugin, muteCmd);
|
ProxyServer.getInstance().getPluginManager().registerCommand(plugin, muteCmd);
|
||||||
@@ -746,7 +746,7 @@ public class ChatModule implements Module, Listener {
|
|||||||
};
|
};
|
||||||
ProxyServer.getInstance().getPluginManager().registerCommand(plugin, reloadCmd);
|
ProxyServer.getInstance().getPluginManager().registerCommand(plugin, reloadCmd);
|
||||||
|
|
||||||
// /chatinfo <spieler> – Admin-Info über einen Spieler
|
// /chatinfo <spieler> – Admin-Info \u00fcber einen Spieler
|
||||||
Command chatInfoCmd = new Command("chatinfo", "chat.admin.bypass") {
|
Command chatInfoCmd = new Command("chatinfo", "chat.admin.bypass") {
|
||||||
@Override
|
@Override
|
||||||
public void execute(CommandSender sender, String[] args) {
|
public void execute(CommandSender sender, String[] args) {
|
||||||
@@ -769,9 +769,9 @@ public class ChatModule implements Module, Listener {
|
|||||||
|
|
||||||
AccountLinkManager.LinkedAccount link = linkManager.getByUUID(tUUID);
|
AccountLinkManager.LinkedAccount link = linkManager.getByUUID(tUUID);
|
||||||
String discordInfo = (link != null && !link.discordUserId.isEmpty())
|
String discordInfo = (link != null && !link.discordUserId.isEmpty())
|
||||||
? "&a" + link.discordUsername + " &8(" + link.discordUserId + ")" : "&7Nicht verknüpft";
|
? "&a" + link.discordUsername + " &8(" + link.discordUserId + ")" : "&7Nicht verkn\u00fcpft";
|
||||||
String telegramInfo = (link != null && !link.telegramUserId.isEmpty())
|
String telegramInfo = (link != null && !link.telegramUserId.isEmpty())
|
||||||
? "&a" + link.telegramUsername + " &8(" + link.telegramUserId + ")" : "&7Nicht verknüpft";
|
? "&a" + link.telegramUsername + " &8(" + link.telegramUserId + ")" : "&7Nicht verkn\u00fcpft";
|
||||||
|
|
||||||
String vanishStatus = VanishProvider.isVanished(target) ? "&eJa" : "&aKein";
|
String vanishStatus = VanishProvider.isVanished(target) ? "&eJa" : "&aKein";
|
||||||
|
|
||||||
@@ -834,7 +834,7 @@ public class ChatModule implements Module, Listener {
|
|||||||
sender.sendMessage(color("&8▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬"));
|
sender.sendMessage(color("&8▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬"));
|
||||||
|
|
||||||
if (history.isEmpty()) {
|
if (history.isEmpty()) {
|
||||||
sender.sendMessage(color("&7Keine Einträge gefunden."));
|
sender.sendMessage(color("&7Keine Eintr\u00e4ge gefunden."));
|
||||||
} else {
|
} else {
|
||||||
for (String line : history) {
|
for (String line : history) {
|
||||||
sender.sendMessage(color("&8" + line));
|
sender.sendMessage(color("&8" + line));
|
||||||
@@ -866,7 +866,7 @@ public class ChatModule implements Module, Listener {
|
|||||||
};
|
};
|
||||||
ProxyServer.getInstance().getPluginManager().registerCommand(plugin, mentionsCmd);
|
ProxyServer.getInstance().getPluginManager().registerCommand(plugin, mentionsCmd);
|
||||||
|
|
||||||
// /chatbypass – Chat-Verarbeitung für nächste Eingabe(n) überspringen
|
// /chatbypass – Chat-Verarbeitung f\u00fcr n\u00e4chste Eingabe(n) \u00fcberspringen
|
||||||
Command bypassCmd = new Command("chatbypass", null, "cbp") {
|
Command bypassCmd = new Command("chatbypass", null, "cbp") {
|
||||||
@Override
|
@Override
|
||||||
public void execute(CommandSender sender, String[] args) {
|
public void execute(CommandSender sender, String[] args) {
|
||||||
@@ -878,14 +878,14 @@ public class ChatModule implements Module, Listener {
|
|||||||
p.sendMessage(color("&aChatModule &l✔ &aaktiv."));
|
p.sendMessage(color("&aChatModule &l✔ &aaktiv."));
|
||||||
} else {
|
} else {
|
||||||
awaitingInput.add(p.getUniqueId());
|
awaitingInput.add(p.getUniqueId());
|
||||||
p.sendMessage(color("&eChatModule &l⏸ &eüberbrückt. &7Nächste Nachricht geht direkt an den Server."));
|
p.sendMessage(color("&eChatModule &l⏸ &e\u00fcberbr\u00fcckt. &7N\u00e4chste Nachricht geht direkt an den Server."));
|
||||||
p.sendMessage(color("&7Mit &f/chatbypass &7wieder einschalten."));
|
p.sendMessage(color("&7Mit &f/chatbypass &7wieder einschalten."));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
ProxyServer.getInstance().getPluginManager().registerCommand(plugin, bypassCmd);
|
ProxyServer.getInstance().getPluginManager().registerCommand(plugin, bypassCmd);
|
||||||
|
|
||||||
// /discordlink – Discord-Account verknüpfen
|
// /discordlink – Discord-Account verkn\u00fcpfen
|
||||||
Command discordLinkCmd = new Command("discordlink", null, "dlink") {
|
Command discordLinkCmd = new Command("discordlink", null, "dlink") {
|
||||||
@Override
|
@Override
|
||||||
public void execute(CommandSender sender, String[] args) {
|
public void execute(CommandSender sender, String[] args) {
|
||||||
@@ -896,16 +896,16 @@ public class ChatModule implements Module, Listener {
|
|||||||
String token = linkManager.generateToken(p.getUniqueId(), p.getName(), "discord");
|
String token = linkManager.generateToken(p.getUniqueId(), p.getName(), "discord");
|
||||||
|
|
||||||
p.sendMessage(color("&8▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬"));
|
p.sendMessage(color("&8▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬"));
|
||||||
p.sendMessage(color("&9&lDiscord-Verknüpfung"));
|
p.sendMessage(color("&9&lDiscord-Verkn\u00fcpfung"));
|
||||||
p.sendMessage(color("&7Schreibe dem Bot auf Discord:"));
|
p.sendMessage(color("&7Schreibe dem Bot auf Discord:"));
|
||||||
p.sendMessage(color("&f!link &b" + token));
|
p.sendMessage(color("&f!link &b" + token));
|
||||||
p.sendMessage(color("&7Token gültig für &f10 Minuten&7."));
|
p.sendMessage(color("&7Token g\u00fcltig f\u00fcr &f10 Minuten&7."));
|
||||||
p.sendMessage(color("&8▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬"));
|
p.sendMessage(color("&8▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬"));
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
ProxyServer.getInstance().getPluginManager().registerCommand(plugin, discordLinkCmd);
|
ProxyServer.getInstance().getPluginManager().registerCommand(plugin, discordLinkCmd);
|
||||||
|
|
||||||
// /telegramlink – Telegram-Account verknüpfen
|
// /telegramlink – Telegram-Account verkn\u00fcpfen
|
||||||
Command telegramLinkCmd = new Command("telegramlink", null, "tlink") {
|
Command telegramLinkCmd = new Command("telegramlink", null, "tlink") {
|
||||||
@Override
|
@Override
|
||||||
public void execute(CommandSender sender, String[] args) {
|
public void execute(CommandSender sender, String[] args) {
|
||||||
@@ -916,16 +916,16 @@ public class ChatModule implements Module, Listener {
|
|||||||
String token = linkManager.generateToken(p.getUniqueId(), p.getName(), "telegram");
|
String token = linkManager.generateToken(p.getUniqueId(), p.getName(), "telegram");
|
||||||
|
|
||||||
p.sendMessage(color("&8▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬"));
|
p.sendMessage(color("&8▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬"));
|
||||||
p.sendMessage(color("&3&lTelegram-Verknüpfung"));
|
p.sendMessage(color("&3&lTelegram-Verkn\u00fcpfung"));
|
||||||
p.sendMessage(color("&7Schreibe dem Bot auf Telegram:"));
|
p.sendMessage(color("&7Schreibe dem Bot auf Telegram:"));
|
||||||
p.sendMessage(color("&f/link &b" + token));
|
p.sendMessage(color("&f/link &b" + token));
|
||||||
p.sendMessage(color("&7Token gültig für &f10 Minuten&7."));
|
p.sendMessage(color("&7Token g\u00fcltig f\u00fcr &f10 Minuten&7."));
|
||||||
p.sendMessage(color("&8▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬"));
|
p.sendMessage(color("&8▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬"));
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
ProxyServer.getInstance().getPluginManager().registerCommand(plugin, telegramLinkCmd);
|
ProxyServer.getInstance().getPluginManager().registerCommand(plugin, telegramLinkCmd);
|
||||||
|
|
||||||
// /unlink – Verknüpfung aufheben
|
// /unlink – Verkn\u00fcpfung aufheben
|
||||||
Command unlinkCmd = new Command("unlink") {
|
Command unlinkCmd = new Command("unlink") {
|
||||||
@Override
|
@Override
|
||||||
public void execute(CommandSender sender, String[] args) {
|
public void execute(CommandSender sender, String[] args) {
|
||||||
@@ -940,21 +940,21 @@ public class ChatModule implements Module, Listener {
|
|||||||
switch (args[0].toLowerCase()) {
|
switch (args[0].toLowerCase()) {
|
||||||
case "discord":
|
case "discord":
|
||||||
if (linkManager.unlinkDiscord(p.getUniqueId()))
|
if (linkManager.unlinkDiscord(p.getUniqueId()))
|
||||||
p.sendMessage(color("&aDiscord-Verknüpfung aufgehoben."));
|
p.sendMessage(color("&aDiscord-Verkn\u00fcpfung aufgehoben."));
|
||||||
else
|
else
|
||||||
p.sendMessage(color("&cKein Discord-Account verknüpft."));
|
p.sendMessage(color("&cKein Discord-Account verkn\u00fcpft."));
|
||||||
break;
|
break;
|
||||||
case "telegram":
|
case "telegram":
|
||||||
if (linkManager.unlinkTelegram(p.getUniqueId()))
|
if (linkManager.unlinkTelegram(p.getUniqueId()))
|
||||||
p.sendMessage(color("&aTelegram-Verknüpfung aufgehoben."));
|
p.sendMessage(color("&aTelegram-Verkn\u00fcpfung aufgehoben."));
|
||||||
else
|
else
|
||||||
p.sendMessage(color("&cKein Telegram-Account verknüpft."));
|
p.sendMessage(color("&cKein Telegram-Account verkn\u00fcpft."));
|
||||||
break;
|
break;
|
||||||
case "all":
|
case "all":
|
||||||
boolean d = linkManager.unlinkDiscord(p.getUniqueId());
|
boolean d = linkManager.unlinkDiscord(p.getUniqueId());
|
||||||
boolean t = linkManager.unlinkTelegram(p.getUniqueId());
|
boolean t = linkManager.unlinkTelegram(p.getUniqueId());
|
||||||
if (d || t) p.sendMessage(color("&aAlle Verknüpfungen aufgehoben."));
|
if (d || t) p.sendMessage(color("&aAlle Verkn\u00fcpfungen aufgehoben."));
|
||||||
else p.sendMessage(color("&cKeine Verknüpfungen vorhanden."));
|
else p.sendMessage(color("&cKeine Verkn\u00fcpfungen vorhanden."));
|
||||||
break;
|
break;
|
||||||
default:
|
default:
|
||||||
p.sendMessage(color("&cBenutzung: /unlink <discord|telegram|all>"));
|
p.sendMessage(color("&cBenutzung: /unlink <discord|telegram|all>"));
|
||||||
@@ -974,7 +974,7 @@ public class ChatModule implements Module, Listener {
|
|||||||
|
|
||||||
String reqPerm = config.getReportPermission();
|
String reqPerm = config.getReportPermission();
|
||||||
if (reqPerm != null && !reqPerm.isEmpty() && !p.hasPermission(reqPerm)) {
|
if (reqPerm != null && !reqPerm.isEmpty() && !p.hasPermission(reqPerm)) {
|
||||||
p.sendMessage(color("&cDu hast keine Berechtigung für /report.")); return;
|
p.sendMessage(color("&cDu hast keine Berechtigung f\u00fcr /report.")); return;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (args.length < 2) {
|
if (args.length < 2) {
|
||||||
@@ -986,7 +986,7 @@ public class ChatModule implements Module, Listener {
|
|||||||
Long last = reportCooldowns.get(p.getUniqueId());
|
Long last = reportCooldowns.get(p.getUniqueId());
|
||||||
if (last != null && (now - last) < config.getReportCooldown()) {
|
if (last != null && (now - last) < config.getReportCooldown()) {
|
||||||
long wait = config.getReportCooldown() - (now - last);
|
long wait = config.getReportCooldown() - (now - last);
|
||||||
p.sendMessage(color("&cBitte warte noch &f" + wait + "s &cvor dem nächsten Report."));
|
p.sendMessage(color("&cBitte warte noch &f" + wait + "s &cvor dem n\u00e4chsten Report."));
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1040,7 +1040,7 @@ public class ChatModule implements Module, Listener {
|
|||||||
};
|
};
|
||||||
ProxyServer.getInstance().getPluginManager().registerCommand(plugin, reportCmd);
|
ProxyServer.getInstance().getPluginManager().registerCommand(plugin, reportCmd);
|
||||||
|
|
||||||
// /reports [all] – Admin-Übersicht
|
// /reports [all] – Admin-\u00dcbersicht
|
||||||
Command reportsCmd = new Command("reports", config.getReportViewPermission()) {
|
Command reportsCmd = new Command("reports", config.getReportViewPermission()) {
|
||||||
@Override
|
@Override
|
||||||
public void execute(CommandSender sender, String[] args) {
|
public void execute(CommandSender sender, String[] args) {
|
||||||
@@ -1054,7 +1054,7 @@ public class ChatModule implements Module, Listener {
|
|||||||
sender.sendMessage(color("&8▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬"));
|
sender.sendMessage(color("&8▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬"));
|
||||||
sender.sendMessage(color("&c&l⚑ REPORTS &8| &f" + list.size()
|
sender.sendMessage(color("&c&l⚑ REPORTS &8| &f" + list.size()
|
||||||
+ (showAll ? " gesamt" : " offen")
|
+ (showAll ? " gesamt" : " offen")
|
||||||
+ " &8| &7/reports all für alle"));
|
+ " &8| &7/reports all f\u00fcr alle"));
|
||||||
sender.sendMessage(color("&8▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬"));
|
sender.sendMessage(color("&8▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬"));
|
||||||
|
|
||||||
if (list.isEmpty()) {
|
if (list.isEmpty()) {
|
||||||
@@ -1102,7 +1102,7 @@ public class ChatModule implements Module, Listener {
|
|||||||
|
|
||||||
sender.sendMessage(color("&8▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬"));
|
sender.sendMessage(color("&8▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬"));
|
||||||
if (!showAll && sender instanceof ProxiedPlayer) {
|
if (!showAll && sender instanceof ProxiedPlayer) {
|
||||||
sender.sendMessage(color("&7Tipp: &f/reportclose <ID> &7zum Schließen."));
|
sender.sendMessage(color("&7Tipp: &f/reportclose <ID> &7zum Schlie\u00dfen."));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
@@ -1150,8 +1150,8 @@ public class ChatModule implements Module, Listener {
|
|||||||
// =========================================================
|
// =========================================================
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Öffentliche API für Sub-Server-Plugins oder BungeeCord-eigene Plugins.
|
* \u00d6ffentliche API f\u00fcr Sub-Server-Plugins oder BungeeCord-eigene Plugins.
|
||||||
* Setzt den Bypass-Status für einen Spieler.
|
* Setzt den Bypass-Status f\u00fcr einen Spieler.
|
||||||
*
|
*
|
||||||
* Beispiel aus einem anderen BungeeCord-Plugin:
|
* Beispiel aus einem anderen BungeeCord-Plugin:
|
||||||
* ChatModule chatModule = (ChatModule) proxy.getPluginManager()
|
* ChatModule chatModule = (ChatModule) proxy.getPluginManager()
|
||||||
@@ -1172,11 +1172,11 @@ public class ChatModule implements Module, Listener {
|
|||||||
// =========================================================
|
// =========================================================
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Sucht einen Spieler nach Name und berücksichtigt den Vanish-Status.
|
* Sucht einen Spieler nach Name und ber\u00fccksichtigt den Vanish-Status.
|
||||||
*
|
*
|
||||||
* @param name Spielername (case-insensitiv)
|
* @param name Spielername (case-insensitiv)
|
||||||
* @param callerIsAdmin true → Vanished Spieler werden ebenfalls gefunden
|
* @param callerIsAdmin true → Vanished Spieler werden ebenfalls gefunden
|
||||||
* @return ProxiedPlayer oder null wenn nicht gefunden / vanished (für Nicht-Admins)
|
* @return ProxiedPlayer oder null wenn nicht gefunden / vanished (f\u00fcr Nicht-Admins)
|
||||||
*/
|
*/
|
||||||
private ProxiedPlayer findVisiblePlayer(String name, boolean callerIsAdmin) {
|
private ProxiedPlayer findVisiblePlayer(String name, boolean callerIsAdmin) {
|
||||||
ProxiedPlayer target = ProxyServer.getInstance().getPlayer(name);
|
ProxiedPlayer target = ProxyServer.getInstance().getPlayer(name);
|
||||||
@@ -1193,8 +1193,8 @@ public class ChatModule implements Module, Listener {
|
|||||||
String player, String suffix, String message) {
|
String player, String suffix, String message) {
|
||||||
String serverColor = config.getServerColor(server);
|
String serverColor = config.getServerColor(server);
|
||||||
String serverDisplay = config.getServerDisplay(server);
|
String serverDisplay = config.getServerDisplay(server);
|
||||||
// Nur den Servernamen-Teil vorübersetzen damit &#RRGGBB im Display-Namen
|
// Nur den Servernamen-Teil vor\u00fcbersetzen damit &#RRGGBB im Display-Namen
|
||||||
// korrekt sitzt; der Rest wird am Ausgabepunkt via translateColors() übersetzt.
|
// korrekt sitzt; der Rest wird am Ausgabepunkt via translateColors() \u00fcbersetzt.
|
||||||
String coloredServer = serverColor + serverDisplay + "&r";
|
String coloredServer = serverColor + serverDisplay + "&r";
|
||||||
|
|
||||||
return format
|
return format
|
||||||
@@ -1218,7 +1218,7 @@ public class ChatModule implements Module, Listener {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Übersetzt sowohl klassische &-Farbcodes als auch HEX-Codes im Format &#RRGGBB.
|
* \u00dcbersetzt sowohl klassische &-Farbcodes als auch HEX-Codes im Format &#RRGGBB.
|
||||||
*/
|
*/
|
||||||
private String translateColors(String text) {
|
private String translateColors(String text) {
|
||||||
if (text == null) return "";
|
if (text == null) return "";
|
||||||
@@ -1238,7 +1238,7 @@ public class ChatModule implements Module, Listener {
|
|||||||
i += 8;
|
i += 8;
|
||||||
continue;
|
continue;
|
||||||
} catch (Exception ignored) {
|
} catch (Exception ignored) {
|
||||||
// Ungültige Farbe → als normalen Text behandeln
|
// Ung\u00fcltige Farbe → als normalen Text behandeln
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -1258,7 +1258,7 @@ public class ChatModule implements Module, Listener {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Benachrichtigt alle online Admins über einen neuen Report.
|
* Benachrichtigt alle online Admins \u00fcber einen neuen Report.
|
||||||
*/
|
*/
|
||||||
private void notifyAdminsReport(String reportId, String reporter, String reported,
|
private void notifyAdminsReport(String reportId, String reporter, String reported,
|
||||||
String server, String reason, String msgContext) {
|
String server, String reason, String msgContext) {
|
||||||
@@ -1366,7 +1366,7 @@ public class ChatModule implements Module, Listener {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// =========================================================
|
// =========================================================
|
||||||
// EXTERNE BRÜCKEN
|
// EXTERNE BR\u00dcCKEN
|
||||||
// =========================================================
|
// =========================================================
|
||||||
|
|
||||||
private void bridgeToDiscord(ChatChannel channel, String playerName, String message, String server) {
|
private void bridgeToDiscord(ChatChannel channel, String playerName, String message, String server) {
|
||||||
|
|||||||
@@ -5,8 +5,8 @@ import java.util.Map;
|
|||||||
/**
|
/**
|
||||||
* Ersetzt Emoji-Shortcuts (:smile:, :heart:, …) durch Unicode-Zeichen.
|
* Ersetzt Emoji-Shortcuts (:smile:, :heart:, …) durch Unicode-Zeichen.
|
||||||
*
|
*
|
||||||
* Bedrock-Spieler (Geyser) unterstützen Unicode-Emojis ebenfalls,
|
* Bedrock-Spieler (Geyser) unterst\u00fctzen Unicode-Emojis ebenfalls,
|
||||||
* da sie als reguläre UTF-8 Zeichen in TextComponents übertragen werden.
|
* da sie als regul\u00e4re UTF-8 Zeichen in TextComponents \u00fcbertragen werden.
|
||||||
*/
|
*/
|
||||||
public class EmojiParser {
|
public class EmojiParser {
|
||||||
|
|
||||||
@@ -20,7 +20,7 @@ public class EmojiParser {
|
|||||||
|
|
||||||
/**
|
/**
|
||||||
* Konvertiert alle bekannten Emoji-Shortcuts in der Nachricht zu Unicode.
|
* Konvertiert alle bekannten Emoji-Shortcuts in der Nachricht zu Unicode.
|
||||||
* Nicht erkannte Shortcuts bleiben unverändert.
|
* Nicht erkannte Shortcuts bleiben unver\u00e4ndert.
|
||||||
*
|
*
|
||||||
* @param message Die Originalnachricht des Spielers
|
* @param message Die Originalnachricht des Spielers
|
||||||
* @return Nachricht mit ersetzten Emojis
|
* @return Nachricht mit ersetzten Emojis
|
||||||
@@ -36,12 +36,12 @@ public class EmojiParser {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Gibt eine lesbare Liste aller Emojis zurück (für /emoji list).
|
* Gibt eine lesbare Liste aller Emojis zur\u00fcck (f\u00fcr /emoji list).
|
||||||
*/
|
*/
|
||||||
public String buildEmojiList() {
|
public String buildEmojiList() {
|
||||||
if (mappings.isEmpty()) return "&cKeine Emojis konfiguriert.";
|
if (mappings.isEmpty()) return "&cKeine Emojis konfiguriert.";
|
||||||
StringBuilder sb = new StringBuilder();
|
StringBuilder sb = new StringBuilder();
|
||||||
sb.append("&eVerfügbare Emojis:\n");
|
sb.append("&eVerf\u00fcgbare Emojis:\n");
|
||||||
int i = 0;
|
int i = 0;
|
||||||
for (Map.Entry<String, String> entry : mappings.entrySet()) {
|
for (Map.Entry<String, String> entry : mappings.entrySet()) {
|
||||||
sb.append("&7").append(entry.getKey()).append(" &f→ ").append(entry.getValue());
|
sb.append("&7").append(entry.getKey()).append(" &f→ ").append(entry.getValue());
|
||||||
|
|||||||
@@ -10,7 +10,7 @@ import java.util.logging.Logger;
|
|||||||
* Verwaltet Mutes von Spielern.
|
* Verwaltet Mutes von Spielern.
|
||||||
* Speichert: UUID → Ablaufzeitpunkt (Unix-Sekunden, 0 = permanent)
|
* Speichert: UUID → Ablaufzeitpunkt (Unix-Sekunden, 0 = permanent)
|
||||||
*
|
*
|
||||||
* Admins/OPs mit dem Bypass-Permission können nicht gemutet werden.
|
* Admins/OPs mit dem Bypass-Permission k\u00f6nnen nicht gemutet werden.
|
||||||
*/
|
*/
|
||||||
public class MuteManager {
|
public class MuteManager {
|
||||||
|
|
||||||
@@ -28,7 +28,7 @@ public class MuteManager {
|
|||||||
// ===== Mute-Logik =====
|
// ===== Mute-Logik =====
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Mutet einen Spieler für durationMinutes Minuten.
|
* Mutet einen Spieler f\u00fcr durationMinutes Minuten.
|
||||||
* durationMinutes = 0 → permanent
|
* durationMinutes = 0 → permanent
|
||||||
*/
|
*/
|
||||||
public void mute(UUID uuid, int durationMinutes) {
|
public void mute(UUID uuid, int durationMinutes) {
|
||||||
@@ -45,7 +45,7 @@ public class MuteManager {
|
|||||||
save();
|
save();
|
||||||
}
|
}
|
||||||
|
|
||||||
/** Prüft ob ein Spieler aktuell gemutet ist. */
|
/** Pr\u00fcft ob ein Spieler aktuell gemutet ist. */
|
||||||
public boolean isMuted(UUID uuid) {
|
public boolean isMuted(UUID uuid) {
|
||||||
Long expiry = mutes.get(uuid);
|
Long expiry = mutes.get(uuid);
|
||||||
if (expiry == null) return false;
|
if (expiry == null) return false;
|
||||||
@@ -60,8 +60,8 @@ public class MuteManager {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Gibt die verbleibende Zeit als lesbaren String zurück.
|
* Gibt die verbleibende Zeit als lesbaren String zur\u00fcck.
|
||||||
* Gibt "permanent" zurück bei dauerhaftem Mute.
|
* Gibt "permanent" zur\u00fcck bei dauerhaftem Mute.
|
||||||
*/
|
*/
|
||||||
public String getRemainingTime(UUID uuid) {
|
public String getRemainingTime(UUID uuid) {
|
||||||
Long expiry = mutes.get(uuid);
|
Long expiry = mutes.get(uuid);
|
||||||
|
|||||||
@@ -18,7 +18,7 @@ public class PrivateMsgManager {
|
|||||||
private final BlockManager blockManager;
|
private final BlockManager blockManager;
|
||||||
private final GlobalRateLimitFramework rateLimiter = GlobalRateLimitFramework.getInstance();
|
private final GlobalRateLimitFramework rateLimiter = GlobalRateLimitFramework.getInstance();
|
||||||
|
|
||||||
// UUID → letzte PM-Gesprächspartner UUID (für /r)
|
// UUID → letzte PM-Gespr\u00e4chspartner UUID (f\u00fcr /r)
|
||||||
private final Map<UUID, UUID> lastPartner = new ConcurrentHashMap<>();
|
private final Map<UUID, UUID> lastPartner = new ConcurrentHashMap<>();
|
||||||
|
|
||||||
// UUIDs die Social-Spy aktiviert haben
|
// UUIDs die Social-Spy aktiviert haben
|
||||||
@@ -36,7 +36,7 @@ public class PrivateMsgManager {
|
|||||||
* @param receiver Der empfangende Spieler
|
* @param receiver Der empfangende Spieler
|
||||||
* @param message Die Nachricht
|
* @param message Die Nachricht
|
||||||
* @param config Chat-Konfiguration (Formate)
|
* @param config Chat-Konfiguration (Formate)
|
||||||
* @param bypassPermission Permission für Admin-Bypass (kann nicht geblockt werden)
|
* @param bypassPermission Permission f\u00fcr Admin-Bypass (kann nicht geblockt werden)
|
||||||
* @return true wenn erfolgreich gesendet
|
* @return true wenn erfolgreich gesendet
|
||||||
*/
|
*/
|
||||||
public boolean send(ProxiedPlayer sender, ProxiedPlayer receiver,
|
public boolean send(ProxiedPlayer sender, ProxiedPlayer receiver,
|
||||||
@@ -84,7 +84,7 @@ public class PrivateMsgManager {
|
|||||||
sender.sendMessage(color(toSender));
|
sender.sendMessage(color(toSender));
|
||||||
receiver.sendMessage(color(toReceiver));
|
receiver.sendMessage(color(toReceiver));
|
||||||
|
|
||||||
// Letzte Partner speichern (für /r)
|
// Letzte Partner speichern (f\u00fcr /r)
|
||||||
lastPartner.put(sender.getUniqueId(), receiver.getUniqueId());
|
lastPartner.put(sender.getUniqueId(), receiver.getUniqueId());
|
||||||
lastPartner.put(receiver.getUniqueId(), sender.getUniqueId());
|
lastPartner.put(receiver.getUniqueId(), sender.getUniqueId());
|
||||||
|
|
||||||
@@ -97,7 +97,7 @@ public class PrivateMsgManager {
|
|||||||
|
|
||||||
/**
|
/**
|
||||||
* Antwort-Funktion (/r).
|
* Antwort-Funktion (/r).
|
||||||
* Sucht den letzten Gesprächspartner des Senders.
|
* Sucht den letzten Gespr\u00e4chspartner des Senders.
|
||||||
*/
|
*/
|
||||||
public void reply(ProxiedPlayer sender, String message, ChatConfig config, String bypassPermission) {
|
public void reply(ProxiedPlayer sender, String message, ChatConfig config, String bypassPermission) {
|
||||||
UUID partnerUuid = lastPartner.get(sender.getUniqueId());
|
UUID partnerUuid = lastPartner.get(sender.getUniqueId());
|
||||||
@@ -140,10 +140,10 @@ public class PrivateMsgManager {
|
|||||||
/**
|
/**
|
||||||
* Formatiert eine PM-Nachricht.
|
* Formatiert eine PM-Nachricht.
|
||||||
* {sender} → Name des Absenders
|
* {sender} → Name des Absenders
|
||||||
* {receiver} → Name des Empfängers
|
* {receiver} → Name des Empf\u00e4ngers
|
||||||
* {player} → Gesprächspartner aus Sicht des jeweiligen Empfängers:
|
* {player} → Gespr\u00e4chspartner aus Sicht des jeweiligen Empf\u00e4ngers:
|
||||||
* Beim Sender: der Empfänger (an wen schreibt er?)
|
* Beim Sender: der Empf\u00e4nger (an wen schreibt er?)
|
||||||
* Beim Empfänger: der Sender (von wem kommt es?)
|
* Beim Empf\u00e4nger: der Sender (von wem kommt es?)
|
||||||
*
|
*
|
||||||
* @param viewerIsSender true wenn der aktuelle Betrachter der Absender ist
|
* @param viewerIsSender true wenn der aktuelle Betrachter der Absender ist
|
||||||
*/
|
*/
|
||||||
@@ -153,7 +153,7 @@ public class PrivateMsgManager {
|
|||||||
return template
|
return template
|
||||||
.replace("{sender}", sender)
|
.replace("{sender}", sender)
|
||||||
.replace("{receiver}", receiver)
|
.replace("{receiver}", receiver)
|
||||||
.replace("{player}", partner) // Gesprächspartner aus Sicht des Betrachters
|
.replace("{player}", partner) // Gespr\u00e4chspartner aus Sicht des Betrachters
|
||||||
.replace("{message}", message);
|
.replace("{message}", message);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -11,11 +11,11 @@ import java.util.logging.Logger;
|
|||||||
* Verwaltet Spieler-Reports (/report).
|
* Verwaltet Spieler-Reports (/report).
|
||||||
*
|
*
|
||||||
* Reports werden mit einer eindeutigen ID (z.B. RPT-0001) gespeichert und
|
* Reports werden mit einer eindeutigen ID (z.B. RPT-0001) gespeichert und
|
||||||
* bleiben offen, bis ein Admin sie explizit mit /reportclose <ID> schließt.
|
* bleiben offen, bis ein Admin sie explizit mit /reportclose <ID> schlie\u00dft.
|
||||||
*
|
*
|
||||||
* Online-Admins werden sofort benachrichtigt.
|
* Online-Admins werden sofort benachrichtigt.
|
||||||
* Offline-Admins erhalten eine verzögerte Benachrichtigung beim nächsten Login
|
* Offline-Admins erhalten eine verz\u00f6gerte Benachrichtigung beim n\u00e4chsten Login
|
||||||
* (gesteuert von außen via getPendingNotificationFor()).
|
* (gesteuert von au\u00dfen via getPendingNotificationFor()).
|
||||||
*
|
*
|
||||||
* Speicherformat (chat_reports.dat):
|
* Speicherformat (chat_reports.dat):
|
||||||
* id|reporter|reporterUUID|reported|server|messageContext|reason|timestamp|closed|closedBy
|
* id|reporter|reporterUUID|reported|server|messageContext|reason|timestamp|closed|closedBy
|
||||||
@@ -28,7 +28,7 @@ public class ReportManager {
|
|||||||
/** Alle Reports (offen und geschlossen). */
|
/** Alle Reports (offen und geschlossen). */
|
||||||
private final ConcurrentHashMap<String, ChatReport> reports = new ConcurrentHashMap<>();
|
private final ConcurrentHashMap<String, ChatReport> reports = new ConcurrentHashMap<>();
|
||||||
|
|
||||||
/** Zähler für Report-IDs. Wird beim Laden synchronisiert. */
|
/** Z\u00e4hler f\u00fcr Report-IDs. Wird beim Laden synchronisiert. */
|
||||||
private final AtomicInteger idCounter = new AtomicInteger(0);
|
private final AtomicInteger idCounter = new AtomicInteger(0);
|
||||||
|
|
||||||
private static final SimpleDateFormat DATE_FMT = new SimpleDateFormat("dd.MM.yyyy HH:mm:ss");
|
private static final SimpleDateFormat DATE_FMT = new SimpleDateFormat("dd.MM.yyyy HH:mm:ss");
|
||||||
@@ -45,7 +45,7 @@ public class ReportManager {
|
|||||||
public String reason;
|
public String reason;
|
||||||
public long timestamp;
|
public long timestamp;
|
||||||
public boolean closed;
|
public boolean closed;
|
||||||
public String closedBy; // Name des schließenden Admins (oder leer)
|
public String closedBy; // Name des schlie\u00dfenden Admins (oder leer)
|
||||||
|
|
||||||
public String getFormattedTime() {
|
public String getFormattedTime() {
|
||||||
return DATE_FMT.format(new Date(timestamp));
|
return DATE_FMT.format(new Date(timestamp));
|
||||||
@@ -68,8 +68,8 @@ public class ReportManager {
|
|||||||
* @param reporterUUID UUID des meldenden Spielers
|
* @param reporterUUID UUID des meldenden Spielers
|
||||||
* @param reportedName Name des gemeldeten Spielers
|
* @param reportedName Name des gemeldeten Spielers
|
||||||
* @param server Server, auf dem sich der Reporter befand
|
* @param server Server, auf dem sich der Reporter befand
|
||||||
* @param messageContext Letzte bekannte Nachricht des Gemeldeten (für Kontext)
|
* @param messageContext Letzte bekannte Nachricht des Gemeldeten (f\u00fcr Kontext)
|
||||||
* @param reason Freitext-Begründung
|
* @param reason Freitext-Begr\u00fcndung
|
||||||
* @return die neue Report-ID (z.B. RPT-0001)
|
* @return die neue Report-ID (z.B. RPT-0001)
|
||||||
*/
|
*/
|
||||||
public String createReport(String reporterName, UUID reporterUUID,
|
public String createReport(String reporterName, UUID reporterUUID,
|
||||||
@@ -95,10 +95,10 @@ public class ReportManager {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Schließt einen Report.
|
* Schlie\u00dft einen Report.
|
||||||
*
|
*
|
||||||
* @param id Report-ID (z.B. RPT-0001, case-insensitiv)
|
* @param id Report-ID (z.B. RPT-0001, case-insensitiv)
|
||||||
* @param adminName Name des Admins, der den Report schließt
|
* @param adminName Name des Admins, der den Report schlie\u00dft
|
||||||
* @return true wenn erfolgreich geschlossen, false wenn nicht gefunden / bereits geschlossen
|
* @return true wenn erfolgreich geschlossen, false wenn nicht gefunden / bereits geschlossen
|
||||||
*/
|
*/
|
||||||
public boolean closeReport(String id, String adminName) {
|
public boolean closeReport(String id, String adminName) {
|
||||||
@@ -110,13 +110,13 @@ public class ReportManager {
|
|||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
/** Gibt einen Report nach ID zurück (case-insensitiv). */
|
/** Gibt einen Report nach ID zur\u00fcck (case-insensitiv). */
|
||||||
public ChatReport getReport(String id) {
|
public ChatReport getReport(String id) {
|
||||||
if (id == null) return null;
|
if (id == null) return null;
|
||||||
return reports.get(id.toUpperCase());
|
return reports.get(id.toUpperCase());
|
||||||
}
|
}
|
||||||
|
|
||||||
/** Gibt alle offenen Reports chronologisch (älteste zuerst) zurück. */
|
/** Gibt alle offenen Reports chronologisch (\u00e4lteste zuerst) zur\u00fcck. */
|
||||||
public List<ChatReport> getOpenReports() {
|
public List<ChatReport> getOpenReports() {
|
||||||
List<ChatReport> list = new ArrayList<>();
|
List<ChatReport> list = new ArrayList<>();
|
||||||
for (ChatReport r : reports.values()) {
|
for (ChatReport r : reports.values()) {
|
||||||
@@ -126,7 +126,7 @@ public class ReportManager {
|
|||||||
return list;
|
return list;
|
||||||
}
|
}
|
||||||
|
|
||||||
/** Gibt alle Reports chronologisch zurück (auch geschlossene). */
|
/** Gibt alle Reports chronologisch zur\u00fcck (auch geschlossene). */
|
||||||
public List<ChatReport> getAllReports() {
|
public List<ChatReport> getAllReports() {
|
||||||
List<ChatReport> list = new ArrayList<>(reports.values());
|
List<ChatReport> list = new ArrayList<>(reports.values());
|
||||||
list.sort(Comparator.comparingLong(r -> r.timestamp));
|
list.sort(Comparator.comparingLong(r -> r.timestamp));
|
||||||
@@ -192,7 +192,7 @@ public class ReportManager {
|
|||||||
r.closedBy = unesc(p[9]);
|
r.closedBy = unesc(p[9]);
|
||||||
reports.put(r.id.toUpperCase(), r);
|
reports.put(r.id.toUpperCase(), r);
|
||||||
|
|
||||||
// Zähler auf höchste bekannte Nummer synchronisieren
|
// Z\u00e4hler auf h\u00f6chste bekannte Nummer synchronisieren
|
||||||
if (r.id.toUpperCase().startsWith("RPT-")) {
|
if (r.id.toUpperCase().startsWith("RPT-")) {
|
||||||
try {
|
try {
|
||||||
int num = Integer.parseInt(r.id.substring(4));
|
int num = Integer.parseInt(r.id.substring(4));
|
||||||
@@ -208,7 +208,7 @@ public class ReportManager {
|
|||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// ===== Escape-Helfer (Pipe-Zeichen und Zeilenumbrüche escapen) =====
|
// ===== Escape-Helfer (Pipe-Zeichen und Zeilenumbr\u00fcche escapen) =====
|
||||||
|
|
||||||
private static String esc(String s) {
|
private static String esc(String s) {
|
||||||
if (s == null) return "";
|
if (s == null) return "";
|
||||||
|
|||||||
@@ -11,9 +11,9 @@ import java.util.concurrent.ConcurrentHashMap;
|
|||||||
* Zentrale Schnittstelle zwischen dem VanishModule und dem ChatModule.
|
* Zentrale Schnittstelle zwischen dem VanishModule und dem ChatModule.
|
||||||
*
|
*
|
||||||
* Das VanishModule (oder jedes andere Modul) ruft {@link #setVanished} auf
|
* Das VanishModule (oder jedes andere Modul) ruft {@link #setVanished} auf
|
||||||
* um Spieler als unsichtbar zu markieren. Das ChatModule prüft via
|
* um Spieler als unsichtbar zu markieren. Das ChatModule pr\u00fcft via
|
||||||
* {@link #isVanished} bevor es Join-/Leave-Nachrichten sendet oder
|
* {@link #isVanished} bevor es Join-/Leave-Nachrichten sendet oder
|
||||||
* Privat-Nachrichten zulässt.
|
* Privat-Nachrichten zul\u00e4sst.
|
||||||
*
|
*
|
||||||
* Verwendung im VanishModule:
|
* Verwendung im VanishModule:
|
||||||
* VanishProvider.setVanished(player.getUniqueId(), true); // beim Verschwinden
|
* VanishProvider.setVanished(player.getUniqueId(), true); // beim Verschwinden
|
||||||
@@ -64,7 +64,7 @@ public final class VanishProvider {
|
|||||||
return uuid != null && vanishedPlayers.contains(uuid);
|
return uuid != null && vanishedPlayers.contains(uuid);
|
||||||
}
|
}
|
||||||
|
|
||||||
/** Snapshot der aktuell unsichtbaren Spieler (für Debugging / Logs). */
|
/** Snapshot der aktuell unsichtbaren Spieler (f\u00fcr Debugging / Logs). */
|
||||||
public static Set<UUID> getVanishedPlayers() {
|
public static Set<UUID> getVanishedPlayers() {
|
||||||
return Collections.unmodifiableSet(vanishedPlayers);
|
return Collections.unmodifiableSet(vanishedPlayers);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -16,10 +16,10 @@ import java.util.concurrent.atomic.AtomicLong;
|
|||||||
import java.util.logging.Logger;
|
import java.util.logging.Logger;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Discord-Brücke für bidirektionale Kommunikation.
|
* Discord-Br\u00fccke f\u00fcr bidirektionale Kommunikation.
|
||||||
*
|
*
|
||||||
* Fix #12: extractJsonString() behandelt Escape-Sequenzen jetzt korrekt.
|
* Fix #12: extractJsonString() behandelt Escape-Sequenzen jetzt korrekt.
|
||||||
* Statt Zeichenvergleich mit dem Vorgänger-Char wird ein expliziter Escape-Flag verwendet.
|
* Statt Zeichenvergleich mit dem Vorg\u00e4nger-Char wird ein expliziter Escape-Flag verwendet.
|
||||||
*/
|
*/
|
||||||
public class DiscordBridge {
|
public class DiscordBridge {
|
||||||
|
|
||||||
@@ -49,7 +49,7 @@ public class DiscordBridge {
|
|||||||
running = true;
|
running = true;
|
||||||
int interval = Math.max(2, config.getDiscordPollInterval());
|
int interval = Math.max(2, config.getDiscordPollInterval());
|
||||||
plugin.getProxy().getScheduler().schedule(plugin, this::pollAllChannels, interval, interval, TimeUnit.SECONDS);
|
plugin.getProxy().getScheduler().schedule(plugin, this::pollAllChannels, interval, interval, TimeUnit.SECONDS);
|
||||||
logger.info("[ChatModule-Discord] Brücke gestartet (Poll-Intervall: " + interval + "s).");
|
logger.info("[ChatModule-Discord] Br\u00fccke gestartet (Poll-Intervall: " + interval + "s).");
|
||||||
}
|
}
|
||||||
|
|
||||||
public void stop() { running = false; }
|
public void stop() { running = false; }
|
||||||
@@ -141,8 +141,8 @@ public class DiscordBridge {
|
|||||||
String token = msg.content.substring(6).trim().toUpperCase();
|
String token = msg.content.substring(6).trim().toUpperCase();
|
||||||
if (linkManager != null) {
|
if (linkManager != null) {
|
||||||
AccountLinkManager.LinkedAccount acc = linkManager.redeemDiscord(token, msg.authorId, msg.authorName);
|
AccountLinkManager.LinkedAccount acc = linkManager.redeemDiscord(token, msg.authorId, msg.authorName);
|
||||||
if (acc != null) sendToChannel(channelId, "✅ Verknüpfung erfolgreich! Minecraft-Account: **" + acc.minecraftName + "**");
|
if (acc != null) sendToChannel(channelId, "✅ Verkn\u00fcpfung erfolgreich! Minecraft-Account: **" + acc.minecraftName + "**");
|
||||||
else sendToChannel(channelId, "❌ Ungültiger oder abgelaufener Token. Bitte `/discordlink` im Spiel erneut ausführen.");
|
else sendToChannel(channelId, "❌ Ung\u00fcltiger oder abgelaufener Token. Bitte `/discordlink` im Spiel erneut ausf\u00fchren.");
|
||||||
}
|
}
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
@@ -158,7 +158,7 @@ public class DiscordBridge {
|
|||||||
() -> ProxyServer.getInstance().broadcast(new TextComponent(formatted)));
|
() -> ProxyServer.getInstance().broadcast(new TextComponent(formatted)));
|
||||||
}
|
}
|
||||||
} catch (Exception e) {
|
} catch (Exception e) {
|
||||||
logger.fine("[ChatModule-Discord] Poll-Fehler für Kanal " + channelId + ": " + e.getMessage());
|
logger.fine("[ChatModule-Discord] Poll-Fehler f\u00fcr Kanal " + channelId + ": " + e.getMessage());
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -264,8 +264,8 @@ public class DiscordBridge {
|
|||||||
|
|
||||||
/**
|
/**
|
||||||
* FIX #12: Escape-Sequenzen werden korrekt mit einem Escape-Flag behandelt
|
* FIX #12: Escape-Sequenzen werden korrekt mit einem Escape-Flag behandelt
|
||||||
* statt den Vorgänger-Char zu vergleichen (der bei '\\' + '"' versagt).
|
* statt den Vorg\u00e4nger-Char zu vergleichen (der bei '\\' + '"' versagt).
|
||||||
* Gibt immer einen leeren String zurück wenn der Key nicht gefunden wird (nie null).
|
* Gibt immer einen leeren String zur\u00fcck wenn der Key nicht gefunden wird (nie null).
|
||||||
*/
|
*/
|
||||||
private String extractJsonString(String json, String key) {
|
private String extractJsonString(String json, String key) {
|
||||||
if (json == null || key == null) return "";
|
if (json == null || key == null) return "";
|
||||||
@@ -279,7 +279,7 @@ public class DiscordBridge {
|
|||||||
if (valStart >= json.length()) return "";
|
if (valStart >= json.length()) return "";
|
||||||
char first = json.charAt(valStart);
|
char first = json.charAt(valStart);
|
||||||
if (first == '"') {
|
if (first == '"') {
|
||||||
// FIX: Expliziter Escape-Flag statt Vorgänger-Char-Vergleich
|
// FIX: Expliziter Escape-Flag statt Vorg\u00e4nger-Char-Vergleich
|
||||||
int end = valStart + 1;
|
int end = valStart + 1;
|
||||||
boolean escaped = false;
|
boolean escaped = false;
|
||||||
while (end < json.length()) {
|
while (end < json.length()) {
|
||||||
|
|||||||
@@ -17,7 +17,7 @@ import java.util.concurrent.atomic.AtomicLong;
|
|||||||
import java.util.logging.Logger;
|
import java.util.logging.Logger;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Telegram-Brücke für bidirektionale Kommunikation.
|
* Telegram-Br\u00fccke f\u00fcr bidirektionale Kommunikation.
|
||||||
*
|
*
|
||||||
* Minecraft → Telegram: Via Bot API (sendMessage)
|
* Minecraft → Telegram: Via Bot API (sendMessage)
|
||||||
* Telegram → Minecraft: Via Long-Polling (getUpdates)
|
* Telegram → Minecraft: Via Long-Polling (getUpdates)
|
||||||
@@ -25,8 +25,8 @@ import java.util.logging.Logger;
|
|||||||
* Voraussetzungen:
|
* Voraussetzungen:
|
||||||
* - Telegram Bot via @BotFather erstellen
|
* - Telegram Bot via @BotFather erstellen
|
||||||
* - Bot-Token in chat.yml eintragen
|
* - Bot-Token in chat.yml eintragen
|
||||||
* - Bot in die gewünschten Gruppen/Kanäle einladen
|
* - Bot in die gew\u00fcnschten Gruppen/Kan\u00e4le einladen
|
||||||
* - Bot zu Admin machen (für Gruppen-Nachrichten empfangen)
|
* - Bot zu Admin machen (f\u00fcr Gruppen-Nachrichten empfangen)
|
||||||
*/
|
*/
|
||||||
public class TelegramBridge {
|
public class TelegramBridge {
|
||||||
|
|
||||||
@@ -37,7 +37,7 @@ public class TelegramBridge {
|
|||||||
private final Logger logger;
|
private final Logger logger;
|
||||||
private AccountLinkManager linkManager; // wird nach dem Start gesetzt
|
private AccountLinkManager linkManager; // wird nach dem Start gesetzt
|
||||||
|
|
||||||
// Letztes verarbeitetes Update-ID (für getUpdates Offset)
|
// Letztes verarbeitetes Update-ID (f\u00fcr getUpdates Offset)
|
||||||
private final AtomicLong lastUpdateId = new AtomicLong(0L);
|
private final AtomicLong lastUpdateId = new AtomicLong(0L);
|
||||||
|
|
||||||
private volatile boolean running = false;
|
private volatile boolean running = false;
|
||||||
@@ -67,7 +67,7 @@ public class TelegramBridge {
|
|||||||
plugin.getProxy().getScheduler().schedule(plugin, this::pollUpdates,
|
plugin.getProxy().getScheduler().schedule(plugin, this::pollUpdates,
|
||||||
interval, interval, TimeUnit.SECONDS);
|
interval, interval, TimeUnit.SECONDS);
|
||||||
|
|
||||||
logger.info("[ChatModule-Telegram] Brücke gestartet (Poll-Intervall: " + interval + "s).");
|
logger.info("[ChatModule-Telegram] Br\u00fccke gestartet (Poll-Intervall: " + interval + "s).");
|
||||||
}
|
}
|
||||||
|
|
||||||
public void stop() {
|
public void stop() {
|
||||||
@@ -78,7 +78,7 @@ public class TelegramBridge {
|
|||||||
|
|
||||||
/**
|
/**
|
||||||
* Sendet eine Nachricht an eine Telegram-Chat-ID.
|
* Sendet eine Nachricht an eine Telegram-Chat-ID.
|
||||||
* Unterstützt Themen-Gruppen via message_thread_id.
|
* Unterst\u00fctzt Themen-Gruppen via message_thread_id.
|
||||||
*/
|
*/
|
||||||
public void sendToTelegram(String chatId, String message) {
|
public void sendToTelegram(String chatId, String message) {
|
||||||
sendToTelegram(chatId, 0, message);
|
sendToTelegram(chatId, 0, message);
|
||||||
@@ -107,7 +107,7 @@ public class TelegramBridge {
|
|||||||
|
|
||||||
/**
|
/**
|
||||||
* Sendet eine formatierte HelpOp/Broadcast-Nachricht an Telegram.
|
* Sendet eine formatierte HelpOp/Broadcast-Nachricht an Telegram.
|
||||||
* Unterstützt Themen-Gruppen via message_thread_id.
|
* Unterst\u00fctzt Themen-Gruppen via message_thread_id.
|
||||||
*/
|
*/
|
||||||
public void sendFormattedToTelegram(String chatId, String header, String content) {
|
public void sendFormattedToTelegram(String chatId, String header, String content) {
|
||||||
sendFormattedToTelegram(chatId, 0, header, content);
|
sendFormattedToTelegram(chatId, 0, header, content);
|
||||||
@@ -167,7 +167,7 @@ public class TelegramBridge {
|
|||||||
if (update.text == null || update.text.isEmpty()) continue;
|
if (update.text == null || update.text.isEmpty()) continue;
|
||||||
if (update.isBot) continue;
|
if (update.isBot) continue;
|
||||||
|
|
||||||
// ── Token-Einlösung: /link <TOKEN> ──
|
// ── Token-Einl\u00f6sung: /link <TOKEN> ──
|
||||||
if (update.text.startsWith("/link ") || update.text.startsWith("/link@")) {
|
if (update.text.startsWith("/link ") || update.text.startsWith("/link@")) {
|
||||||
String[] parts = update.text.split("\\s+", 2);
|
String[] parts = update.text.split("\\s+", 2);
|
||||||
if (parts.length == 2 && linkManager != null) {
|
if (parts.length == 2 && linkManager != null) {
|
||||||
@@ -176,11 +176,11 @@ public class TelegramBridge {
|
|||||||
linkManager.redeemTelegram(token, update.fromId, update.fromName);
|
linkManager.redeemTelegram(token, update.fromId, update.fromName);
|
||||||
if (acc != null) {
|
if (acc != null) {
|
||||||
sendToTelegram(update.chatId, update.threadId,
|
sendToTelegram(update.chatId, update.threadId,
|
||||||
"✅ Verknüpfung erfolgreich! Minecraft-Account: <b>"
|
"✅ Verkn\u00fcpfung erfolgreich! Minecraft-Account: <b>"
|
||||||
+ escapeHtml(acc.minecraftName) + "</b>");
|
+ escapeHtml(acc.minecraftName) + "</b>");
|
||||||
} else {
|
} else {
|
||||||
sendToTelegram(update.chatId, update.threadId,
|
sendToTelegram(update.chatId, update.threadId,
|
||||||
"❌ Ungültiger oder abgelaufener Token. Bitte /telegramlink im Spiel erneut ausführen.");
|
"❌ Ung\u00fcltiger oder abgelaufener Token. Bitte /telegramlink im Spiel erneut ausf\u00fchren.");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
continue; // Nicht als Chat-Nachricht weiterleiten
|
continue; // Nicht als Chat-Nachricht weiterleiten
|
||||||
@@ -189,17 +189,17 @@ public class TelegramBridge {
|
|||||||
// Bot-Befehle ignorieren
|
// Bot-Befehle ignorieren
|
||||||
if (update.text.startsWith("/")) continue;
|
if (update.text.startsWith("/")) continue;
|
||||||
|
|
||||||
// ── Account-Name auflösen ──
|
// ── Account-Name aufl\u00f6sen ──
|
||||||
String displayName = (linkManager != null)
|
String displayName = (linkManager != null)
|
||||||
? linkManager.resolveTelegramName(update.fromId, update.fromName)
|
? linkManager.resolveTelegramName(update.fromId, update.fromName)
|
||||||
: update.fromName;
|
: update.fromName;
|
||||||
|
|
||||||
// Welchem Minecraft-Kanal gehört diese Telegram-Chat-ID + Thread?
|
// Welchem Minecraft-Kanal geh\u00f6rt diese Telegram-Chat-ID + Thread?
|
||||||
final boolean isAdminChat = update.chatId.equals(config.getTelegramAdminChatId())
|
final boolean isAdminChat = update.chatId.equals(config.getTelegramAdminChatId())
|
||||||
&& (config.getTelegramAdminTopicId() == 0
|
&& (config.getTelegramAdminTopicId() == 0
|
||||||
|| config.getTelegramAdminTopicId() == update.threadId);
|
|| config.getTelegramAdminTopicId() == update.threadId);
|
||||||
|
|
||||||
// Prüfen ob die Nachricht zu einem konfigurierten Kanal-Thema gehört
|
// Pr\u00fcfen ob die Nachricht zu einem konfigurierten Kanal-Thema geh\u00f6rt
|
||||||
final boolean matchesChannel = isAdminChat || matchesTelegramChannel(update);
|
final boolean matchesChannel = isAdminChat || matchesTelegramChannel(update);
|
||||||
|
|
||||||
if (!matchesChannel && !isAdminChat) continue;
|
if (!matchesChannel && !isAdminChat) continue;
|
||||||
@@ -257,11 +257,11 @@ public class TelegramBridge {
|
|||||||
private static class TelegramUpdate {
|
private static class TelegramUpdate {
|
||||||
long updateId;
|
long updateId;
|
||||||
String chatId = "";
|
String chatId = "";
|
||||||
String fromId = ""; // Telegram User-ID (für Account-Link)
|
String fromId = ""; // Telegram User-ID (f\u00fcr Account-Link)
|
||||||
String fromName = "";
|
String fromName = "";
|
||||||
String text = "";
|
String text = "";
|
||||||
boolean isBot = false;
|
boolean isBot = false;
|
||||||
int threadId = 0; // message_thread_id für Themen-Gruppen (0 = kein Thema)
|
int threadId = 0; // message_thread_id f\u00fcr Themen-Gruppen (0 = kein Thema)
|
||||||
}
|
}
|
||||||
|
|
||||||
private java.util.List<TelegramUpdate> parseUpdates(String json) {
|
private java.util.List<TelegramUpdate> parseUpdates(String json) {
|
||||||
@@ -289,11 +289,11 @@ public class TelegramBridge {
|
|||||||
return result;
|
return result;
|
||||||
}
|
}
|
||||||
|
|
||||||
/** Prüft ob ein Update zu einem konfigurierten Kanal-Thema gehört. */
|
/** Pr\u00fcft ob ein Update zu einem konfigurierten Kanal-Thema geh\u00f6rt. */
|
||||||
private boolean matchesTelegramChannel(TelegramUpdate update) {
|
private boolean matchesTelegramChannel(TelegramUpdate update) {
|
||||||
for (net.viper.status.modules.chat.ChatChannel ch : config.getChannels().values()) {
|
for (net.viper.status.modules.chat.ChatChannel ch : config.getChannels().values()) {
|
||||||
if (!ch.getTelegramChatId().equals(update.chatId)) continue;
|
if (!ch.getTelegramChatId().equals(update.chatId)) continue;
|
||||||
// Thema konfiguriert? → Thread-ID muss übereinstimmen
|
// Thema konfiguriert? → Thread-ID muss \u00fcbereinstimmen
|
||||||
if (ch.getTelegramThreadId() > 0 && ch.getTelegramThreadId() != update.threadId) continue;
|
if (ch.getTelegramThreadId() > 0 && ch.getTelegramThreadId() != update.threadId) continue;
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -23,7 +23,7 @@ import org.yaml.snakeyaml.Yaml;
|
|||||||
public class CommandBlockerModule implements Module, Listener {
|
public class CommandBlockerModule implements Module, Listener {
|
||||||
|
|
||||||
private StatusAPI plugin;
|
private StatusAPI plugin;
|
||||||
private boolean enabled = true; // Standardmäßig aktiv
|
private boolean enabled = true; // Standardm\u00e4\u00dfig aktiv
|
||||||
private String bypassPermission = "commandblocker.bypass"; // Standard Permission
|
private String bypassPermission = "commandblocker.bypass"; // Standard Permission
|
||||||
|
|
||||||
private File file;
|
private File file;
|
||||||
|
|||||||
@@ -20,7 +20,7 @@ import net.md_5.bungee.api.connection.ProxiedPlayer;
|
|||||||
import net.md_5.bungee.api.event.ChatEvent;
|
import net.md_5.bungee.api.event.ChatEvent;
|
||||||
import net.md_5.bungee.api.plugin.Command;
|
import net.md_5.bungee.api.plugin.Command;
|
||||||
import net.md_5.bungee.api.plugin.Listener;
|
import net.md_5.bungee.api.plugin.Listener;
|
||||||
import net.md_5.bungee.api.plugin.Plugin; // Import für das Interface Argument
|
import net.md_5.bungee.api.plugin.Plugin; // Import f\u00fcr das Interface Argument
|
||||||
import net.md_5.bungee.config.Configuration;
|
import net.md_5.bungee.config.Configuration;
|
||||||
import net.md_5.bungee.config.ConfigurationProvider;
|
import net.md_5.bungee.config.ConfigurationProvider;
|
||||||
import net.md_5.bungee.config.YamlConfiguration;
|
import net.md_5.bungee.config.YamlConfiguration;
|
||||||
@@ -109,8 +109,8 @@ public class CustomCommandModule implements Module, Listener {
|
|||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void onDisable(Plugin plugin) {
|
public void onDisable(Plugin plugin) {
|
||||||
// Optional: Cleanup logic, falls nötig.
|
// Optional: Cleanup logic, falls n\u00f6tig.
|
||||||
// Wir nutzen hier das übergebene 'plugin' Argument (oder this.plugin, ist egal)
|
// Wir nutzen hier das \u00fcbergebene 'plugin' Argument (oder this.plugin, ist egal)
|
||||||
// Listener und Commands werden automatisch entfernt, wenn das Plugin stoppt.
|
// Listener und Commands werden automatisch entfernt, wenn das Plugin stoppt.
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -6,7 +6,7 @@ import net.md_5.bungee.api.plugin.Plugin;
|
|||||||
|
|
||||||
/**
|
/**
|
||||||
* /ecoadmin – wird NICHT mehr auf BungeeCord registriert.
|
* /ecoadmin – wird NICHT mehr auf BungeeCord registriert.
|
||||||
* NexEco /eco auf dem Spigot-Server übernimmt Admin-Befehle.
|
* NexEco /eco auf dem Spigot-Server \u00fcbernimmt Admin-Befehle.
|
||||||
*/
|
*/
|
||||||
public class EcoAdminCommand extends Command {
|
public class EcoAdminCommand extends Command {
|
||||||
|
|
||||||
|
|||||||
@@ -14,8 +14,8 @@ import java.util.logging.Logger;
|
|||||||
*
|
*
|
||||||
* Fixes:
|
* Fixes:
|
||||||
* - balance-Spalte als DOUBLE(30,2) statt VARCHAR → kompatibel mit NexEco & SurvivalPlus
|
* - balance-Spalte als DOUBLE(30,2) statt VARCHAR → kompatibel mit NexEco & SurvivalPlus
|
||||||
* - atomare Transaktion für withdraw+deposit → kein Geldverlust bei Absturz
|
* - atomare Transaktion f\u00fcr withdraw+deposit → kein Geldverlust bei Absturz
|
||||||
* - FOR UPDATE Lock → kein Race-Condition-Bug bei gleichzeitigen Überweisungen
|
* - FOR UPDATE Lock → kein Race-Condition-Bug bei gleichzeitigen \u00dcberweisungen
|
||||||
*/
|
*/
|
||||||
public class EconomyDatabase {
|
public class EconomyDatabase {
|
||||||
|
|
||||||
@@ -67,7 +67,7 @@ public class EconomyDatabase {
|
|||||||
"MODIFY COLUMN `balance` DOUBLE(30,2) NOT NULL DEFAULT 0.00"
|
"MODIFY COLUMN `balance` DOUBLE(30,2) NOT NULL DEFAULT 0.00"
|
||||||
);
|
);
|
||||||
} catch (SQLException e) {
|
} catch (SQLException e) {
|
||||||
// ALTER schlägt fehl wenn Typ bereits korrekt ist – kein Problem
|
// ALTER schl\u00e4gt fehl wenn Typ bereits korrekt ist – kein Problem
|
||||||
if (!e.getMessage().contains("Duplicate") && !e.getMessage().contains("doesn't exist")) {
|
if (!e.getMessage().contains("Duplicate") && !e.getMessage().contains("doesn't exist")) {
|
||||||
log.warning("[Economy] Tabellen-Setup bc_accounts: " + e.getMessage());
|
log.warning("[Economy] Tabellen-Setup bc_accounts: " + e.getMessage());
|
||||||
}
|
}
|
||||||
@@ -98,7 +98,7 @@ public class EconomyDatabase {
|
|||||||
|
|
||||||
// ── Kontostand ────────────────────────────────────────────────────────────
|
// ── Kontostand ────────────────────────────────────────────────────────────
|
||||||
|
|
||||||
/** Lädt den Kontostand direkt aus der DB. Gibt -1 zurück wenn kein Eintrag. */
|
/** L\u00e4dt den Kontostand direkt aus der DB. Gibt -1 zur\u00fcck wenn kein Eintrag. */
|
||||||
public double load(UUID uuid) {
|
public double load(UUID uuid) {
|
||||||
if (!isConnected()) return -1;
|
if (!isConnected()) return -1;
|
||||||
try (Connection con = dataSource.getConnection();
|
try (Connection con = dataSource.getConnection();
|
||||||
@@ -109,7 +109,7 @@ public class EconomyDatabase {
|
|||||||
if (rs.next()) return rs.getDouble("balance");
|
if (rs.next()) return rs.getDouble("balance");
|
||||||
}
|
}
|
||||||
} catch (SQLException e) {
|
} catch (SQLException e) {
|
||||||
log.warning("[Economy] Load fehlgeschlagen für " + uuid + ": " + e.getMessage());
|
log.warning("[Economy] Load fehlgeschlagen f\u00fcr " + uuid + ": " + e.getMessage());
|
||||||
}
|
}
|
||||||
return -1;
|
return -1;
|
||||||
}
|
}
|
||||||
@@ -125,14 +125,14 @@ public class EconomyDatabase {
|
|||||||
ps.setDouble(2, balance);
|
ps.setDouble(2, balance);
|
||||||
ps.executeUpdate();
|
ps.executeUpdate();
|
||||||
} catch (SQLException e) {
|
} catch (SQLException e) {
|
||||||
log.warning("[Economy] Save fehlgeschlagen für " + uuid + ": " + e.getMessage());
|
log.warning("[Economy] Save fehlgeschlagen f\u00fcr " + uuid + ": " + e.getMessage());
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Atomare Überweisung von → to.
|
* Atomare \u00dcberweisung von → to.
|
||||||
* Nutzt eine SQL-Transaktion mit FOR UPDATE Lock – race-condition-sicher.
|
* Nutzt eine SQL-Transaktion mit FOR UPDATE Lock – race-condition-sicher.
|
||||||
* Gibt false zurück wenn Sender nicht genug Guthaben hat.
|
* Gibt false zur\u00fcck wenn Sender nicht genug Guthaben hat.
|
||||||
*/
|
*/
|
||||||
public boolean transfer(UUID from, UUID to, double amount, double startBalance) {
|
public boolean transfer(UUID from, UUID to, double amount, double startBalance) {
|
||||||
if (!isConnected()) return false;
|
if (!isConnected()) return false;
|
||||||
@@ -162,7 +162,7 @@ public class EconomyDatabase {
|
|||||||
ps.executeUpdate();
|
ps.executeUpdate();
|
||||||
}
|
}
|
||||||
|
|
||||||
// Empfänger gutschreiben (Konto anlegen falls nötig)
|
// Empf\u00e4nger gutschreiben (Konto anlegen falls n\u00f6tig)
|
||||||
try (PreparedStatement ps = con.prepareStatement(
|
try (PreparedStatement ps = con.prepareStatement(
|
||||||
"INSERT INTO `" + TABLE + "` (`player_name`, `balance`) VALUES (?, ?) " +
|
"INSERT INTO `" + TABLE + "` (`player_name`, `balance`) VALUES (?, ?) " +
|
||||||
"ON DUPLICATE KEY UPDATE `balance` = `balance` + ?")) {
|
"ON DUPLICATE KEY UPDATE `balance` = `balance` + ?")) {
|
||||||
|
|||||||
@@ -9,20 +9,20 @@ import net.md_5.bungee.event.EventHandler;
|
|||||||
import net.viper.status.StatusAPI;
|
import net.viper.status.StatusAPI;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* EconomyListener – nur noch Aufräumen der playerBalances Map.
|
* EconomyListener – nur noch Aufr\u00e4umen der playerBalances Map.
|
||||||
*
|
*
|
||||||
* Das Befüllen der Map geschieht ausschließlich durch die StatusAPIBridge
|
* Das Bef\u00fcllen der Map geschieht ausschlie\u00dflich durch die StatusAPIBridge
|
||||||
* (Spigot) die über Vault/NexEco den Kontostand per HTTP an die StatusAPI sendet.
|
* (Spigot) die \u00fcber Vault/NexEco den Kontostand per HTTP an die StatusAPI sendet.
|
||||||
*/
|
*/
|
||||||
public class EconomyListener implements Listener {
|
public class EconomyListener implements Listener {
|
||||||
|
|
||||||
public EconomyListener(Plugin plugin, EconomyManager manager) {
|
public EconomyListener(Plugin plugin, EconomyManager manager) {
|
||||||
// EconomyManager wird nicht mehr benötigt
|
// EconomyManager wird nicht mehr ben\u00f6tigt
|
||||||
}
|
}
|
||||||
|
|
||||||
@EventHandler
|
@EventHandler
|
||||||
public void onLogin(PostLoginEvent event) {
|
public void onLogin(PostLoginEvent event) {
|
||||||
// Wird von StatusAPIBridge befüllt – nichts zu tun beim Login
|
// Wird von StatusAPIBridge bef\u00fcllt – nichts zu tun beim Login
|
||||||
}
|
}
|
||||||
|
|
||||||
@EventHandler
|
@EventHandler
|
||||||
@@ -32,6 +32,6 @@ public class EconomyListener implements Listener {
|
|||||||
}
|
}
|
||||||
|
|
||||||
public void cancelTasks() {
|
public void cancelTasks() {
|
||||||
// Kein periodischer Task mehr nötig
|
// Kein periodischer Task mehr n\u00f6tig
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -5,7 +5,7 @@ import java.util.UUID;
|
|||||||
|
|
||||||
/**
|
/**
|
||||||
* EconomyManager – Stub, nicht mehr aktiv.
|
* EconomyManager – Stub, nicht mehr aktiv.
|
||||||
* Economy wird ausschließlich über NexEco (Spigot) verwaltet.
|
* Economy wird ausschlie\u00dflich \u00fcber NexEco (Spigot) verwaltet.
|
||||||
*/
|
*/
|
||||||
public class EconomyManager {
|
public class EconomyManager {
|
||||||
|
|
||||||
|
|||||||
@@ -6,13 +6,13 @@ import net.viper.status.module.Module;
|
|||||||
/**
|
/**
|
||||||
* EconomyModule – DEAKTIVIERT.
|
* EconomyModule – DEAKTIVIERT.
|
||||||
*
|
*
|
||||||
* Die Economy wird ausschließlich über NexEco (Spigot) verwaltet.
|
* Die Economy wird ausschlie\u00dflich \u00fcber NexEco (Spigot) verwaltet.
|
||||||
* Die StatusAPIBridge (Spigot-Plugin) liest den Kontostand über Vault/NexEco
|
* Die StatusAPIBridge (Spigot-Plugin) liest den Kontostand \u00fcber Vault/NexEco
|
||||||
* und pushed ihn per HTTP an die StatusAPI → playerBalances Map.
|
* und pushed ihn per HTTP an die StatusAPI → playerBalances Map.
|
||||||
*
|
*
|
||||||
* Damit gibt es nur EINE Datenquelle für Kontostände: NexEco / money_accounts.
|
* Damit gibt es nur EINE Datenquelle f\u00fcr Kontost\u00e4nde: NexEco / money_accounts.
|
||||||
* Das alte EconomyModule schrieb in bc_accounts – das führte zu doppelten,
|
* Das alte EconomyModule schrieb in bc_accounts – das f\u00fchrte zu doppelten,
|
||||||
* inkonsistenten Kontoständen.
|
* inkonsistenten Kontost\u00e4nden.
|
||||||
*/
|
*/
|
||||||
public class EconomyModule implements Module {
|
public class EconomyModule implements Module {
|
||||||
|
|
||||||
@@ -21,8 +21,8 @@ public class EconomyModule implements Module {
|
|||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void onEnable(Plugin plugin) {
|
public void onEnable(Plugin plugin) {
|
||||||
plugin.getLogger().info("[Economy] EconomyModule ist deaktiviert – NexEco ist zuständig.");
|
plugin.getLogger().info("[Economy] EconomyModule ist deaktiviert – NexEco ist zust\u00e4ndig.");
|
||||||
plugin.getLogger().info("[Economy] Kontostände kommen via StatusAPIBridge (Vault → NexEco → HTTP).");
|
plugin.getLogger().info("[Economy] Kontost\u00e4nde kommen via StatusAPIBridge (Vault → NexEco → HTTP).");
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
|
|||||||
@@ -6,8 +6,8 @@ import net.md_5.bungee.api.plugin.Plugin;
|
|||||||
|
|
||||||
/**
|
/**
|
||||||
* /pay – wird NICHT mehr auf BungeeCord registriert.
|
* /pay – wird NICHT mehr auf BungeeCord registriert.
|
||||||
* NexEco auf dem Spigot-Server übernimmt /pay direkt.
|
* NexEco auf dem Spigot-Server \u00fcbernimmt /pay direkt.
|
||||||
* Diese Klasse existiert nur noch für Kompilier-Kompatibilität.
|
* Diese Klasse existiert nur noch f\u00fcr Kompilier-Kompatibilit\u00e4t.
|
||||||
*/
|
*/
|
||||||
public class PayCommand extends Command {
|
public class PayCommand extends Command {
|
||||||
|
|
||||||
|
|||||||
@@ -39,8 +39,8 @@ import java.util.concurrent.TimeUnit;
|
|||||||
/**
|
/**
|
||||||
* ForumBridgeModule: Verbindet das WP Business Forum mit dem Minecraft-Server.
|
* ForumBridgeModule: Verbindet das WP Business Forum mit dem Minecraft-Server.
|
||||||
*
|
*
|
||||||
* Fix #13: extractJsonString() gibt jetzt immer einen leeren String statt null zurück.
|
* Fix #13: extractJsonString() gibt jetzt immer einen leeren String statt null zur\u00fcck.
|
||||||
* Alle Aufrufer müssen nicht mehr auf null prüfen, was NullPointerExceptions verhindert.
|
* Alle Aufrufer m\u00fcssen nicht mehr auf null pr\u00fcfen, was NullPointerExceptions verhindert.
|
||||||
*/
|
*/
|
||||||
public class ForumBridgeModule implements Module, Listener {
|
public class ForumBridgeModule implements Module, Listener {
|
||||||
|
|
||||||
@@ -106,7 +106,7 @@ public class ForumBridgeModule implements Module, Listener {
|
|||||||
return "{\"success\":false,\"error\":\"unauthorized\"}";
|
return "{\"success\":false,\"error\":\"unauthorized\"}";
|
||||||
}
|
}
|
||||||
|
|
||||||
// FIX #13: extractJsonString gibt "" statt null → kein NullPointerException möglich
|
// FIX #13: extractJsonString gibt "" statt null → kein NullPointerException m\u00f6glich
|
||||||
String playerUuid = extractJsonString(body, "player_uuid");
|
String playerUuid = extractJsonString(body, "player_uuid");
|
||||||
String type = extractJsonString(body, "type");
|
String type = extractJsonString(body, "type");
|
||||||
String title = extractJsonString(body, "title");
|
String title = extractJsonString(body, "title");
|
||||||
@@ -122,7 +122,7 @@ public class ForumBridgeModule implements Module, Listener {
|
|||||||
if ("thread".equalsIgnoreCase(type) && title.toLowerCase().contains("umfrage")) type = "poll";
|
if ("thread".equalsIgnoreCase(type) && title.toLowerCase().contains("umfrage")) type = "poll";
|
||||||
if (type.isEmpty()) type = "reply";
|
if (type.isEmpty()) type = "reply";
|
||||||
|
|
||||||
// Alle Werte sind garantiert nicht null (extractJsonString gibt "" zurück)
|
// Alle Werte sind garantiert nicht null (extractJsonString gibt "" zur\u00fcck)
|
||||||
ForumNotification notification = new ForumNotification(uuid, type, title, author, url);
|
ForumNotification notification = new ForumNotification(uuid, type, title, author, url);
|
||||||
|
|
||||||
ProxiedPlayer online = ProxyServer.getInstance().getPlayer(uuid);
|
ProxiedPlayer online = ProxyServer.getInstance().getPlayer(uuid);
|
||||||
@@ -160,7 +160,7 @@ public class ForumBridgeModule implements Module, Listener {
|
|||||||
TextComponent link = new TextComponent("§a ➜ Im Forum ansehen");
|
TextComponent link = new TextComponent("§a ➜ Im Forum ansehen");
|
||||||
link.setClickEvent(new ClickEvent(ClickEvent.Action.OPEN_URL, notif.getUrl()));
|
link.setClickEvent(new ClickEvent(ClickEvent.Action.OPEN_URL, notif.getUrl()));
|
||||||
link.setHoverEvent(new HoverEvent(HoverEvent.Action.SHOW_TEXT,
|
link.setHoverEvent(new HoverEvent(HoverEvent.Action.SHOW_TEXT,
|
||||||
new ComponentBuilder("§7Klicke um den Beitrag im Forum zu öffnen").create()));
|
new ComponentBuilder("§7Klicke um den Beitrag im Forum zu \u00f6ffnen").create()));
|
||||||
player.sendMessage(link);
|
player.sendMessage(link);
|
||||||
}
|
}
|
||||||
player.sendMessage(new TextComponent("§8§m "));
|
player.sendMessage(new TextComponent("§8§m "));
|
||||||
@@ -197,16 +197,16 @@ public class ForumBridgeModule implements Module, Listener {
|
|||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void execute(CommandSender sender, String[] args) {
|
public void execute(CommandSender sender, String[] args) {
|
||||||
if (!(sender instanceof ProxiedPlayer)) { sender.sendMessage(new TextComponent("§cNur Spieler können diesen Befehl benutzen.")); return; }
|
if (!(sender instanceof ProxiedPlayer)) { sender.sendMessage(new TextComponent("§cNur Spieler k\u00f6nnen diesen Befehl benutzen.")); return; }
|
||||||
ProxiedPlayer p = (ProxiedPlayer) sender;
|
ProxiedPlayer p = (ProxiedPlayer) sender;
|
||||||
if (args.length != 1) {
|
if (args.length != 1) {
|
||||||
p.sendMessage(new TextComponent("§eBenutzung: §f/forumlink <token>"));
|
p.sendMessage(new TextComponent("§eBenutzung: §f/forumlink <token>"));
|
||||||
p.sendMessage(new TextComponent("§7Den Token erhältst du in deinem Forum-Profil unter §fMinecraft-Verknüpfung§7."));
|
p.sendMessage(new TextComponent("§7Den Token erh\u00e4ltst du in deinem Forum-Profil unter §fMinecraft-Verkn\u00fcpfung§7."));
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
String token = args[0].trim().toUpperCase();
|
String token = args[0].trim().toUpperCase();
|
||||||
if (wpBaseUrl.isEmpty()) { p.sendMessage(new TextComponent("§cForum-Verknüpfung ist nicht konfiguriert.")); return; }
|
if (wpBaseUrl.isEmpty()) { p.sendMessage(new TextComponent("§cForum-Verkn\u00fcpfung ist nicht konfiguriert.")); return; }
|
||||||
p.sendMessage(new TextComponent("§7Überprüfe Token..."));
|
p.sendMessage(new TextComponent("§7\u00dcberpr\u00fcfe Token..."));
|
||||||
|
|
||||||
plugin.getProxy().getScheduler().runAsync(plugin, () -> {
|
plugin.getProxy().getScheduler().runAsync(plugin, () -> {
|
||||||
try {
|
try {
|
||||||
@@ -236,17 +236,17 @@ public class ForumBridgeModule implements Module, Listener {
|
|||||||
String username = extractJsonString(resp, "username");
|
String username = extractJsonString(resp, "username");
|
||||||
String show = !displayName.isEmpty() ? displayName : username;
|
String show = !displayName.isEmpty() ? displayName : username;
|
||||||
p.sendMessage(new TextComponent("§8§m "));
|
p.sendMessage(new TextComponent("§8§m "));
|
||||||
p.sendMessage(new TextComponent("§a§l✓ §fForum-Account erfolgreich verknüpft!"));
|
p.sendMessage(new TextComponent("§a§l✓ §fForum-Account erfolgreich verkn\u00fcpft!"));
|
||||||
if (!show.isEmpty()) p.sendMessage(new TextComponent("§7 Forum-User: §f" + show));
|
if (!show.isEmpty()) p.sendMessage(new TextComponent("§7 Forum-User: §f" + show));
|
||||||
p.sendMessage(new TextComponent("§7 Du erhältst jetzt Ingame-Benachrichtigungen."));
|
p.sendMessage(new TextComponent("§7 Du erh\u00e4ltst jetzt Ingame-Benachrichtigungen."));
|
||||||
p.sendMessage(new TextComponent("§8§m "));
|
p.sendMessage(new TextComponent("§8§m "));
|
||||||
} else {
|
} else {
|
||||||
String error = extractJsonString(resp, "error");
|
String error = extractJsonString(resp, "error");
|
||||||
String message = extractJsonString(resp, "message");
|
String message = extractJsonString(resp, "message");
|
||||||
if ("token_expired".equals(error)) p.sendMessage(new TextComponent("§c✗ Der Token ist abgelaufen."));
|
if ("token_expired".equals(error)) p.sendMessage(new TextComponent("§c✗ Der Token ist abgelaufen."));
|
||||||
else if ("uuid_already_linked".equals(error)) p.sendMessage(new TextComponent("§c✗ " + (!message.isEmpty() ? message : "Diese UUID ist bereits verknüpft.")));
|
else if ("uuid_already_linked".equals(error)) p.sendMessage(new TextComponent("§c✗ " + (!message.isEmpty() ? message : "Diese UUID ist bereits verkn\u00fcpft.")));
|
||||||
else if ("invalid_token".equals(error)) p.sendMessage(new TextComponent("§c✗ Ungültiger Token."));
|
else if ("invalid_token".equals(error)) p.sendMessage(new TextComponent("§c✗ Ung\u00fcltiger Token."));
|
||||||
else p.sendMessage(new TextComponent("§c✗ Verknüpfung fehlgeschlagen: " + (!error.isEmpty() ? error : "Unbekannter Fehler")));
|
else p.sendMessage(new TextComponent("§c✗ Verkn\u00fcpfung fehlgeschlagen: " + (!error.isEmpty() ? error : "Unbekannter Fehler")));
|
||||||
}
|
}
|
||||||
} catch (Exception ex) {
|
} catch (Exception ex) {
|
||||||
p.sendMessage(new TextComponent("§c✗ Fehler bei der Verbindung zum Forum."));
|
p.sendMessage(new TextComponent("§c✗ Fehler bei der Verbindung zum Forum."));
|
||||||
@@ -261,16 +261,16 @@ public class ForumBridgeModule implements Module, Listener {
|
|||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void execute(CommandSender sender, String[] args) {
|
public void execute(CommandSender sender, String[] args) {
|
||||||
if (!(sender instanceof ProxiedPlayer)) { sender.sendMessage(new TextComponent("§cNur Spieler können diesen Befehl benutzen.")); return; }
|
if (!(sender instanceof ProxiedPlayer)) { sender.sendMessage(new TextComponent("§cNur Spieler k\u00f6nnen diesen Befehl benutzen.")); return; }
|
||||||
ProxiedPlayer p = (ProxiedPlayer) sender;
|
ProxiedPlayer p = (ProxiedPlayer) sender;
|
||||||
List<ForumNotification> pending = storage.getPending(p.getUniqueId());
|
List<ForumNotification> pending = storage.getPending(p.getUniqueId());
|
||||||
|
|
||||||
if (pending.isEmpty()) {
|
if (pending.isEmpty()) {
|
||||||
p.sendMessage(new TextComponent("§7Keine neuen Forum-Benachrichtigungen."));
|
p.sendMessage(new TextComponent("§7Keine neuen Forum-Benachrichtigungen."));
|
||||||
if (!wpBaseUrl.isEmpty()) {
|
if (!wpBaseUrl.isEmpty()) {
|
||||||
TextComponent link = new TextComponent("§a➜ Forum öffnen");
|
TextComponent link = new TextComponent("§a➜ Forum \u00f6ffnen");
|
||||||
link.setClickEvent(new ClickEvent(ClickEvent.Action.OPEN_URL, wpBaseUrl));
|
link.setClickEvent(new ClickEvent(ClickEvent.Action.OPEN_URL, wpBaseUrl));
|
||||||
link.setHoverEvent(new HoverEvent(HoverEvent.Action.SHOW_TEXT, new ComponentBuilder("§7Klicke um das Forum zu öffnen").create()));
|
link.setHoverEvent(new HoverEvent(HoverEvent.Action.SHOW_TEXT, new ComponentBuilder("§7Klicke um das Forum zu \u00f6ffnen").create()));
|
||||||
p.sendMessage(link);
|
p.sendMessage(link);
|
||||||
}
|
}
|
||||||
return;
|
return;
|
||||||
@@ -287,7 +287,7 @@ public class ForumBridgeModule implements Module, Listener {
|
|||||||
TextComponent detail = new TextComponent(!n.getTitle().isEmpty() ? "§f" + n.getTitle() : "§fvon " + n.getAuthor());
|
TextComponent detail = new TextComponent(!n.getTitle().isEmpty() ? "§f" + n.getTitle() : "§fvon " + n.getAuthor());
|
||||||
if (!n.getUrl().isEmpty()) {
|
if (!n.getUrl().isEmpty()) {
|
||||||
detail.setClickEvent(new ClickEvent(ClickEvent.Action.OPEN_URL, n.getUrl()));
|
detail.setClickEvent(new ClickEvent(ClickEvent.Action.OPEN_URL, n.getUrl()));
|
||||||
detail.setHoverEvent(new HoverEvent(HoverEvent.Action.SHOW_TEXT, new ComponentBuilder("§7Klicke zum Öffnen").create()));
|
detail.setHoverEvent(new HoverEvent(HoverEvent.Action.SHOW_TEXT, new ComponentBuilder("§7Klicke zum \u00d6ffnen").create()));
|
||||||
}
|
}
|
||||||
line.addExtra(detail);
|
line.addExtra(detail);
|
||||||
p.sendMessage(line);
|
p.sendMessage(line);
|
||||||
@@ -305,7 +305,7 @@ public class ForumBridgeModule implements Module, Listener {
|
|||||||
public ForumNotifStorage getStorage() { return storage; }
|
public ForumNotifStorage getStorage() { return storage; }
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* FIX #13: Gibt immer einen leeren String zurück, niemals null.
|
* FIX #13: Gibt immer einen leeren String zur\u00fcck, niemals null.
|
||||||
* Verhindert NullPointerExceptions in allen Aufrufern.
|
* Verhindert NullPointerExceptions in allen Aufrufern.
|
||||||
*/
|
*/
|
||||||
private static String extractJsonString(String json, String key) {
|
private static String extractJsonString(String json, String key) {
|
||||||
|
|||||||
@@ -9,7 +9,7 @@ import java.util.logging.Logger;
|
|||||||
/**
|
/**
|
||||||
* Speichert ausstehende Forum-Benachrichtigungen (Datei-basiert).
|
* Speichert ausstehende Forum-Benachrichtigungen (Datei-basiert).
|
||||||
* Benachrichtigungen die nicht sofort zugestellt werden konnten (Spieler offline)
|
* Benachrichtigungen die nicht sofort zugestellt werden konnten (Spieler offline)
|
||||||
* werden hier gespeichert und beim nächsten Login zugestellt.
|
* werden hier gespeichert und beim n\u00e4chsten Login zugestellt.
|
||||||
*/
|
*/
|
||||||
public class ForumNotifStorage {
|
public class ForumNotifStorage {
|
||||||
|
|
||||||
@@ -26,7 +26,7 @@ public class ForumNotifStorage {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Fügt eine Benachrichtigung hinzu.
|
* F\u00fcgt eine Benachrichtigung hinzu.
|
||||||
*/
|
*/
|
||||||
public void add(ForumNotification notification) {
|
public void add(ForumNotification notification) {
|
||||||
pending.computeIfAbsent(notification.getPlayerUuid(), k -> new CopyOnWriteArrayList<>())
|
pending.computeIfAbsent(notification.getPlayerUuid(), k -> new CopyOnWriteArrayList<>())
|
||||||
@@ -34,7 +34,7 @@ public class ForumNotifStorage {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Gibt alle ausstehenden (nicht zugestellten) Benachrichtigungen eines Spielers zurück.
|
* Gibt alle ausstehenden (nicht zugestellten) Benachrichtigungen eines Spielers zur\u00fcck.
|
||||||
*/
|
*/
|
||||||
public List<ForumNotification> getPending(UUID playerUuid) {
|
public List<ForumNotification> getPending(UUID playerUuid) {
|
||||||
CopyOnWriteArrayList<ForumNotification> list = pending.get(playerUuid);
|
CopyOnWriteArrayList<ForumNotification> list = pending.get(playerUuid);
|
||||||
@@ -81,7 +81,7 @@ public class ForumNotifStorage {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Entfernt Benachrichtigungen die älter als maxDays Tage sind.
|
* Entfernt Benachrichtigungen die \u00e4lter als maxDays Tage sind.
|
||||||
*/
|
*/
|
||||||
public void purgeOld(int maxDays) {
|
public void purgeOld(int maxDays) {
|
||||||
long cutoff = System.currentTimeMillis() - ((long) maxDays * 24 * 60 * 60 * 1000);
|
long cutoff = System.currentTimeMillis() - ((long) maxDays * 24 * 60 * 60 * 1000);
|
||||||
|
|||||||
@@ -25,7 +25,7 @@ public class ForumNotification {
|
|||||||
this.delivered = false;
|
this.delivered = false;
|
||||||
}
|
}
|
||||||
|
|
||||||
/** Interner Konstruktor für Deserialisierung */
|
/** Interner Konstruktor f\u00fcr Deserialisierung */
|
||||||
ForumNotification(UUID playerUuid, String type, String title, String author, String url, long timestamp, boolean delivered) {
|
ForumNotification(UUID playerUuid, String type, String title, String author, String url, long timestamp, boolean delivered) {
|
||||||
this.playerUuid = playerUuid;
|
this.playerUuid = playerUuid;
|
||||||
this.type = type;
|
this.type = type;
|
||||||
@@ -48,12 +48,12 @@ public class ForumNotification {
|
|||||||
public void setDelivered(boolean d) { this.delivered = d; }
|
public void setDelivered(boolean d) { this.delivered = d; }
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Deutsches Label für den Benachrichtigungstyp.
|
* Deutsches Label f\u00fcr den Benachrichtigungstyp.
|
||||||
*/
|
*/
|
||||||
public String getTypeLabel() {
|
public String getTypeLabel() {
|
||||||
switch (type) {
|
switch (type) {
|
||||||
case "reply": return "Neue Antwort";
|
case "reply": return "Neue Antwort";
|
||||||
case "mention": return "Erwähnung";
|
case "mention": return "Erw\u00e4hnung";
|
||||||
case "message": return "Neue PN";
|
case "message": return "Neue PN";
|
||||||
case "thread": return "Neuer Thread";
|
case "thread": return "Neuer Thread";
|
||||||
case "poll": return "Neue Umfrage";
|
case "poll": return "Neue Umfrage";
|
||||||
@@ -70,15 +70,15 @@ public class ForumNotification {
|
|||||||
case "reply": return "§b"; // Aqua
|
case "reply": return "§b"; // Aqua
|
||||||
case "mention": return "§e"; // Gelb
|
case "mention": return "§e"; // Gelb
|
||||||
case "message": return "§d"; // Rosa
|
case "message": return "§d"; // Rosa
|
||||||
case "thread": return "§a"; // Grün
|
case "thread": return "§a"; // Gr\u00fcn
|
||||||
case "poll": return "§3"; // Dunkel-Aqua
|
case "poll": return "§3"; // Dunkel-Aqua
|
||||||
case "answer": return "§2"; // Dunkel-Grün
|
case "answer": return "§2"; // Dunkel-Gr\u00fcn
|
||||||
default: return "§f"; // Weiß
|
default: return "§f"; // Wei\u00df
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Serialisierung für Datei-Speicherung.
|
* Serialisierung f\u00fcr Datei-Speicherung.
|
||||||
* Format: uuid|type|title|author|url|timestamp|delivered
|
* Format: uuid|type|title|author|url|timestamp|delivered
|
||||||
*/
|
*/
|
||||||
public String toLine() {
|
public String toLine() {
|
||||||
|
|||||||
@@ -65,7 +65,7 @@ public class HelpModule implements Module {
|
|||||||
@Override
|
@Override
|
||||||
public void execute(CommandSender sender, String[] args) {
|
public void execute(CommandSender sender, String[] args) {
|
||||||
if (args.length == 0) {
|
if (args.length == 0) {
|
||||||
send(sender, "&7Nutze &e/" + getName() + " help &7für eine Befehlsübersicht.");
|
send(sender, "&7Nutze &e/" + getName() + " help &7f\u00fcr eine Befehls\u00fcbersicht.");
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
if (!args[0].equalsIgnoreCase("help")) {
|
if (!args[0].equalsIgnoreCase("help")) {
|
||||||
@@ -87,13 +87,13 @@ public class HelpModule implements Module {
|
|||||||
try {
|
try {
|
||||||
page = Integer.parseInt(args[1].trim());
|
page = Integer.parseInt(args[1].trim());
|
||||||
} catch (NumberFormatException e) {
|
} catch (NumberFormatException e) {
|
||||||
send(sender, "&cUngültige Seitenzahl. Nutze &e/" + getName() + " help <1-" + totalPages + ">&c.");
|
send(sender, "&cUng\u00fcltige Seitenzahl. Nutze &e/" + getName() + " help <1-" + totalPages + ">&c.");
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (page < 1 || page > totalPages) {
|
if (page < 1 || page > totalPages) {
|
||||||
send(sender, "&cSeite &e" + page + " &cexistiert nicht. Verfügbar: &e1&c-&e" + totalPages + "&c.");
|
send(sender, "&cSeite &e" + page + " &cexistiert nicht. Verf\u00fcgbar: &e1&c-&e" + totalPages + "&c.");
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -116,7 +116,7 @@ public class HelpModule implements Module {
|
|||||||
send(sender, "");
|
send(sender, "");
|
||||||
}
|
}
|
||||||
|
|
||||||
/** Baut alle Seiten zusammen. Admins bekommen zusätzliche Seiten. */
|
/** Baut alle Seiten zusammen. Admins bekommen zus\u00e4tzliche Seiten. */
|
||||||
private List<List<String>> buildPages(boolean isAdmin) {
|
private List<List<String>> buildPages(boolean isAdmin) {
|
||||||
List<List<String>> pages = new ArrayList<>();
|
List<List<String>> pages = new ArrayList<>();
|
||||||
|
|
||||||
@@ -124,7 +124,7 @@ public class HelpModule implements Module {
|
|||||||
List<String> p1 = new ArrayList<>();
|
List<String> p1 = new ArrayList<>();
|
||||||
p1.add(" &e&lAllgemein");
|
p1.add(" &e&lAllgemein");
|
||||||
p1.add(" &a/verify <token> &8– &7Account verifizieren");
|
p1.add(" &a/verify <token> &8– &7Account verifizieren");
|
||||||
p1.add(" &a/forumlink &8(&7/fl&8) &8– &7Forum-Account verknüpfen");
|
p1.add(" &a/forumlink &8(&7/fl&8) &8– &7Forum-Account verkn\u00fcpfen");
|
||||||
p1.add(" &a/forum &8– &7Forum-Benachrichtigungen");
|
p1.add(" &a/forum &8– &7Forum-Benachrichtigungen");
|
||||||
p1.add(" &a/go [server] &8(&7/wechsel, /switch&8) &8– &7Serverwechsel");
|
p1.add(" &a/go [server] &8(&7/wechsel, /switch&8) &8– &7Serverwechsel");
|
||||||
p1.add(" &a/scoreboard &8(&7/sb&8) [hide|show] &8– &7Scoreboard umschalten");
|
p1.add(" &a/scoreboard &8(&7/sb&8) [hide|show] &8– &7Scoreboard umschalten");
|
||||||
@@ -138,22 +138,22 @@ public class HelpModule implements Module {
|
|||||||
p1.add(" &a/chataus &8(&7/togglechat&8) &8– &7Chat-Empfang umschalten");
|
p1.add(" &a/chataus &8(&7/togglechat&8) &8– &7Chat-Empfang umschalten");
|
||||||
pages.add(p1);
|
pages.add(p1);
|
||||||
|
|
||||||
// ── Seite 2: Chat (weiter) & Account-Verknüpfungen ───────────────
|
// ── Seite 2: Chat (weiter) & Account-Verkn\u00fcpfungen ───────────────
|
||||||
List<String> p2 = new ArrayList<>();
|
List<String> p2 = new ArrayList<>();
|
||||||
p2.add(" &e&lChat (Fortsetzung)");
|
p2.add(" &e&lChat (Fortsetzung)");
|
||||||
p2.add(" &a/emoji &8(&7/emojis&8) &8– &7Alle Emojis anzeigen");
|
p2.add(" &a/emoji &8(&7/emojis&8) &8– &7Alle Emojis anzeigen");
|
||||||
p2.add(" &a/mentions &8(&7/mention&8) &8– &7Mention-Benachrichtigungen");
|
p2.add(" &a/mentions &8(&7/mention&8) &8– &7Mention-Benachrichtigungen");
|
||||||
p2.add(" &a/helpop <Nachricht> &8– &7Team um Hilfe bitten");
|
p2.add(" &a/helpop <Nachricht> &8– &7Team um Hilfe bitten");
|
||||||
p2.add(" &a/report <Spieler> <Grund> &8– &7Spieler melden");
|
p2.add(" &a/report <Spieler> <Grund> &8– &7Spieler melden");
|
||||||
p2.add(" &a/chatbypass &8(&7/cbp&8) &8– &7ChatModule überspringen");
|
p2.add(" &a/chatbypass &8(&7/cbp&8) &8– &7ChatModule \u00fcberspringen");
|
||||||
p2.add("");
|
p2.add("");
|
||||||
p2.add(" &e&lAccount-Verknüpfungen");
|
p2.add(" &e&lAccount-Verkn\u00fcpfungen");
|
||||||
p2.add(" &a/discordlink &8(&7/dlink&8) &8– &7Discord verknüpfen");
|
p2.add(" &a/discordlink &8(&7/dlink&8) &8– &7Discord verkn\u00fcpfen");
|
||||||
p2.add(" &a/telegramlink &8(&7/tlink&8) &8– &7Telegram verknüpfen");
|
p2.add(" &a/telegramlink &8(&7/tlink&8) &8– &7Telegram verkn\u00fcpfen");
|
||||||
p2.add(" &a/unlink <discord|telegram|all> &8– &7Verknüpfung aufheben");
|
p2.add(" &a/unlink <discord|telegram|all> &8– &7Verkn\u00fcpfung aufheben");
|
||||||
pages.add(p2);
|
pages.add(p2);
|
||||||
|
|
||||||
// ── Admin-Seiten nur für Berechtigte ──────────────────────────────
|
// ── Admin-Seiten nur f\u00fcr Berechtigte ──────────────────────────────
|
||||||
if (isAdmin) {
|
if (isAdmin) {
|
||||||
// ── Seite 3: StatusAPI, AntiBot, Vanish ───────────────────────
|
// ── Seite 3: StatusAPI, AntiBot, Vanish ───────────────────────
|
||||||
List<String> p3 = new ArrayList<>();
|
List<String> p3 = new ArrayList<>();
|
||||||
@@ -185,7 +185,7 @@ public class HelpModule implements Module {
|
|||||||
p4.add("");
|
p4.add("");
|
||||||
p4.add(" &c&lAdmin &8– &eReports, Tools");
|
p4.add(" &c&lAdmin &8– &eReports, Tools");
|
||||||
p4.add(" &c/reports [all] &8– &7Offene Reports anzeigen");
|
p4.add(" &c/reports [all] &8– &7Offene Reports anzeigen");
|
||||||
p4.add(" &c/reportclose <ID> &8– &7Report schließen");
|
p4.add(" &c/reportclose <ID> &8– &7Report schlie\u00dfen");
|
||||||
p4.add(" &c/automessage reload &8– &7AutoMessage neu laden");
|
p4.add(" &c/automessage reload &8– &7AutoMessage neu laden");
|
||||||
p4.add(" &c/bcmds reload &8– &7Custom-Commands neu laden");
|
p4.add(" &c/bcmds reload &8– &7Custom-Commands neu laden");
|
||||||
p4.add(" &c/cb <Befehl> &8– &7Command-Blocker verwalten");
|
p4.add(" &c/cb <Befehl> &8– &7Command-Blocker verwalten");
|
||||||
@@ -198,7 +198,7 @@ public class HelpModule implements Module {
|
|||||||
|
|
||||||
/** Sendet eine klickbare Navigationszeile mit ◀ Seite X/Y ▶ */
|
/** Sendet eine klickbare Navigationszeile mit ◀ Seite X/Y ▶ */
|
||||||
private void sendNavigation(CommandSender sender, String cmd, int page, int total) {
|
private void sendNavigation(CommandSender sender, String cmd, int page, int total) {
|
||||||
// Für Konsole: einfacher Text
|
// F\u00fcr Konsole: einfacher Text
|
||||||
if (!(sender instanceof ProxiedPlayer)) {
|
if (!(sender instanceof ProxiedPlayer)) {
|
||||||
String nav = " ";
|
String nav = " ";
|
||||||
if (page > 1) nav += "&7[&e◀&7] ";
|
if (page > 1) nav += "&7[&e◀&7] ";
|
||||||
@@ -208,10 +208,10 @@ public class HelpModule implements Module {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Für Spieler: klickbare Buttons
|
// F\u00fcr Spieler: klickbare Buttons
|
||||||
TextComponent line = new TextComponent(" ");
|
TextComponent line = new TextComponent(" ");
|
||||||
|
|
||||||
// ◀ zurück
|
// ◀ zur\u00fcck
|
||||||
if (page > 1) {
|
if (page > 1) {
|
||||||
TextComponent prev = new TextComponent(
|
TextComponent prev = new TextComponent(
|
||||||
ChatColor.translateAlternateColorCodes('&', "&7[&e◀&7] "));
|
ChatColor.translateAlternateColorCodes('&', "&7[&e◀&7] "));
|
||||||
|
|||||||
@@ -32,10 +32,10 @@ import java.util.logging.Logger;
|
|||||||
*
|
*
|
||||||
* Features:
|
* Features:
|
||||||
* - IP-Check: blockiert zweiten Account von gleicher IP
|
* - IP-Check: blockiert zweiten Account von gleicher IP
|
||||||
* - Bypass NUR über LuckPerms (OP zählt nicht)
|
* - Bypass NUR \u00fcber LuckPerms (OP z\u00e4hlt nicht)
|
||||||
* - Persistentes Log in multiaccountguard.log
|
* - Persistentes Log in multiaccountguard.log
|
||||||
* - Staff-Benachrichtigung ingame (Permission: statusapi.staff.notify)
|
* - Staff-Benachrichtigung ingame (Permission: statusapi.staff.notify)
|
||||||
* - Temporärer IP-Bann nach X Versuchen (Integration mit AntiBotModule)
|
* - Tempor\u00e4rer IP-Bann nach X Versuchen (Integration mit AntiBotModule)
|
||||||
* - Discord-Webhook bei Konflikt
|
* - Discord-Webhook bei Konflikt
|
||||||
*/
|
*/
|
||||||
public class MultiAccountGuard implements Module, Listener {
|
public class MultiAccountGuard implements Module, Listener {
|
||||||
@@ -62,7 +62,7 @@ public class MultiAccountGuard implements Module, Listener {
|
|||||||
private boolean staffNotifyEnabled = true;
|
private boolean staffNotifyEnabled = true;
|
||||||
private String staffNotifyFormat = "&8[&cMAG&8] &e{blocked} &7wurde blockiert &8(2. Account von &e{existing}&8) &7| IP: &f{ip}";
|
private String staffNotifyFormat = "&8[&cMAG&8] &e{blocked} &7wurde blockiert &8(2. Account von &e{existing}&8) &7| IP: &f{ip}";
|
||||||
|
|
||||||
// Temporärer IP-Bann
|
// Tempor\u00e4rer IP-Bann
|
||||||
private boolean tempBanEnabled = true;
|
private boolean tempBanEnabled = true;
|
||||||
private int tempBanMaxAttempts = 3;
|
private int tempBanMaxAttempts = 3;
|
||||||
private int tempBanDurationSecs = 300;
|
private int tempBanDurationSecs = 300;
|
||||||
@@ -119,7 +119,7 @@ public class MultiAccountGuard implements Module, Listener {
|
|||||||
ProxiedPlayer joining = event.getPlayer();
|
ProxiedPlayer joining = event.getPlayer();
|
||||||
|
|
||||||
if (hasBypass(joining)) {
|
if (hasBypass(joining)) {
|
||||||
log.info("[MultiAccountGuard] " + joining.getName() + " hat Bypass (LuckPerms) – übersprungen.");
|
log.info("[MultiAccountGuard] " + joining.getName() + " hat Bypass (LuckPerms) – \u00fcbersprungen.");
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -127,14 +127,14 @@ public class MultiAccountGuard implements Module, Listener {
|
|||||||
String joiningIp = extractIp(joining.getSocketAddress());
|
String joiningIp = extractIp(joining.getSocketAddress());
|
||||||
|
|
||||||
if (joiningIp == null) {
|
if (joiningIp == null) {
|
||||||
log.warning("[MultiAccountGuard] Konnte IP von " + joining.getName() + " nicht lesen – übersprungen.");
|
log.warning("[MultiAccountGuard] Konnte IP von " + joining.getName() + " nicht lesen – \u00fcbersprungen.");
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
log.info("[MultiAccountGuard] Login-Check: " + joining.getName()
|
log.info("[MultiAccountGuard] Login-Check: " + joining.getName()
|
||||||
+ " | UUID=" + joiningUuid + " | IP=" + joiningIp);
|
+ " | UUID=" + joiningUuid + " | IP=" + joiningIp);
|
||||||
|
|
||||||
// Alle anderen Spieler (sich selbst per UUID ausschließen)
|
// Alle anderen Spieler (sich selbst per UUID ausschlie\u00dfen)
|
||||||
List<ProxiedPlayer> others = new ArrayList<>();
|
List<ProxiedPlayer> others = new ArrayList<>();
|
||||||
for (ProxiedPlayer p : ProxyServer.getInstance().getPlayers()) {
|
for (ProxiedPlayer p : ProxyServer.getInstance().getPlayers()) {
|
||||||
if (p.getUniqueId().equals(joiningUuid)) continue;
|
if (p.getUniqueId().equals(joiningUuid)) continue;
|
||||||
@@ -192,7 +192,7 @@ public class MultiAccountGuard implements Module, Listener {
|
|||||||
notifyStaff(blockedName, allowedName, ip);
|
notifyStaff(blockedName, allowedName, ip);
|
||||||
}
|
}
|
||||||
|
|
||||||
// 3. Temporärer IP-Bann
|
// 3. Tempor\u00e4rer IP-Bann
|
||||||
if (tempBanEnabled) {
|
if (tempBanEnabled) {
|
||||||
int attempts = attemptsByIp.merge(ip, 1, Integer::sum);
|
int attempts = attemptsByIp.merge(ip, 1, Integer::sum);
|
||||||
log.info("[MultiAccountGuard] IP " + ip + " hat " + attempts + "/" + tempBanMaxAttempts + " Versuche.");
|
log.info("[MultiAccountGuard] IP " + ip + " hat " + attempts + "/" + tempBanMaxAttempts + " Versuche.");
|
||||||
@@ -258,7 +258,7 @@ public class MultiAccountGuard implements Module, Listener {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// -------------------------------------------------------------------------
|
// -------------------------------------------------------------------------
|
||||||
// 3. Temporärer IP-Bann via AntiBotModule
|
// 3. Tempor\u00e4rer IP-Bann via AntiBotModule
|
||||||
// -------------------------------------------------------------------------
|
// -------------------------------------------------------------------------
|
||||||
|
|
||||||
private void banIp(String ip) {
|
private void banIp(String ip) {
|
||||||
@@ -266,20 +266,20 @@ public class MultiAccountGuard implements Module, Listener {
|
|||||||
StatusAPI statusApi = (StatusAPI) ProxyServer.getInstance()
|
StatusAPI statusApi = (StatusAPI) ProxyServer.getInstance()
|
||||||
.getPluginManager().getPlugin("StatusAPI");
|
.getPluginManager().getPlugin("StatusAPI");
|
||||||
if (statusApi == null) {
|
if (statusApi == null) {
|
||||||
log.warning("[MultiAccountGuard] StatusAPI nicht gefunden – IP-Bann nicht möglich.");
|
log.warning("[MultiAccountGuard] StatusAPI nicht gefunden – IP-Bann nicht m\u00f6glich.");
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
AntiBotModule antiBot = statusApi.getModuleManager().getModule(AntiBotModule.class);
|
AntiBotModule antiBot = statusApi.getModuleManager().getModule(AntiBotModule.class);
|
||||||
if (antiBot == null) {
|
if (antiBot == null) {
|
||||||
log.warning("[MultiAccountGuard] AntiBotModule nicht gefunden – IP-Bann nicht möglich.");
|
log.warning("[MultiAccountGuard] AntiBotModule nicht gefunden – IP-Bann nicht m\u00f6glich.");
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
antiBot.blockIpExternal(ip, tempBanDurationSecs);
|
antiBot.blockIpExternal(ip, tempBanDurationSecs);
|
||||||
log.warning("[MultiAccountGuard] IP " + ip + " für " + tempBanDurationSecs + "s gebannt (zu viele Multi-Account-Versuche).");
|
log.warning("[MultiAccountGuard] IP " + ip + " f\u00fcr " + tempBanDurationSecs + "s gebannt (zu viele Multi-Account-Versuche).");
|
||||||
|
|
||||||
// Staff über den Bann informieren
|
// Staff \u00fcber den Bann informieren
|
||||||
String banMsg = ChatColor.translateAlternateColorCodes('&',
|
String banMsg = ChatColor.translateAlternateColorCodes('&',
|
||||||
"&8[&cMAG&8] &7IP &f" + ip + " &7wurde für &c" + tempBanDurationSecs + "s &7gebannt &8(zu viele Versuche).");
|
"&8[&cMAG&8] &7IP &f" + ip + " &7wurde f\u00fcr &c" + tempBanDurationSecs + "s &7gebannt &8(zu viele Versuche).");
|
||||||
for (ProxiedPlayer p : ProxyServer.getInstance().getPlayers()) {
|
for (ProxiedPlayer p : ProxyServer.getInstance().getPlayers()) {
|
||||||
if (p.hasPermission(STAFF_PERM)) {
|
if (p.hasPermission(STAFF_PERM)) {
|
||||||
p.sendMessage(new TextComponent(banMsg));
|
p.sendMessage(new TextComponent(banMsg));
|
||||||
@@ -384,7 +384,7 @@ public class MultiAccountGuard implements Module, Listener {
|
|||||||
}
|
}
|
||||||
return false;
|
return false;
|
||||||
} catch (Exception e) {
|
} catch (Exception e) {
|
||||||
log.warning("[MultiAccountGuard] LuckPerms-Check fehlgeschlagen für " + player.getName() + ": " + e.getMessage());
|
log.warning("[MultiAccountGuard] LuckPerms-Check fehlgeschlagen f\u00fcr " + player.getName() + ": " + e.getMessage());
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -33,7 +33,7 @@ import java.util.UUID;
|
|||||||
import java.util.concurrent.TimeUnit;
|
import java.util.concurrent.TimeUnit;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Liefert erweiterte Proxy- und Systeminformationen für API und Ingame-Debug.
|
* Liefert erweiterte Proxy- und Systeminformationen f\u00fcr API und Ingame-Debug.
|
||||||
*/
|
*/
|
||||||
public class NetworkInfoModule implements Module {
|
public class NetworkInfoModule implements Module {
|
||||||
|
|
||||||
@@ -66,7 +66,7 @@ public class NetworkInfoModule implements Module {
|
|||||||
private long lastTpsAlertAt = 0L;
|
private long lastTpsAlertAt = 0L;
|
||||||
private volatile double currentProxyTps = 20.0D;
|
private volatile double currentProxyTps = 20.0D;
|
||||||
|
|
||||||
/** FIX: Öffentlicher Getter damit ScoreboardModule als TPS-Fallback darauf zugreifen kann */
|
/** FIX: \u00d6ffentlicher Getter damit ScoreboardModule als TPS-Fallback darauf zugreifen kann */
|
||||||
public double getProxyTps() { return currentProxyTps; }
|
public double getProxyTps() { return currentProxyTps; }
|
||||||
private long lastTpsSampleAtMs = 0L;
|
private long lastTpsSampleAtMs = 0L;
|
||||||
private ScheduledTask alertTask;
|
private ScheduledTask alertTask;
|
||||||
@@ -139,7 +139,7 @@ public class NetworkInfoModule implements Module {
|
|||||||
return sendWebhookEmbed(
|
return sendWebhookEmbed(
|
||||||
webhookUrl,
|
webhookUrl,
|
||||||
"✅ NetworkInfo gestartet",
|
"✅ NetworkInfo gestartet",
|
||||||
"Proxy: **" + ProxyServer.getInstance().getName() + "**\nÜberwachung und Webhook-Alerts sind jetzt aktiv.",
|
"Proxy: **" + ProxyServer.getInstance().getName() + "**\n\u00dcberwachung und Webhook-Alerts sind jetzt aktiv.",
|
||||||
0x2ECC71,
|
0x2ECC71,
|
||||||
null,
|
null,
|
||||||
false
|
false
|
||||||
@@ -153,7 +153,7 @@ public class NetworkInfoModule implements Module {
|
|||||||
return sendWebhookEmbed(
|
return sendWebhookEmbed(
|
||||||
webhookUrl,
|
webhookUrl,
|
||||||
"✅ NetworkInfo gestartet",
|
"✅ NetworkInfo gestartet",
|
||||||
"Überwachung und Webhook-Alerts sind jetzt aktiv.",
|
"\u00dcberwachung und Webhook-Alerts sind jetzt aktiv.",
|
||||||
0x2ECC71,
|
0x2ECC71,
|
||||||
fields.toString(),
|
fields.toString(),
|
||||||
false
|
false
|
||||||
@@ -165,7 +165,7 @@ public class NetworkInfoModule implements Module {
|
|||||||
return sendWebhookEmbed(
|
return sendWebhookEmbed(
|
||||||
webhookUrl,
|
webhookUrl,
|
||||||
"🛑 NetworkInfo gestoppt",
|
"🛑 NetworkInfo gestoppt",
|
||||||
"Die NetworkInfo-Überwachung wurde gestoppt.\nKeine weiteren Auto-Alerts bis zum nächsten Start.",
|
"Die NetworkInfo-\u00dcberwachung wurde gestoppt.\nKeine weiteren Auto-Alerts bis zum n\u00e4chsten Start.",
|
||||||
0xE74C3C,
|
0xE74C3C,
|
||||||
null,
|
null,
|
||||||
false
|
false
|
||||||
@@ -179,7 +179,7 @@ public class NetworkInfoModule implements Module {
|
|||||||
return sendWebhookEmbed(
|
return sendWebhookEmbed(
|
||||||
webhookUrl,
|
webhookUrl,
|
||||||
"🛑 NetworkInfo gestoppt",
|
"🛑 NetworkInfo gestoppt",
|
||||||
"Die NetworkInfo-Überwachung wurde gestoppt.",
|
"Die NetworkInfo-\u00dcberwachung wurde gestoppt.",
|
||||||
0xE74C3C,
|
0xE74C3C,
|
||||||
fields.toString(),
|
fields.toString(),
|
||||||
false
|
false
|
||||||
@@ -222,7 +222,7 @@ public class NetworkInfoModule implements Module {
|
|||||||
color = 0x2ECC71;
|
color = 0x2ECC71;
|
||||||
} else {
|
} else {
|
||||||
title = "🚨 Attack Detected";
|
title = "🚨 Attack Detected";
|
||||||
shortText = "Ungewöhnlich hoher Verbindungs-Traffic erkannt.";
|
shortText = "Ungew\u00f6hnlich hoher Verbindungs-Traffic erkannt.";
|
||||||
color = 0xE74C3C;
|
color = 0xE74C3C;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -565,7 +565,7 @@ public class NetworkInfoModule implements Module {
|
|||||||
sendWebhookEmbed(
|
sendWebhookEmbed(
|
||||||
webhookUrl,
|
webhookUrl,
|
||||||
"⚠️ Hohe RAM-Auslastung",
|
"⚠️ Hohe RAM-Auslastung",
|
||||||
"Ein Schwellwert wurde überschritten.",
|
"Ein Schwellwert wurde \u00fcberschritten.",
|
||||||
0xF39C12,
|
0xF39C12,
|
||||||
fields.toString()
|
fields.toString()
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -58,20 +58,20 @@ public class ScoreboardModule implements Module, Listener {
|
|||||||
// ── TicketSystem Placeholder ──────────────────────────────────────────────
|
// ── TicketSystem Placeholder ──────────────────────────────────────────────
|
||||||
/** Eigene aktive Tickets des Spielers (OPEN + CLAIMED + FORWARDED) */
|
/** Eigene aktive Tickets des Spielers (OPEN + CLAIMED + FORWARDED) */
|
||||||
public static final ConcurrentHashMap<UUID, Integer> ticketMyOpen = new ConcurrentHashMap<>();
|
public static final ConcurrentHashMap<UUID, Integer> ticketMyOpen = new ConcurrentHashMap<>();
|
||||||
/** Alle offenen Tickets gesamt (Status: OPEN) – für Supporter & Admin */
|
/** Alle offenen Tickets gesamt (Status: OPEN) – f\u00fcr Supporter & Admin */
|
||||||
public static final java.util.concurrent.atomic.AtomicInteger ticketTotalOpen = new java.util.concurrent.atomic.AtomicInteger(0);
|
public static final java.util.concurrent.atomic.AtomicInteger ticketTotalOpen = new java.util.concurrent.atomic.AtomicInteger(0);
|
||||||
/** Alle Tickets in Bearbeitung gesamt (Status: CLAIMED) – für Admin */
|
/** Alle Tickets in Bearbeitung gesamt (Status: CLAIMED) – f\u00fcr Admin */
|
||||||
public static final java.util.concurrent.atomic.AtomicInteger ticketTotalClaimed = new java.util.concurrent.atomic.AtomicInteger(0);
|
public static final java.util.concurrent.atomic.AtomicInteger ticketTotalClaimed = new java.util.concurrent.atomic.AtomicInteger(0);
|
||||||
/** Positive Bewertungen gesamt – für Admin */
|
/** Positive Bewertungen gesamt – f\u00fcr Admin */
|
||||||
public static final java.util.concurrent.atomic.AtomicInteger ticketRatingGood = new java.util.concurrent.atomic.AtomicInteger(0);
|
public static final java.util.concurrent.atomic.AtomicInteger ticketRatingGood = new java.util.concurrent.atomic.AtomicInteger(0);
|
||||||
/** Negative Bewertungen gesamt – für Admin */
|
/** Negative Bewertungen gesamt – f\u00fcr Admin */
|
||||||
public static final java.util.concurrent.atomic.AtomicInteger ticketRatingBad = new java.util.concurrent.atomic.AtomicInteger(0);
|
public static final java.util.concurrent.atomic.AtomicInteger ticketRatingBad = new java.util.concurrent.atomic.AtomicInteger(0);
|
||||||
|
|
||||||
private final ConcurrentHashMap<UUID, Long> joinTimes = new ConcurrentHashMap<>();
|
private final ConcurrentHashMap<UUID, Long> joinTimes = new ConcurrentHashMap<>();
|
||||||
/** Aktuell gerenderter Spieler – für PAPI-Auflösung in ph() */
|
/** Aktuell gerenderter Spieler – f\u00fcr PAPI-Aufl\u00f6sung in ph() */
|
||||||
private UUID currentPlayerUuid = null;
|
private UUID currentPlayerUuid = null;
|
||||||
// Spieler, die das Scoreboard ausgeblendet haben
|
// Spieler, die das Scoreboard ausgeblendet haben
|
||||||
/** FIX: Referenz auf NetworkInfoModule für TPS-Fallback */
|
/** FIX: Referenz auf NetworkInfoModule f\u00fcr TPS-Fallback */
|
||||||
private net.viper.status.modules.network.NetworkInfoModule networkInfoModule = null;
|
private net.viper.status.modules.network.NetworkInfoModule networkInfoModule = null;
|
||||||
|
|
||||||
/** Wird von StatusAPI nach dem Registrieren aller Module aufgerufen */
|
/** Wird von StatusAPI nach dem Registrieren aller Module aufgerufen */
|
||||||
@@ -88,6 +88,10 @@ public class ScoreboardModule implements Module, Listener {
|
|||||||
private final Set<UUID> forceAdminView = ConcurrentHashMap.newKeySet();
|
private final Set<UUID> forceAdminView = ConcurrentHashMap.newKeySet();
|
||||||
|
|
||||||
private boolean enabled = true;
|
private boolean enabled = true;
|
||||||
|
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();
|
||||||
private int updateInterval = 500; // Millisekunden
|
private int updateInterval = 500; // Millisekunden
|
||||||
private int tickerSpeed = 1;
|
private int tickerSpeed = 1;
|
||||||
private boolean rainbowEnabled = true;
|
private boolean rainbowEnabled = true;
|
||||||
@@ -138,7 +142,7 @@ public class ScoreboardModule implements Module, Listener {
|
|||||||
"§8","§9","§a","§b","§c","§d","§e",
|
"§8","§9","§a","§b","§c","§d","§e",
|
||||||
"§f§0","§f§1","§f§2","§f§3","§f§4"
|
"§f§0","§f§1","§f§2","§f§3","§f§4"
|
||||||
};
|
};
|
||||||
private static final int MAX_LINES = 15; // Minecraft Client zeigt max 15 Scoreboard-Einträge
|
private static final int MAX_LINES = 15; // Minecraft Client zeigt max 15 Scoreboard-Eintr\u00e4ge
|
||||||
|
|
||||||
private final ConcurrentHashMap<UUID, Integer> tickerPos = new ConcurrentHashMap<>();
|
private final ConcurrentHashMap<UUID, Integer> tickerPos = new ConcurrentHashMap<>();
|
||||||
private final ConcurrentHashMap<UUID, Integer> rainbowIdx = new ConcurrentHashMap<>();
|
private final ConcurrentHashMap<UUID, Integer> rainbowIdx = new ConcurrentHashMap<>();
|
||||||
@@ -183,13 +187,13 @@ public class ScoreboardModule implements Module, Listener {
|
|||||||
}
|
}
|
||||||
ProxyServer.getInstance().getPluginManager().registerListener(plugin, this);
|
ProxyServer.getInstance().getPluginManager().registerListener(plugin, this);
|
||||||
ProxyServer.getInstance().getPluginManager().registerCommand(plugin, new ScoreboardToggleCommand());
|
ProxyServer.getInstance().getPluginManager().registerCommand(plugin, new ScoreboardToggleCommand());
|
||||||
// updateInterval in ms für Daten-Updates (Kompass, Zeilen, etc.)
|
// updateInterval in ms f\u00fcr Daten-Updates (Kompass, Zeilen, etc.)
|
||||||
updateTask = ProxyServer.getInstance().getScheduler().schedule(
|
updateTask = ProxyServer.getInstance().getScheduler().schedule(
|
||||||
plugin, this::tickAll, updateInterval, updateInterval, TimeUnit.MILLISECONDS);
|
plugin, this::tickAll, updateInterval, updateInterval, TimeUnit.MILLISECONDS);
|
||||||
// Separater schneller Task nur für den Titel (Wave-Animation, 100ms = 10fps)
|
// Separater schneller Task nur f\u00fcr den Titel (Wave-Animation, 100ms = 10fps)
|
||||||
titleTask = ProxyServer.getInstance().getScheduler().schedule(
|
titleTask = ProxyServer.getInstance().getScheduler().schedule(
|
||||||
plugin, this::tickTitle, 100, 100, TimeUnit.MILLISECONDS);
|
plugin, this::tickTitle, 100, 100, TimeUnit.MILLISECONDS);
|
||||||
// Separater Task für News-Ticker (100ms = flüssiges Scrollen)
|
// Separater Task f\u00fcr News-Ticker (100ms = fl\u00fcssiges Scrollen)
|
||||||
newsTask = ProxyServer.getInstance().getScheduler().schedule(
|
newsTask = ProxyServer.getInstance().getScheduler().schedule(
|
||||||
plugin, this::tickNews, 100, 100, TimeUnit.MILLISECONDS);
|
plugin, this::tickNews, 100, 100, TimeUnit.MILLISECONDS);
|
||||||
}
|
}
|
||||||
@@ -220,6 +224,17 @@ public class ScoreboardModule implements Module, Listener {
|
|||||||
created.remove(id);
|
created.remove(id);
|
||||||
createdAdmin.remove(id);
|
createdAdmin.remove(id);
|
||||||
createdSupporter.remove(id);
|
createdSupporter.remove(id);
|
||||||
|
// Nametags: Neuen Spieler nach kurzer Verz\u00f6gerung mit allen bestehenden Nametags versorgen
|
||||||
|
// UND allen anderen den Nametag des neuen Spielers senden
|
||||||
|
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
|
||||||
|
for (ProxiedPlayer existing : ProxyServer.getInstance().getPlayers()) {
|
||||||
|
if (existing.isConnected()) updateNametag(existing);
|
||||||
|
}
|
||||||
|
}, 2L, TimeUnit.SECONDS);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@EventHandler
|
@EventHandler
|
||||||
@@ -227,11 +242,11 @@ public class ScoreboardModule implements Module, Listener {
|
|||||||
if (!ready) return;
|
if (!ready) return;
|
||||||
ProxiedPlayer p = e.getPlayer();
|
ProxiedPlayer p = e.getPlayer();
|
||||||
UUID id = p.getUniqueId();
|
UUID id = p.getUniqueId();
|
||||||
// Altes Objective sauber entfernen – tickAll übernimmt den Neuaufbau
|
// Altes Objective sauber entfernen – tickAll \u00fcbernimmt den Neuaufbau
|
||||||
if (created.contains(id)) { removeObjectiveAndTeams(p, OBJ_NAME, "vt"); created.remove(id); }
|
if (created.contains(id)) { removeObjectiveAndTeams(p, OBJ_NAME, "vt"); created.remove(id); }
|
||||||
if (createdAdmin.contains(id)) { removeObjectiveAndTeams(p, OBJ_NAME_ADMIN, "vta"); createdAdmin.remove(id); }
|
if (createdAdmin.contains(id)) { removeObjectiveAndTeams(p, OBJ_NAME_ADMIN, "vta"); createdAdmin.remove(id); }
|
||||||
if (createdSupporter.contains(id)) { removeObjectiveAndTeams(p, OBJ_NAME_SUPP, "vts"); createdSupporter.remove(id); }
|
if (createdSupporter.contains(id)) { removeObjectiveAndTeams(p, OBJ_NAME_SUPP, "vts"); createdSupporter.remove(id); }
|
||||||
// Kein verzögerter sendAll-Call mehr – tickAll baut nach max. 500ms neu auf
|
// Kein verz\u00f6gerter sendAll-Call mehr – tickAll baut nach max. 500ms neu auf
|
||||||
}
|
}
|
||||||
|
|
||||||
@EventHandler
|
@EventHandler
|
||||||
@@ -244,6 +259,8 @@ public class ScoreboardModule implements Module, Listener {
|
|||||||
playerWorld.remove(id); playerGamemode.remove(id);
|
playerWorld.remove(id); playerGamemode.remove(id);
|
||||||
playerExp.remove(id); playerFood.remove(id); playerSpeed.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); forceAdminView.remove(id); forcePlayerView.remove(id); newsPos.remove(id);
|
||||||
|
// Nametag-Team f\u00fcr diesen Spieler bei allen anderen entfernen
|
||||||
|
if (nametagEnabled) removeNametag(e.getPlayer());
|
||||||
}
|
}
|
||||||
|
|
||||||
/** Schneller Task: aktualisiert News-Position und sendet nur die betroffene Team-Zeile */
|
/** Schneller Task: aktualisiert News-Position und sendet nur die betroffene Team-Zeile */
|
||||||
@@ -263,7 +280,7 @@ public class ScoreboardModule implements Module, Listener {
|
|||||||
Set<UUID> activeCreated = isAdmin ? createdAdmin : isSupporter ? createdSupporter : created;
|
Set<UUID> activeCreated = isAdmin ? createdAdmin : isSupporter ? createdSupporter : created;
|
||||||
if (!activeCreated.contains(id)) continue;
|
if (!activeCreated.contains(id)) continue;
|
||||||
|
|
||||||
// Position vorrücken
|
// Position vorr\u00fccken
|
||||||
int nOff = (newsPos.getOrDefault(id, 0) + newsSpeed) % nCycle;
|
int nOff = (newsPos.getOrDefault(id, 0) + newsSpeed) % nCycle;
|
||||||
newsPos.put(id, nOff);
|
newsPos.put(id, nOff);
|
||||||
|
|
||||||
@@ -271,7 +288,7 @@ public class ScoreboardModule implements Module, Listener {
|
|||||||
try {
|
try {
|
||||||
String activeObjName = isAdmin ? OBJ_NAME_ADMIN : OBJ_NAME;
|
String activeObjName = isAdmin ? OBJ_NAME_ADMIN : OBJ_NAME;
|
||||||
String newsStr = buildNewsTicker(nOff);
|
String newsStr = buildNewsTicker(nOff);
|
||||||
// Finde welche Zeilennummer(n) %news% enthält und sende nur diese
|
// Finde welche Zeilennummer(n) %news% enth\u00e4lt und sende nur diese
|
||||||
java.util.Map<Integer, java.util.List<String>> lineMap =
|
java.util.Map<Integer, java.util.List<String>> lineMap =
|
||||||
isAdmin ? adminLineMap : isSupporter ? supporterLineMap : playerLineMap;
|
isAdmin ? adminLineMap : isSupporter ? supporterLineMap : playerLineMap;
|
||||||
for (java.util.Map.Entry<Integer, java.util.List<String>> entry : lineMap.entrySet()) {
|
for (java.util.Map.Entry<Integer, java.util.List<String>> entry : lineMap.entrySet()) {
|
||||||
@@ -293,7 +310,7 @@ public class ScoreboardModule implements Module, Listener {
|
|||||||
|
|
||||||
String lineText = c(tpl.replace("%news%", newsStr));
|
String lineText = c(tpl.replace("%news%", newsStr));
|
||||||
|
|
||||||
// Team-Packet nur für diese Zeile senden
|
// Team-Packet nur f\u00fcr diese Zeile senden
|
||||||
net.md_5.bungee.protocol.packet.Team team = new net.md_5.bungee.protocol.packet.Team();
|
net.md_5.bungee.protocol.packet.Team team = new net.md_5.bungee.protocol.packet.Team();
|
||||||
team.setName((isAdmin ? "vta" : isSupporter ? "vts" : "vt") + lineIdx);
|
team.setName((isAdmin ? "vta" : isSupporter ? "vts" : "vt") + lineIdx);
|
||||||
team.setMode((byte) 2); // UPDATE
|
team.setMode((byte) 2); // UPDATE
|
||||||
@@ -314,7 +331,7 @@ public class ScoreboardModule implements Module, Listener {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/** Schneller Task: aktualisiert nur den Objective-Titel für flüssige Wave-Animation */
|
/** Schneller Task: aktualisiert nur den Objective-Titel f\u00fcr fl\u00fcssige Wave-Animation */
|
||||||
private void tickTitle() {
|
private void tickTitle() {
|
||||||
if (!rainbowEnabled || !"wave".equals(rainbowMode)) return;
|
if (!rainbowEnabled || !"wave".equals(rainbowMode)) return;
|
||||||
for (ProxiedPlayer p : ProxyServer.getInstance().getPlayers()) {
|
for (ProxiedPlayer p : ProxyServer.getInstance().getPlayers()) {
|
||||||
@@ -346,7 +363,7 @@ public class ScoreboardModule implements Module, Listener {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private void tickAll() {
|
private void tickAll() {
|
||||||
// Nametags (Prefix über dem Kopf) periodisch aktualisieren
|
// Nametags (Prefix \u00fcber dem Kopf) periodisch aktualisieren
|
||||||
for (ProxiedPlayer p : ProxyServer.getInstance().getPlayers()) {
|
for (ProxiedPlayer p : ProxyServer.getInstance().getPlayers()) {
|
||||||
if (!p.isConnected()) continue;
|
if (!p.isConnected()) continue;
|
||||||
UUID id = p.getUniqueId();
|
UUID id = p.getUniqueId();
|
||||||
@@ -357,6 +374,95 @@ public class ScoreboardModule implements Module, Listener {
|
|||||||
created.add(id);
|
created.add(id);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
// Nametag-Packets an alle Spieler senden (AFK-Prefix \u00fcber dem Kopf)
|
||||||
|
if (nametagEnabled) {
|
||||||
|
for (ProxiedPlayer target : ProxyServer.getInstance().getPlayers()) {
|
||||||
|
updateNametag(target);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 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.
|
||||||
|
*/
|
||||||
|
private void updateNametag(ProxiedPlayer target) {
|
||||||
|
if (!ready || !target.isConnected()) return;
|
||||||
|
try {
|
||||||
|
boolean isAfk = Boolean.TRUE.equals(net.viper.status.StatusAPI.playerAfk.get(target.getUniqueId()));
|
||||||
|
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 prefixStr = isAfk ? "§7[AFK] §r" : (lpPrefix.isEmpty() ? "" : lpPrefix + "§r ");
|
||||||
|
|
||||||
|
// Packet an alle Online-Spieler senden (damit alle den ge\u00e4nderten Prefix sehen)
|
||||||
|
for (ProxiedPlayer viewer : ProxyServer.getInstance().getPlayers()) {
|
||||||
|
if (!viewer.isConnected()) 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
|
||||||
|
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)))
|
||||||
|
pfxComp.addExtra(bc);
|
||||||
|
team.setPrefix(Either.right(pfxComp));
|
||||||
|
team.setSuffix(Either.right(new net.md_5.bungee.api.chat.TextComponent("")));
|
||||||
|
team.setDisplayName(Either.right(new net.md_5.bungee.api.chat.TextComponent("")));
|
||||||
|
team.setNameTagVisibility(Either.right(NameTagVisibility.ALWAYS));
|
||||||
|
team.setCollisionRule(Either.right(CollisionRule.ALWAYS));
|
||||||
|
team.setColor(Optional.of(21)); // RESET
|
||||||
|
team.setFriendlyFire((byte) 3);
|
||||||
|
if (firstTime) team.setPlayers(new String[]{ target.getName() });
|
||||||
|
sendPkt.invoke(viewer, team);
|
||||||
|
} catch (Exception ignored) {}
|
||||||
|
}
|
||||||
|
nametagCreated.add(target.getUniqueId());
|
||||||
|
} catch (Exception e) {
|
||||||
|
plugin.getLogger().warning("[ScoreboardModule] Nametag-Fehler f\u00fcr " + target.getName() + ": " + e.getMessage());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 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) {}
|
||||||
|
}
|
||||||
|
nametagCreated.remove(target.getUniqueId());
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Liest den LuckPerms-Prefix eines Spielers (gleiche Logik wie getRank, aber roher Prefix).
|
||||||
|
*/
|
||||||
|
private String getLpPrefix(ProxiedPlayer player) {
|
||||||
|
try {
|
||||||
|
Class<?> prov = Class.forName("net.luckperms.api.LuckPermsProvider");
|
||||||
|
Object api = prov.getMethod("get").invoke(null);
|
||||||
|
Object um = api.getClass().getMethod("getUserManager").invoke(api);
|
||||||
|
Object usr = um.getClass().getMethod("getUser", UUID.class).invoke(um, player.getUniqueId());
|
||||||
|
if (usr != null) {
|
||||||
|
Class<?> qo = Class.forName("net.luckperms.api.query.QueryOptions");
|
||||||
|
Object opts = qo.getMethod("defaultContextualOptions").invoke(null);
|
||||||
|
Object cache= usr.getClass().getMethod("getCachedData").invoke(usr);
|
||||||
|
Object meta = cache.getClass().getMethod("getMetaData", qo).invoke(cache, opts);
|
||||||
|
Object pfx = meta.getClass().getMethod("getPrefix").invoke(meta);
|
||||||
|
if (pfx != null && !pfx.toString().isEmpty()) return pfx.toString();
|
||||||
|
}
|
||||||
|
} catch (Exception ignored) {}
|
||||||
|
return "";
|
||||||
}
|
}
|
||||||
|
|
||||||
private void sendAll(ProxiedPlayer p) throws Exception {
|
private void sendAll(ProxiedPlayer p) throws Exception {
|
||||||
@@ -470,8 +576,8 @@ public class ScoreboardModule implements Module, Listener {
|
|||||||
List<String> lines = new ArrayList<>();
|
List<String> lines = new ArrayList<>();
|
||||||
boolean hasTicker = !tickerText.isEmpty() && !isAdmin && !isSupporter;
|
boolean hasTicker = !tickerText.isEmpty() && !isAdmin && !isSupporter;
|
||||||
if (hasTicker) lines.add(ticker(rawTicker, tOff, rIdx));
|
if (hasTicker) lines.add(ticker(rawTicker, tOff, rIdx));
|
||||||
// Maximale Inhaltszeilen: MAX_LINES insgesamt (Ticker zählt als eine)
|
// Maximale Inhaltszeilen: MAX_LINES insgesamt (Ticker z\u00e4hlt als eine)
|
||||||
currentPlayerUuid = id; // für PAPI-Auflösung in ph()
|
currentPlayerUuid = id; // f\u00fcr PAPI-Aufl\u00f6sung in ph()
|
||||||
for (String tpl : srcLines) {
|
for (String tpl : srcLines) {
|
||||||
if (lines.size() >= MAX_LINES) break;
|
if (lines.size() >= MAX_LINES) break;
|
||||||
lines.add(c(ph(tpl, pn, rank, money, srv, comp, hp, hpNum, ping, online, maxpl, tps, ram, time, playtime,
|
lines.add(c(ph(tpl, pn, rank, money, srv, comp, hp, hpNum, ping, online, maxpl, tps, ram, time, playtime,
|
||||||
@@ -479,7 +585,7 @@ public class ScoreboardModule implements Module, Listener {
|
|||||||
ticketMyOpenStr, ticketTotalOpenStr, ticketTotalClaimedStr,
|
ticketMyOpenStr, ticketTotalOpenStr, ticketTotalClaimedStr,
|
||||||
ticketRatingGoodStr, ticketRatingBadStr, ticketRatingPctStr)));
|
ticketRatingGoodStr, ticketRatingBadStr, ticketRatingPctStr)));
|
||||||
}
|
}
|
||||||
// Immer genau MAX_LINES Zeilen (Rest mit Leerzeilen auffüllen)
|
// Immer genau MAX_LINES Zeilen (Rest mit Leerzeilen auff\u00fcllen)
|
||||||
if (lines.size() > MAX_LINES) lines = new ArrayList<>(lines.subList(0, MAX_LINES));
|
if (lines.size() > MAX_LINES) lines = new ArrayList<>(lines.subList(0, MAX_LINES));
|
||||||
while (lines.size() < MAX_LINES) lines.add(" ");
|
while (lines.size() < MAX_LINES) lines.add(" ");
|
||||||
|
|
||||||
@@ -618,14 +724,14 @@ public class ScoreboardModule implements Module, Listener {
|
|||||||
boolean strike = text.contains("§m") || text.contains("&m");
|
boolean strike = text.contains("§m") || text.contains("&m");
|
||||||
String fmt = (bold ? "§l" : "") + (italic ? "§o" : "")
|
String fmt = (bold ? "§l" : "") + (italic ? "§o" : "")
|
||||||
+ (underline ? "§n" : "") + (strike ? "§m" : "");
|
+ (underline ? "§n" : "") + (strike ? "§m" : "");
|
||||||
// Sichtbare Zeichen zählen
|
// Sichtbare Zeichen z\u00e4hlen
|
||||||
int visLen = 0;
|
int visLen = 0;
|
||||||
for (char c : plain.toCharArray()) if (c != ' ') visLen++;
|
for (char c : plain.toCharArray()) if (c != ' ') visLen++;
|
||||||
int charIdx = 0;
|
int charIdx = 0;
|
||||||
for (int i = 0; i < plain.length(); i++) {
|
for (int i = 0; i < plain.length(); i++) {
|
||||||
char ch = plain.charAt(i);
|
char ch = plain.charAt(i);
|
||||||
if (ch == ' ') { sb.append(' '); continue; }
|
if (ch == ' ') { sb.append(' '); continue; }
|
||||||
// JAWa-Style: Hue gleichmäßig über alle Buchstaben verteilt, wandert pro Tick
|
// JAWa-Style: Hue gleichm\u00e4\u00dfig \u00fcber alle Buchstaben verteilt, wandert pro Tick
|
||||||
float hue = ((float) charIdx / Math.max(visLen, 1) + idx * this.waveSpeed) % 1.0f;
|
float hue = ((float) charIdx / Math.max(visLen, 1) + idx * this.waveSpeed) % 1.0f;
|
||||||
if (hue < 0) hue += 1.0f;
|
if (hue < 0) hue += 1.0f;
|
||||||
int[] rgb = waveColors != null
|
int[] rgb = waveColors != null
|
||||||
@@ -688,12 +794,12 @@ public class ScoreboardModule implements Module, Listener {
|
|||||||
// Parse: %gradient:C1:C2:...:TEXT%
|
// Parse: %gradient:C1:C2:...:TEXT%
|
||||||
String inner = input.substring(start + 10, end);
|
String inner = input.substring(start + 10, end);
|
||||||
// Letzter Teil ist der Text, vorherige Teile sind Farben
|
// Letzter Teil ist der Text, vorherige Teile sind Farben
|
||||||
// Text beginnt nach dem letzten ':' der eine Farbe abschließt
|
// Text beginnt nach dem letzten ':' der eine Farbe abschlie\u00dft
|
||||||
// Strategie: Teile von links lesen solange sie Farben sind
|
// Strategie: Teile von links lesen solange sie Farben sind
|
||||||
java.util.List<int[]> stops = new java.util.ArrayList<>();
|
java.util.List<int[]> stops = new java.util.ArrayList<>();
|
||||||
int colonIdx = 0;
|
int colonIdx = 0;
|
||||||
while (colonIdx < inner.length()) {
|
while (colonIdx < inner.length()) {
|
||||||
// Nächsten ':' suchen
|
// N\u00e4chsten ':' suchen
|
||||||
int nextColon = inner.indexOf(':', colonIdx);
|
int nextColon = inner.indexOf(':', colonIdx);
|
||||||
if (nextColon < 0) break;
|
if (nextColon < 0) break;
|
||||||
String candidate = inner.substring(colonIdx, nextColon);
|
String candidate = inner.substring(colonIdx, nextColon);
|
||||||
@@ -723,18 +829,30 @@ public class ScoreboardModule implements Module, Listener {
|
|||||||
// Gradient auf sichtbare Zeichen anwenden
|
// Gradient auf sichtbare Zeichen anwenden
|
||||||
int visLen = 0;
|
int visLen = 0;
|
||||||
for (char ch : plain.toCharArray()) if (ch != ' ') visLen++;
|
for (char ch : plain.toCharArray()) if (ch != ' ') visLen++;
|
||||||
|
if (visLen == 0) visLen = 1;
|
||||||
int charIdx = 0;
|
int charIdx = 0;
|
||||||
|
int[] lastRgb = stops.get(0);
|
||||||
for (char ch : plain.toCharArray()) {
|
for (char ch : plain.toCharArray()) {
|
||||||
if (ch == ' ') { result.append(' '); continue; }
|
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);
|
float pos = visLen <= 1 ? 0f : (float) charIdx / (visLen - 1);
|
||||||
int[] rgb = interpolateGradient(stops, pos);
|
lastRgb = interpolateGradient(stops, pos);
|
||||||
result.append('\u00A7').append('x');
|
result.append('\u00A7').append('x');
|
||||||
result.append('\u00A7').append(String.format("%02X", rgb[0]).charAt(0));
|
result.append('\u00A7').append(String.format("%02X", lastRgb[0]).charAt(0));
|
||||||
result.append('\u00A7').append(String.format("%02X", rgb[0]).charAt(1));
|
result.append('\u00A7').append(String.format("%02X", lastRgb[0]).charAt(1));
|
||||||
result.append('\u00A7').append(String.format("%02X", rgb[1]).charAt(0));
|
result.append('\u00A7').append(String.format("%02X", lastRgb[1]).charAt(0));
|
||||||
result.append('\u00A7').append(String.format("%02X", rgb[1]).charAt(1));
|
result.append('\u00A7').append(String.format("%02X", lastRgb[1]).charAt(1));
|
||||||
result.append('\u00A7').append(String.format("%02X", rgb[2]).charAt(0));
|
result.append('\u00A7').append(String.format("%02X", lastRgb[2]).charAt(0));
|
||||||
result.append('\u00A7').append(String.format("%02X", rgb[2]).charAt(1));
|
result.append('\u00A7').append(String.format("%02X", lastRgb[2]).charAt(1));
|
||||||
result.append(fmt);
|
result.append(fmt);
|
||||||
result.append(ch);
|
result.append(ch);
|
||||||
charIdx++;
|
charIdx++;
|
||||||
@@ -795,20 +913,20 @@ public class ScoreboardModule implements Module, Listener {
|
|||||||
switch (Character.toLowerCase(code)) {
|
switch (Character.toLowerCase(code)) {
|
||||||
case '0': return new int[]{ 0, 0, 0}; // §0 Schwarz
|
case '0': return new int[]{ 0, 0, 0}; // §0 Schwarz
|
||||||
case '1': return new int[]{ 0, 0, 170}; // §1 Dunkelblau
|
case '1': return new int[]{ 0, 0, 170}; // §1 Dunkelblau
|
||||||
case '2': return new int[]{ 0, 170, 0}; // §2 Dunkelgrün
|
case '2': return new int[]{ 0, 170, 0}; // §2 Dunkelgr\u00fcn
|
||||||
case '3': return new int[]{ 0, 170, 170}; // §3 Dunkeltürkis
|
case '3': return new int[]{ 0, 170, 170}; // §3 Dunkelt\u00fcrkis
|
||||||
case '4': return new int[]{170, 0, 0}; // §4 Dunkelrot
|
case '4': return new int[]{170, 0, 0}; // §4 Dunkelrot
|
||||||
case '5': return new int[]{170, 0, 170}; // §5 Lila
|
case '5': return new int[]{170, 0, 170}; // §5 Lila
|
||||||
case '6': return new int[]{255, 170, 0}; // §6 Gold
|
case '6': return new int[]{255, 170, 0}; // §6 Gold
|
||||||
case '7': return new int[]{170, 170, 170}; // §7 Grau
|
case '7': return new int[]{170, 170, 170}; // §7 Grau
|
||||||
case '8': return new int[]{ 85, 85, 85}; // §8 Dunkelgrau
|
case '8': return new int[]{ 85, 85, 85}; // §8 Dunkelgrau
|
||||||
case '9': return new int[]{ 85, 85, 255}; // §9 Blau
|
case '9': return new int[]{ 85, 85, 255}; // §9 Blau
|
||||||
case 'a': return new int[]{ 85, 255, 85}; // §a Hellgrün
|
case 'a': return new int[]{ 85, 255, 85}; // §a Hellgr\u00fcn
|
||||||
case 'b': return new int[]{ 85, 255, 255}; // §b Türkis
|
case 'b': return new int[]{ 85, 255, 255}; // §b T\u00fcrkis
|
||||||
case 'c': return new int[]{255, 85, 85}; // §c Hellrot
|
case 'c': return new int[]{255, 85, 85}; // §c Hellrot
|
||||||
case 'd': return new int[]{255, 85, 255}; // §d Hellviolett
|
case 'd': return new int[]{255, 85, 255}; // §d Hellviolett
|
||||||
case 'e': return new int[]{255, 255, 85}; // §e Gelb
|
case 'e': return new int[]{255, 255, 85}; // §e Gelb
|
||||||
case 'f': return new int[]{255, 255, 255}; // §f Weiß
|
case 'f': return new int[]{255, 255, 255}; // §f Wei\u00df
|
||||||
default: return null;
|
default: return null;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -841,7 +959,7 @@ public class ScoreboardModule implements Module, Listener {
|
|||||||
String ticketMyOpen, String ticketTotalOpen, String ticketTotalClaimed,
|
String ticketMyOpen, String ticketTotalOpen, String ticketTotalClaimed,
|
||||||
String ticketRatingGood, String ticketRatingBad, String ticketRatingPct) {
|
String ticketRatingGood, String ticketRatingBad, String ticketRatingPct) {
|
||||||
if (tpl == null) return " ";
|
if (tpl == null) return " ";
|
||||||
// PAPI-Werte zuerst einsetzen; native Tokens überschreiben sie danach
|
// PAPI-Werte zuerst einsetzen; native Tokens \u00fcberschreiben sie danach
|
||||||
String s = resolvePapiPlaceholders(tpl, currentPlayerUuid);
|
String s = resolvePapiPlaceholders(tpl, currentPlayerUuid);
|
||||||
s = s
|
s = s
|
||||||
.replace("%player%", player) .replace("%rank%", rank)
|
.replace("%player%", player) .replace("%rank%", rank)
|
||||||
@@ -972,18 +1090,18 @@ public class ScoreboardModule implements Module, Listener {
|
|||||||
* Beispiel-Output (Blick nach N): "- - - - &c&lN&r&7 - - - -"
|
* Beispiel-Output (Blick nach N): "- - - - &c&lN&r&7 - - - -"
|
||||||
*/
|
*/
|
||||||
/**
|
/**
|
||||||
* Baut den Kompass-Balken mit Sub-Grad-Auflösung.
|
* Baut den Kompass-Balken mit Sub-Grad-Aufl\u00f6sung.
|
||||||
*
|
*
|
||||||
* Das Fenster hat COMPASS_WIN Slots (z.B. 9). Jeder Slot entspricht genau
|
* Das Fenster hat COMPASS_WIN Slots (z.B. 9). Jeder Slot entspricht genau
|
||||||
* 1 Grad auf dem Kreis (COMPASS_SLOTS = 360). Dadurch verschiebt sich der
|
* 1 Grad auf dem Kreis (COMPASS_SLOTS = 360). Dadurch verschiebt sich der
|
||||||
* Balken bei jeder 1°-Änderung um genau eine Position – kein Springen.
|
* Balken bei jeder 1°-\u00c4nderung um genau eine Position – kein Springen.
|
||||||
*
|
*
|
||||||
* Jeder Slot zeigt:
|
* Jeder Slot zeigt:
|
||||||
* - 'N' / 'E' / 'S' / 'W' wenn sein Grad-Slot mit einem Himmelsrichtungs-
|
* - 'N' / 'E' / 'S' / 'W' wenn sein Grad-Slot mit einem Himmelsrichtungs-
|
||||||
* Label übereinstimmt (±0°, kein Runden)
|
* Label \u00fcbereinstimmt (±0°, kein Runden)
|
||||||
* - '|' für den Mittelpunkt (aktuelle Blickrichtung),
|
* - '|' f\u00fcr den Mittelpunkt (aktuelle Blickrichtung),
|
||||||
* falls kein Label genau trifft
|
* falls kein Label genau trifft
|
||||||
* - '·' für alle anderen Slots
|
* - '·' f\u00fcr alle anderen Slots
|
||||||
*
|
*
|
||||||
* Akzeptierte raw-Formate:
|
* Akzeptierte raw-Formate:
|
||||||
* Float-String "normYaw" (0..360): Bridge sendet normYaw = ((yaw%360)+360)%360
|
* Float-String "normYaw" (0..360): Bridge sendet normYaw = ((yaw%360)+360)%360
|
||||||
@@ -994,11 +1112,11 @@ public class ScoreboardModule implements Module, Listener {
|
|||||||
*
|
*
|
||||||
* Zeichen:
|
* Zeichen:
|
||||||
* '─' normaler Slot (grau, &8)
|
* '─' normaler Slot (grau, &8)
|
||||||
* N/E/S/W außerhalb Mitte: gelb &e
|
* N/E/S/W au\u00dferhalb Mitte: gelb &e
|
||||||
* Mitte mit Himmelsrichtung: rot+fett &c&l
|
* Mitte mit Himmelsrichtung: rot+fett &c&l
|
||||||
* Mitte ohne Himmelsrichtung: rot+fett &c&l '|'
|
* Mitte ohne Himmelsrichtung: rot+fett &c&l '|'
|
||||||
*
|
*
|
||||||
* Bridge sendet normYaw 0..360 (0 = Süden/MC-Konvention).
|
* Bridge sendet normYaw 0..360 (0 = S\u00fcden/MC-Konvention).
|
||||||
* Umrechnung: facingDeg = (normYaw + 180) % 360 → 0=N, 90=E, 180=S, 270=W
|
* Umrechnung: facingDeg = (normYaw + 180) % 360 → 0=N, 90=E, 180=S, 270=W
|
||||||
*/
|
*/
|
||||||
private static final int SCOREBOARD_WIDTH = 26; // sichtbare Breite des Scoreboards
|
private static final int SCOREBOARD_WIDTH = 26; // sichtbare Breite des Scoreboards
|
||||||
@@ -1044,10 +1162,10 @@ public class ScoreboardModule implements Module, Listener {
|
|||||||
char marker = (label != 0) ? label : '|';
|
char marker = (label != 0) ? label : '|';
|
||||||
sb.append("&c&l").append(marker).append("&r&8");
|
sb.append("&c&l").append(marker).append("&r&8");
|
||||||
} else if (label != 0) {
|
} else if (label != 0) {
|
||||||
// Himmelsrichtung außerhalb Mitte: gelb, gut sichtbar
|
// Himmelsrichtung au\u00dferhalb Mitte: gelb, gut sichtbar
|
||||||
sb.append("&e").append(label).append("&8");
|
sb.append("&e").append(label).append("&8");
|
||||||
} else {
|
} else {
|
||||||
sb.append('-'); // ASCII-Strich, sicher für alle MC-Versionen
|
sb.append('-'); // ASCII-Strich, sicher f\u00fcr alle MC-Versionen
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
// Kompass zentrieren: Leerzeichen links = (Scoreboard-Breite - Kompass-Breite) / 2
|
// Kompass zentrieren: Leerzeichen links = (Scoreboard-Breite - Kompass-Breite) / 2
|
||||||
@@ -1062,8 +1180,8 @@ public class ScoreboardModule implements Module, Listener {
|
|||||||
* Baut den News-Ticker: Text gleitet von rechts nach links durch ein fixes Fenster.
|
* Baut den News-Ticker: Text gleitet von rechts nach links durch ein fixes Fenster.
|
||||||
*
|
*
|
||||||
* Das Fenster ist IMMER exakt newsWidth Zeichen breit – Scoreboard-Breite konstant.
|
* Das Fenster ist IMMER exakt newsWidth Zeichen breit – Scoreboard-Breite konstant.
|
||||||
* Text erscheint von rechts, läuft durch, verschwindet links.
|
* Text erscheint von rechts, l\u00e4uft durch, verschwindet links.
|
||||||
* Dann Pause (Leerzeichen) bevor der Text wieder von rechts einläuft.
|
* Dann Pause (Leerzeichen) bevor der Text wieder von rechts einl\u00e4uft.
|
||||||
*
|
*
|
||||||
* newsPrefix ist optional – leer lassen in Config zum Deaktivieren.
|
* newsPrefix ist optional – leer lassen in Config zum Deaktivieren.
|
||||||
*/
|
*/
|
||||||
@@ -1081,7 +1199,7 @@ public class ScoreboardModule implements Module, Listener {
|
|||||||
int cycleLen = plain.length() + gap;
|
int cycleLen = plain.length() + gap;
|
||||||
int pos = offset % cycleLen;
|
int pos = offset % cycleLen;
|
||||||
|
|
||||||
// Virtuelles Band: plain + 4 Leerzeichen, läuft zyklisch
|
// Virtuelles Band: plain + 4 Leerzeichen, l\u00e4uft zyklisch
|
||||||
// Fenster zeigt winWidth Zeichen aus dem Band
|
// Fenster zeigt winWidth Zeichen aus dem Band
|
||||||
// Band-Position des ersten Fensterzeichens: pos - winWidth + 1
|
// Band-Position des ersten Fensterzeichens: pos - winWidth + 1
|
||||||
StringBuilder window = new StringBuilder();
|
StringBuilder window = new StringBuilder();
|
||||||
@@ -1104,12 +1222,12 @@ public class ScoreboardModule implements Module, Listener {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private String getTps(UUID id) {
|
private String getTps(UUID id) {
|
||||||
// Primär: TPS vom Backend-Server (per POST /scoreboard/tps gesendet)
|
// Prim\u00e4r: TPS vom Backend-Server (per POST /scoreboard/tps gesendet)
|
||||||
Double t = playerTps.get(id);
|
Double t = playerTps.get(id);
|
||||||
if (t != null) {
|
if (t != null) {
|
||||||
return new DecimalFormat("0.0").format(Math.min(20.0, t));
|
return new DecimalFormat("0.0").format(Math.min(20.0, t));
|
||||||
}
|
}
|
||||||
// FIX: Fallback auf Proxy-eigene TPS aus NetworkInfoModule (immer verfügbar)
|
// FIX: Fallback auf Proxy-eigene TPS aus NetworkInfoModule (immer verf\u00fcgbar)
|
||||||
if (networkInfoModule != null && networkInfoModule.isEnabled()) {
|
if (networkInfoModule != null && networkInfoModule.isEnabled()) {
|
||||||
double proxyTps = networkInfoModule.getProxyTps();
|
double proxyTps = networkInfoModule.getProxyTps();
|
||||||
return new DecimalFormat("0.0").format(Math.min(20.0, proxyTps)) + " (P)";
|
return new DecimalFormat("0.0").format(Math.min(20.0, proxyTps)) + " (P)";
|
||||||
@@ -1123,7 +1241,7 @@ public class ScoreboardModule implements Module, Listener {
|
|||||||
+ "MB/" + (m.getHeapMemoryUsage().getMax() / 1048576) + "MB";
|
+ "MB/" + (m.getHeapMemoryUsage().getMax() / 1048576) + "MB";
|
||||||
}
|
}
|
||||||
|
|
||||||
// ── Component Builder (Hex-Farb-Support für Scoreboard) ─────────────────────
|
// ── Component Builder (Hex-Farb-Support f\u00fcr Scoreboard) ─────────────────────
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Wandelt einen bereits mit c() prozessierten String (§-Codes + §x§R§R§G§G§B§B)
|
* Wandelt einen bereits mit c() prozessierten String (§-Codes + §x§R§R§G§G§B§B)
|
||||||
@@ -1160,7 +1278,7 @@ public class ScoreboardModule implements Module, Listener {
|
|||||||
+ text.charAt(i+7) + text.charAt(i+9)
|
+ text.charAt(i+7) + text.charAt(i+9)
|
||||||
+ text.charAt(i+11) + text.charAt(i+13);
|
+ text.charAt(i+11) + text.charAt(i+13);
|
||||||
currentColor = net.md_5.bungee.api.ChatColor.of("#" + hex);
|
currentColor = net.md_5.bungee.api.ChatColor.of("#" + hex);
|
||||||
// Formatierungen NICHT zurücksetzen – Bold/Italic bleiben erhalten
|
// Formatierungen NICHT zur\u00fccksetzen – Bold/Italic bleiben erhalten
|
||||||
} catch (Exception ignored) {}
|
} catch (Exception ignored) {}
|
||||||
i += 14;
|
i += 14;
|
||||||
continue;
|
continue;
|
||||||
@@ -1222,7 +1340,7 @@ public class ScoreboardModule implements Module, Listener {
|
|||||||
|
|
||||||
// ══════════════════════════════════════════════════════════════════════════
|
// ══════════════════════════════════════════════════════════════════════════
|
||||||
// Farb-Parser: Birdflop-kompatibel
|
// Farb-Parser: Birdflop-kompatibel
|
||||||
// Unterstützte Formate (alle gleichzeitig nutzbar):
|
// Unterst\u00fctzte Formate (alle gleichzeitig nutzbar):
|
||||||
//
|
//
|
||||||
// &#RRGGBB → Pro-Zeichen Hex (Birdflop Standard-Output)
|
// &#RRGGBB → Pro-Zeichen Hex (Birdflop Standard-Output)
|
||||||
// {#RRGGBB} → Bracket-Format
|
// {#RRGGBB} → Bracket-Format
|
||||||
@@ -1249,7 +1367,7 @@ public class ScoreboardModule implements Module, Listener {
|
|||||||
|
|
||||||
private static String parseMiniMessage(String text) {
|
private static String parseMiniMessage(String text) {
|
||||||
if (text == null || !text.contains("<")) return text == null ? "" : text;
|
if (text == null || !text.contains("<")) return text == null ? "" : text;
|
||||||
// gradient-Tags als erstes, weil sie anderen Text enthalten können
|
// gradient-Tags als erstes, weil sie anderen Text enthalten k\u00f6nnen
|
||||||
text = parseGradientTags(text);
|
text = parseGradientTags(text);
|
||||||
// shadow-Tags
|
// shadow-Tags
|
||||||
text = parseShadowTags(text);
|
text = parseShadowTags(text);
|
||||||
@@ -1268,7 +1386,7 @@ public class ScoreboardModule implements Module, Listener {
|
|||||||
int start = text.indexOf("<gradient:", i);
|
int start = text.indexOf("<gradient:", i);
|
||||||
if (start < 0) { result.append(text, i, text.length()); break; }
|
if (start < 0) { result.append(text, i, text.length()); break; }
|
||||||
result.append(text, i, start);
|
result.append(text, i, start);
|
||||||
// Schließendes > suchen (mit Tiefenzähler für verschachtelte <...>)
|
// Schlie\u00dfendes > suchen (mit Tiefenz\u00e4hler f\u00fcr verschachtelte <...>)
|
||||||
int end = findClosingAngle(text, start + 1);
|
int end = findClosingAngle(text, start + 1);
|
||||||
if (end < 0) { result.append(text, i, text.length()); break; }
|
if (end < 0) { result.append(text, i, text.length()); break; }
|
||||||
String inner = text.substring(start + 1, end); // "gradient:#C1:#C2:TEXT"
|
String inner = text.substring(start + 1, end); // "gradient:#C1:#C2:TEXT"
|
||||||
@@ -1279,8 +1397,8 @@ public class ScoreboardModule implements Module, Listener {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Parst "gradient:#C1:#C2:#C3:TEXT" → eingefärbten Text.
|
* Parst "gradient:#C1:#C2:#C3:TEXT" → eingef\u00e4rbten Text.
|
||||||
* TEXT darf selbst wieder §-Codes oder &-Codes enthalten (z.B. &l für Bold).
|
* TEXT darf selbst wieder §-Codes oder &-Codes enthalten (z.B. &l f\u00fcr Bold).
|
||||||
*/
|
*/
|
||||||
private static String applyGradientTag(String inner) {
|
private static String applyGradientTag(String inner) {
|
||||||
// inner = "gradient:COLOR:COLOR:...:TEXT"
|
// inner = "gradient:COLOR:COLOR:...:TEXT"
|
||||||
@@ -1309,14 +1427,14 @@ public class ScoreboardModule implements Module, Listener {
|
|||||||
}
|
}
|
||||||
if (colors.size() < 2) return textSb.toString();
|
if (colors.size() < 2) return textSb.toString();
|
||||||
|
|
||||||
// Shadow-Tags im Text zuerst auflösen (können im Gradient-Text stecken)
|
// Shadow-Tags im Text zuerst aufl\u00f6sen (k\u00f6nnen im Gradient-Text stecken)
|
||||||
String rawText = parseShadowTags(textSb.toString());
|
String rawText = parseShadowTags(textSb.toString());
|
||||||
return applyGradient(rawText, colors);
|
return applyGradient(rawText, colors);
|
||||||
}
|
}
|
||||||
|
|
||||||
private static String applyGradient(String text, java.util.List<String> colorStops) {
|
private static String applyGradient(String text, java.util.List<String> colorStops) {
|
||||||
if (text == null || text.isEmpty()) return text;
|
if (text == null || text.isEmpty()) return text;
|
||||||
// §-Codes und &-Codes aus Text herausfiltern für Längenberechnung
|
// §-Codes und &-Codes aus Text herausfiltern f\u00fcr L\u00e4ngenberechnung
|
||||||
String plain = text
|
String plain = text
|
||||||
.replaceAll("\u00A7[0-9a-fk-orx]", "")
|
.replaceAll("\u00A7[0-9a-fk-orx]", "")
|
||||||
.replaceAll("&[0-9a-fA-Fk-orK-OR]", "")
|
.replaceAll("&[0-9a-fA-Fk-orK-OR]", "")
|
||||||
@@ -1334,7 +1452,7 @@ public class ScoreboardModule implements Module, Listener {
|
|||||||
while (ci < text.length()) {
|
while (ci < text.length()) {
|
||||||
char ch = text.charAt(ci);
|
char ch = text.charAt(ci);
|
||||||
|
|
||||||
// §x§R§R§G§G§B§B durchreichen (bereits aufgelöste Hex-Farbe z.B. von shadow)
|
// §x§R§R§G§G§B§B durchreichen (bereits aufgel\u00f6ste Hex-Farbe z.B. von shadow)
|
||||||
if (ch == '\u00A7' && ci + 1 < text.length() && text.charAt(ci + 1) == 'x') {
|
if (ch == '\u00A7' && ci + 1 < text.length() && text.charAt(ci + 1) == 'x') {
|
||||||
// Lese die 12 folgenden Zeichen (§x + 6x §digit)
|
// Lese die 12 folgenden Zeichen (§x + 6x §digit)
|
||||||
if (ci + 13 < text.length() + 1) {
|
if (ci + 13 < text.length() + 1) {
|
||||||
@@ -1409,7 +1527,7 @@ public class ScoreboardModule implements Module, Listener {
|
|||||||
text = text.replace("<st>", "&m").replace("</st>", "&r");
|
text = text.replace("<st>", "&m").replace("</st>", "&r");
|
||||||
text = text.replace("<obf>", "&k").replace("</obf>", "&r");
|
text = text.replace("<obf>", "&k").replace("</obf>", "&r");
|
||||||
text = text.replace("<reset>", "&r").replace("</reset>", "");
|
text = text.replace("<reset>", "&r").replace("</reset>", "");
|
||||||
// Closing-Tags entfernen (werden nach Verarbeitung nicht mehr benötigt)
|
// Closing-Tags entfernen (werden nach Verarbeitung nicht mehr ben\u00f6tigt)
|
||||||
text = text.replaceAll("</gradient>", "");
|
text = text.replaceAll("</gradient>", "");
|
||||||
text = text.replaceAll("</shadow>", "");
|
text = text.replaceAll("</shadow>", "");
|
||||||
text = text.replaceAll("</color>", "");
|
text = text.replaceAll("</color>", "");
|
||||||
@@ -1513,8 +1631,8 @@ public class ScoreboardModule implements Module, Listener {
|
|||||||
private static int clamp(int v) { return Math.max(0, Math.min(255, v)); }
|
private static int clamp(int v) { return Math.max(0, Math.min(255, v)); }
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Findet das schließende '>' für ein Tag das bei fromIndex beginnt.
|
* Findet das schlie\u00dfende '>' f\u00fcr ein Tag das bei fromIndex beginnt.
|
||||||
* Berücksichtigt verschachtelte <...>.
|
* Ber\u00fccksichtigt verschachtelte <...>.
|
||||||
*/
|
*/
|
||||||
private static int findClosingAngle(String text, int fromIndex) {
|
private static int findClosingAngle(String text, int fromIndex) {
|
||||||
int depth = 0;
|
int depth = 0;
|
||||||
@@ -1556,7 +1674,7 @@ public class ScoreboardModule implements Module, Listener {
|
|||||||
"scoreboard.ticker.speed=1\n" +
|
"scoreboard.ticker.speed=1\n" +
|
||||||
"\n" +
|
"\n" +
|
||||||
"scoreboard.rainbow.enabled=true\n" +
|
"scoreboard.rainbow.enabled=true\n" +
|
||||||
"# wave=fließende Welle, chars=Regenbogen pro Buchstabe, line=eine Farbe\n" +
|
"# wave=flie\u00dfende Welle, chars=Regenbogen pro Buchstabe, line=eine Farbe\n" +
|
||||||
"scoreboard.rainbow.mode=wave\n" +
|
"scoreboard.rainbow.mode=wave\n" +
|
||||||
"# Wellengeschwindigkeit: 1=sehr langsam, 10=normal, 50=schnell\n" +
|
"# Wellengeschwindigkeit: 1=sehr langsam, 10=normal, 50=schnell\n" +
|
||||||
"scoreboard.rainbow.speed=10\n" +
|
"scoreboard.rainbow.speed=10\n" +
|
||||||
@@ -1674,6 +1792,7 @@ public class ScoreboardModule implements Module, Listener {
|
|||||||
}
|
}
|
||||||
java.util.function.BiFunction<String,String,String> g = (k,d) -> map.getOrDefault(k, d);
|
java.util.function.BiFunction<String,String,String> g = (k,d) -> map.getOrDefault(k, d);
|
||||||
enabled = Boolean.parseBoolean(g.apply("scoreboard.enabled", "true"));
|
enabled = Boolean.parseBoolean(g.apply("scoreboard.enabled", "true"));
|
||||||
|
nametagEnabled = Boolean.parseBoolean(g.apply("nametag.enabled", "true"));
|
||||||
updateInterval = Math.max(250, pi(g.apply("scoreboard.update_interval", "500"), 500));
|
updateInterval = Math.max(250, pi(g.apply("scoreboard.update_interval", "500"), 500));
|
||||||
title = g.apply("scoreboard.title", "&6&lViper Network");
|
title = g.apply("scoreboard.title", "&6&lViper Network");
|
||||||
adminTitle = g.apply("scoreboard.admin_title", "&c&l[Admin] &4&lPanel");
|
adminTitle = g.apply("scoreboard.admin_title", "&c&l[Admin] &4&lPanel");
|
||||||
@@ -1810,7 +1929,7 @@ public class ScoreboardModule implements Module, Listener {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Entfernt ein Scoreboard-Objective und alle zugehörigen Teams sauber vom Client.
|
* Entfernt ein Scoreboard-Objective und alle zugeh\u00f6rigen Teams sauber vom Client.
|
||||||
* Muss aufgerufen werden bevor ein anderes Objective aktiviert wird,
|
* Muss aufgerufen werden bevor ein anderes Objective aktiviert wird,
|
||||||
* sonst crasht der Client beim erneuten Team-CREATE.
|
* sonst crasht der Client beim erneuten Team-CREATE.
|
||||||
*
|
*
|
||||||
@@ -1819,7 +1938,7 @@ public class ScoreboardModule implements Module, Listener {
|
|||||||
* @param teamPrefix Team-Prefix (z.B. "vt" oder "vta")
|
* @param teamPrefix Team-Prefix (z.B. "vt" oder "vta")
|
||||||
*/
|
*/
|
||||||
private void removeObjectiveAndTeams(ProxiedPlayer p, String objName, String teamPrefix) {
|
private void removeObjectiveAndTeams(ProxiedPlayer p, String objName, String teamPrefix) {
|
||||||
// 1. Alle Teams löschen (Mode 1 = REMOVE)
|
// 1. Alle Teams l\u00f6schen (Mode 1 = REMOVE)
|
||||||
for (int i = 0; i < 15; i++) {
|
for (int i = 0; i < 15; i++) {
|
||||||
try {
|
try {
|
||||||
Team team = new Team();
|
Team team = new Team();
|
||||||
@@ -1849,7 +1968,7 @@ public class ScoreboardModule implements Module, Listener {
|
|||||||
*/
|
*/
|
||||||
private static final List<String> SB_SUBS = Arrays.asList("hide", "show", "player", "admin", "supporter");
|
private static final List<String> SB_SUBS = Arrays.asList("hide", "show", "player", "admin", "supporter");
|
||||||
|
|
||||||
/** Tab-Completion für /scoreboard via TabCompleteEvent */
|
/** Tab-Completion f\u00fcr /scoreboard via TabCompleteEvent */
|
||||||
@EventHandler
|
@EventHandler
|
||||||
public void onTabComplete(TabCompleteEvent event) {
|
public void onTabComplete(TabCompleteEvent event) {
|
||||||
if (!(event.getSender() instanceof ProxiedPlayer)) return;
|
if (!(event.getSender() instanceof ProxiedPlayer)) return;
|
||||||
@@ -1887,7 +2006,7 @@ public class ScoreboardModule implements Module, Listener {
|
|||||||
public void execute(CommandSender sender, String[] args) {
|
public void execute(CommandSender sender, String[] args) {
|
||||||
if (!(sender instanceof ProxiedPlayer)) {
|
if (!(sender instanceof ProxiedPlayer)) {
|
||||||
sender.sendMessage(new net.md_5.bungee.api.chat.TextComponent(
|
sender.sendMessage(new net.md_5.bungee.api.chat.TextComponent(
|
||||||
ChatColor.RED + "Nur für Spieler."));
|
ChatColor.RED + "Nur f\u00fcr Spieler."));
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
ProxiedPlayer p = (ProxiedPlayer) sender;
|
ProxiedPlayer p = (ProxiedPlayer) sender;
|
||||||
|
|||||||
@@ -53,7 +53,7 @@ public class ServerSwitcherModule implements Module {
|
|||||||
private List<String> aliases = new ArrayList<>(Arrays.asList("wechsel", "switch"));
|
private List<String> aliases = new ArrayList<>(Arrays.asList("wechsel", "switch"));
|
||||||
private List<String> serverWhitelist = new ArrayList<>();
|
private List<String> serverWhitelist = new ArrayList<>();
|
||||||
|
|
||||||
private String colorHeader = "&8&m---&r &6&lServer-Menü &8&m---";
|
private String colorHeader = "&8&m---&r &6&lServer-Men\u00fc &8&m---";
|
||||||
private String colorEntry = "&7>> &e";
|
private String colorEntry = "&7>> &e";
|
||||||
private String colorOnline = "&a";
|
private String colorOnline = "&a";
|
||||||
private String colorOffline = "&c";
|
private String colorOffline = "&c";
|
||||||
@@ -103,7 +103,7 @@ public class ServerSwitcherModule implements Module {
|
|||||||
"# Optionale Whitelist (leer = alle BungeeCord-Server)\n" +
|
"# Optionale Whitelist (leer = alle BungeeCord-Server)\n" +
|
||||||
"# Beispiel: serverswitcher.servers=lobby,citybuild,survival\n" +
|
"# Beispiel: serverswitcher.servers=lobby,citybuild,survival\n" +
|
||||||
"serverswitcher.servers=\n\n" +
|
"serverswitcher.servers=\n\n" +
|
||||||
"serverswitcher.color.header=&8&m---&r &6&lServer-Menü &8&m---\n" +
|
"serverswitcher.color.header=&8&m---&r &6&lServer-Men\u00fc &8&m---\n" +
|
||||||
"serverswitcher.color.entry=&7>> &e\n" +
|
"serverswitcher.color.entry=&7>> &e\n" +
|
||||||
"serverswitcher.color.online=&a\n" +
|
"serverswitcher.color.online=&a\n" +
|
||||||
"serverswitcher.color.offline=&c\n" +
|
"serverswitcher.color.offline=&c\n" +
|
||||||
@@ -178,7 +178,7 @@ public class ServerSwitcherModule implements Module {
|
|||||||
@Override
|
@Override
|
||||||
public void execute(CommandSender sender, String[] args) {
|
public void execute(CommandSender sender, String[] args) {
|
||||||
if (!(sender instanceof ProxiedPlayer)) {
|
if (!(sender instanceof ProxiedPlayer)) {
|
||||||
sender.sendMessage(c("&cDieser Befehl ist nur für Spieler verfügbar."));
|
sender.sendMessage(c("&cDieser Befehl ist nur f\u00fcr Spieler verf\u00fcgbar."));
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -242,7 +242,7 @@ public class ServerSwitcherModule implements Module {
|
|||||||
}
|
}
|
||||||
|
|
||||||
player.sendMessage(c("&8&m----------------------------"));
|
player.sendMessage(c("&8&m----------------------------"));
|
||||||
player.sendMessage(c("&7Tipp: &e/" + commandName + " <Server> &7für direkten Wechsel"));
|
player.sendMessage(c("&7Tipp: &e/" + commandName + " <Server> &7f\u00fcr direkten Wechsel"));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -30,7 +30,7 @@ public class TablistModule implements Module, Listener {
|
|||||||
|
|
||||||
private static final String CONFIG_FILE = "tablist.properties";
|
private static final String CONFIG_FILE = "tablist.properties";
|
||||||
|
|
||||||
// Leerer Skin (grauer Kopf) für Platzhalter-Slots
|
// Leerer Skin (grauer Kopf) f\u00fcr Platzhalter-Slots
|
||||||
private static final net.md_5.bungee.protocol.data.Property[] EMPTY_SKIN = {
|
private static final net.md_5.bungee.protocol.data.Property[] EMPTY_SKIN = {
|
||||||
new net.md_5.bungee.protocol.data.Property(
|
new net.md_5.bungee.protocol.data.Property(
|
||||||
"textures",
|
"textures",
|
||||||
@@ -62,7 +62,7 @@ public class TablistModule implements Module, Listener {
|
|||||||
private String footerLine2 = " &7Discord: &ediscord.viper-network.de &8| &7Shop: &eviper-network.de/shop";
|
private String footerLine2 = " &7Discord: &ediscord.viper-network.de &8| &7Shop: &eviper-network.de/shop";
|
||||||
private String footerLine3 = "&8&m" + rep('\u2501', 53);
|
private String footerLine3 = "&8&m" + rep('\u2501', 53);
|
||||||
|
|
||||||
private String compactHeader1 = "&6&lViper Network &8• &2Hallo, &a%player%&7! &6Schön dass du da bist!";
|
private String compactHeader1 = "&6&lViper Network &8• &2Hallo, &a%player%&7! &6Sch\u00f6n dass du da bist!";
|
||||||
private String compactHeader2 = "";
|
private String compactHeader2 = "";
|
||||||
private String compactHeader3 = "";
|
private String compactHeader3 = "";
|
||||||
private boolean compactHeader1Spacer = false;
|
private boolean compactHeader1Spacer = false;
|
||||||
@@ -197,7 +197,7 @@ public class TablistModule implements Module, Listener {
|
|||||||
if (skin != null && skin.length > 0) skinCache.put(p.getUniqueId(), skin);
|
if (skin != null && skin.length > 0) skinCache.put(p.getUniqueId(), skin);
|
||||||
disableBungeeTabHandler(p);
|
disableBungeeTabHandler(p);
|
||||||
// BungeeCord resettet tabListHandler nach PostLoginEvent intern nochmals.
|
// BungeeCord resettet tabListHandler nach PostLoginEvent intern nochmals.
|
||||||
// Deshalb: 0.5s, 1s, 2s nacheinander überschreiben.
|
// Deshalb: 0.5s, 1s, 2s nacheinander \u00fcberschreiben.
|
||||||
long[] delays = {500L, 1000L, 2000L};
|
long[] delays = {500L, 1000L, 2000L};
|
||||||
for (long delayMs : delays) {
|
for (long delayMs : delays) {
|
||||||
ProxyServer.getInstance().getScheduler().schedule(plugin, () -> {
|
ProxyServer.getInstance().getScheduler().schedule(plugin, () -> {
|
||||||
@@ -218,8 +218,8 @@ public class TablistModule implements Module, Listener {
|
|||||||
// Sofort deaktivieren
|
// Sofort deaktivieren
|
||||||
disableBungeeTabHandler(switched);
|
disableBungeeTabHandler(switched);
|
||||||
|
|
||||||
// BungeeCord setzt den tabListHandler nach ServerSwitch intern mehrfach zurück.
|
// BungeeCord setzt den tabListHandler nach ServerSwitch intern mehrfach zur\u00fcck.
|
||||||
// Deshalb: 0.5s, 1s, 2s, 3s nacheinander überschreiben + tablist neu senden.
|
// Deshalb: 0.5s, 1s, 2s, 3s nacheinander \u00fcberschreiben + tablist neu senden.
|
||||||
long[] delays = {500L, 1000L, 2000L, 3000L};
|
long[] delays = {500L, 1000L, 2000L, 3000L};
|
||||||
for (long delayMs : delays) {
|
for (long delayMs : delays) {
|
||||||
ProxyServer.getInstance().getScheduler().schedule(plugin, () -> {
|
ProxyServer.getInstance().getScheduler().schedule(plugin, () -> {
|
||||||
@@ -238,6 +238,7 @@ public class TablistModule implements Module, Listener {
|
|||||||
public void onDisconnect(PlayerDisconnectEvent e) {
|
public void onDisconnect(PlayerDisconnectEvent e) {
|
||||||
if (!enabled) return;
|
if (!enabled) return;
|
||||||
skinCache.remove(e.getPlayer().getUniqueId());
|
skinCache.remove(e.getPlayer().getUniqueId());
|
||||||
|
net.viper.status.StatusAPI.playerAfk.remove(e.getPlayer().getUniqueId());
|
||||||
ProxyServer.getInstance().getScheduler().schedule(plugin, () -> {
|
ProxyServer.getInstance().getScheduler().schedule(plugin, () -> {
|
||||||
for (ProxiedPlayer viewer : ProxyServer.getInstance().getPlayers()) {
|
for (ProxiedPlayer viewer : ProxyServer.getInstance().getPlayers()) {
|
||||||
try { removeFakeSlots(viewer); } catch (Exception ignored) {}
|
try { removeFakeSlots(viewer); } catch (Exception ignored) {}
|
||||||
@@ -305,8 +306,8 @@ public class TablistModule implements Module, Listener {
|
|||||||
new net.md_5.bungee.api.chat.TextComponent(hComps),
|
new net.md_5.bungee.api.chat.TextComponent(hComps),
|
||||||
new net.md_5.bungee.api.chat.TextComponent(fComps));
|
new net.md_5.bungee.api.chat.TextComponent(fComps));
|
||||||
// Erst Slots senden (ADD_PLAYER), DANN echte Spieler verstecken (UPDATE_LISTED=false).
|
// Erst Slots senden (ADD_PLAYER), DANN echte Spieler verstecken (UPDATE_LISTED=false).
|
||||||
// Umgekehrte Reihenfolge führt zu "Ignoring player info update for unknown player"
|
// Umgekehrte Reihenfolge f\u00fchrt zu "Ignoring player info update for unknown player"
|
||||||
// weil der Client UPDATE_LISTED für unbekannte UUIDs ignoriert.
|
// weil der Client UPDATE_LISTED f\u00fcr unbekannte UUIDs ignoriert.
|
||||||
sendSlots(viewer, buildItems(viewer));
|
sendSlots(viewer, buildItems(viewer));
|
||||||
hideRealPlayers(viewer);
|
hideRealPlayers(viewer);
|
||||||
} catch (Exception ex) {
|
} catch (Exception ex) {
|
||||||
@@ -365,9 +366,9 @@ public class TablistModule implements Module, Listener {
|
|||||||
|
|
||||||
boolean compact = "compact".equalsIgnoreCase(layoutMode);
|
boolean compact = "compact".equalsIgnoreCase(layoutMode);
|
||||||
// Ob der Spalten-Header einen Slot belegt:
|
// Ob der Spalten-Header einen Slot belegt:
|
||||||
// "full" = explizit aktiviert, "none"/"small" = früher deaktiviert.
|
// "full" = explizit aktiviert, "none"/"small" = fr\u00fcher deaktiviert.
|
||||||
// FIX: Im Server-Modus immer den Servernamen in Zeile 0 schreiben,
|
// FIX: Im Server-Modus immer den Servernamen in Zeile 0 schreiben,
|
||||||
// sonst weiß der Spieler nicht welche Spalte welcher Server ist.
|
// sonst wei\u00df der Spieler nicht welche Spalte welcher Server ist.
|
||||||
boolean useSlotHeader = !"none".equalsIgnoreCase(columnHeaderMode);
|
boolean useSlotHeader = !"none".equalsIgnoreCase(columnHeaderMode);
|
||||||
|
|
||||||
// Info-Spalte (nur classic)
|
// Info-Spalte (nur classic)
|
||||||
@@ -400,7 +401,7 @@ public class TablistModule implements Module, Listener {
|
|||||||
if ("custom".equalsIgnoreCase(playerDisplayMode)) {
|
if ("custom".equalsIgnoreCase(playerDisplayMode)) {
|
||||||
// ── Custom-Modus: alle Spieler zusammen, nach Rang sortiert ──────────
|
// ── Custom-Modus: alle Spieler zusammen, nach Rang sortiert ──────────
|
||||||
// Minecraft Tab-Grid ist spaltenweise aufgebaut (Spalte 1 = Slots 0-19, Spalte 2 = Slots 20-39)
|
// Minecraft Tab-Grid ist spaltenweise aufgebaut (Spalte 1 = Slots 0-19, Spalte 2 = Slots 20-39)
|
||||||
// "Links nach rechts" = Zeile 0 über alle Spalten, dann Zeile 1 usw.
|
// "Links nach rechts" = Zeile 0 \u00fcber alle Spalten, dann Zeile 1 usw.
|
||||||
// Spieler 0 → Spalte 0 Zeile 0, Spieler 1 → Spalte 1 Zeile 0, Spieler 2 → Spalte 2 Zeile 0
|
// Spieler 0 → Spalte 0 Zeile 0, Spieler 1 → Spalte 1 Zeile 0, Spieler 2 → Spalte 2 Zeile 0
|
||||||
// Spieler 3 → Spalte 0 Zeile 1, Spieler 4 → Spalte 1 Zeile 1 usw.
|
// Spieler 3 → Spalte 0 Zeile 1, Spieler 4 → Spalte 1 Zeile 1 usw.
|
||||||
List<ProxiedPlayer> allPlayers = new ArrayList<>(ProxyServer.getInstance().getPlayers());
|
List<ProxiedPlayer> allPlayers = new ArrayList<>(ProxyServer.getInstance().getPlayers());
|
||||||
@@ -409,7 +410,7 @@ public class TablistModule implements Module, Listener {
|
|||||||
int usedCols = columns - startCol;
|
int usedCols = columns - startCol;
|
||||||
int maxSlots = usedCols * rows;
|
int maxSlots = usedCols * rows;
|
||||||
int playerIdx = 0;
|
int playerIdx = 0;
|
||||||
// Zeile für Zeile iterieren, innerhalb jeder Zeile alle Spalten
|
// Zeile f\u00fcr Zeile iterieren, innerhalb jeder Zeile alle Spalten
|
||||||
outer:
|
outer:
|
||||||
for (int row = 0; row < rows; row++) {
|
for (int row = 0; row < rows; row++) {
|
||||||
for (int col = startCol; col < columns; col++) {
|
for (int col = startCol; col < columns; col++) {
|
||||||
@@ -417,11 +418,16 @@ public class TablistModule implements Module, Listener {
|
|||||||
ProxiedPlayer p = allPlayers.get(playerIdx++);
|
ProxiedPlayer p = allPlayers.get(playerIdx++);
|
||||||
int base = col * rows;
|
int base = col * rows;
|
||||||
String prefix = getLuckPermsPrefix(p);
|
String prefix = getLuckPermsPrefix(p);
|
||||||
|
boolean isAfk = Boolean.TRUE.equals(net.viper.status.StatusAPI.playerAfk.get(p.getUniqueId()));
|
||||||
String symbol = getServerSymbol(p);
|
String symbol = getServerSymbol(p);
|
||||||
String nameStr = p.getName() + (symbol.isEmpty() ? "" : " " + symbol);
|
String nameStr = p.getName() + (symbol.isEmpty() ? "" : " " + symbol);
|
||||||
set(texts, base, row, prefix.isEmpty()
|
if (isAfk) {
|
||||||
? c("&7" + nameStr)
|
set(texts, base, row, c("&7[AFK] &r" + nameStr));
|
||||||
: c(prefix + "&r " + nameStr));
|
} else {
|
||||||
|
set(texts, base, row, prefix.isEmpty()
|
||||||
|
? c("&7" + nameStr)
|
||||||
|
: c(prefix + "&r " + nameStr));
|
||||||
|
}
|
||||||
net.md_5.bungee.protocol.data.Property[] skin = skinCache.get(p.getUniqueId());
|
net.md_5.bungee.protocol.data.Property[] skin = skinCache.get(p.getUniqueId());
|
||||||
skins[base + row] = (skin != null && skin.length > 0) ? skin : EMPTY_SKIN;
|
skins[base + row] = (skin != null && skin.length > 0) ? skin : EMPTY_SKIN;
|
||||||
pings[base + row] = p.getPing() < 0 ? 1 : p.getPing();
|
pings[base + row] = p.getPing() < 0 ? 1 : p.getPing();
|
||||||
@@ -443,11 +449,16 @@ public class TablistModule implements Module, Listener {
|
|||||||
for (ProxiedPlayer p : sortPlayersByRank(new ArrayList<>(si.getPlayers()))) {
|
for (ProxiedPlayer p : sortPlayersByRank(new ArrayList<>(si.getPlayers()))) {
|
||||||
if (row >= rows) break;
|
if (row >= rows) break;
|
||||||
String prefix = getLuckPermsPrefix(p);
|
String prefix = getLuckPermsPrefix(p);
|
||||||
|
boolean isAfk = Boolean.TRUE.equals(net.viper.status.StatusAPI.playerAfk.get(p.getUniqueId()));
|
||||||
String symbol = getServerSymbol(p);
|
String symbol = getServerSymbol(p);
|
||||||
String nameStr = p.getName() + (symbol.isEmpty() ? "" : " " + symbol);
|
String nameStr = p.getName() + (symbol.isEmpty() ? "" : " " + symbol);
|
||||||
set(texts, base, row, prefix.isEmpty()
|
if (isAfk) {
|
||||||
? c("&7" + nameStr)
|
set(texts, base, row, c("&7[AFK] &r" + nameStr));
|
||||||
: c(prefix + "&r " + nameStr));
|
} else {
|
||||||
|
set(texts, base, row, prefix.isEmpty()
|
||||||
|
? c("&7" + nameStr)
|
||||||
|
: c(prefix + "&r " + nameStr));
|
||||||
|
}
|
||||||
net.md_5.bungee.protocol.data.Property[] skin = skinCache.get(p.getUniqueId());
|
net.md_5.bungee.protocol.data.Property[] skin = skinCache.get(p.getUniqueId());
|
||||||
skins[base + row] = (skin != null && skin.length > 0) ? skin : EMPTY_SKIN;
|
skins[base + row] = (skin != null && skin.length > 0) ? skin : EMPTY_SKIN;
|
||||||
pings[base + row] = p.getPing() < 0 ? 1 : p.getPing();
|
pings[base + row] = p.getPing() < 0 ? 1 : p.getPing();
|
||||||
@@ -496,8 +507,8 @@ public class TablistModule implements Module, Listener {
|
|||||||
sendPacketQueuedMethod.invoke(viewer, playerPkt);
|
sendPacketQueuedMethod.invoke(viewer, playerPkt);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Server-UUIDs (BungeeCord schreibt pro Server 2 Einträge in die Tablist):
|
// Server-UUIDs (BungeeCord schreibt pro Server 2 Eintr\u00e4ge in die Tablist):
|
||||||
// Diese per PlayerListItemRemove entfernen – das funktioniert auch für
|
// Diese per PlayerListItemRemove entfernen – das funktioniert auch f\u00fcr
|
||||||
// UUIDs die der Client noch nicht kennt, ohne Skin-Schaden.
|
// UUIDs die der Client noch nicht kennt, ohne Skin-Schaden.
|
||||||
List<UUID> serverUuids = new ArrayList<>();
|
List<UUID> serverUuids = new ArrayList<>();
|
||||||
for (String srvName : ProxyServer.getInstance().getServers().keySet()) {
|
for (String srvName : ProxyServer.getInstance().getServers().keySet()) {
|
||||||
@@ -543,9 +554,9 @@ public class TablistModule implements Module, Listener {
|
|||||||
pkt.setItems(items);
|
pkt.setItems(items);
|
||||||
sendPacketQueuedMethod.invoke(viewer, pkt);
|
sendPacketQueuedMethod.invoke(viewer, pkt);
|
||||||
|
|
||||||
// Paket 2: Explizit UPDATE_LISTED=true für alle Fake-Slots nochmal senden.
|
// Paket 2: Explizit UPDATE_LISTED=true f\u00fcr alle Fake-Slots nochmal senden.
|
||||||
// BungeeCord sendet nach ServerSwitch ein eigenes Tab-Paket das einige Slots
|
// BungeeCord sendet nach ServerSwitch ein eigenes Tab-Paket das einige Slots
|
||||||
// auf listed=false setzt – dieses zweite Paket überschreibt das wieder.
|
// auf listed=false setzt – dieses zweite Paket \u00fcberschreibt das wieder.
|
||||||
PlayerListItemUpdate listedPkt = new PlayerListItemUpdate();
|
PlayerListItemUpdate listedPkt = new PlayerListItemUpdate();
|
||||||
listedPkt.setActions(EnumSet.of(PlayerListItemUpdate.Action.UPDATE_LISTED));
|
listedPkt.setActions(EnumSet.of(PlayerListItemUpdate.Action.UPDATE_LISTED));
|
||||||
listedPkt.setItems(items); // items haben alle listed=true gesetzt
|
listedPkt.setItems(items); // items haben alle listed=true gesetzt
|
||||||
@@ -601,15 +612,15 @@ public class TablistModule implements Module, Listener {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* ── ULTIMATE: Gibt das konfigurierte Server-Symbol für den Spieler zurück.
|
* ── ULTIMATE: Gibt das konfigurierte Server-Symbol f\u00fcr den Spieler zur\u00fcck.
|
||||||
* Leer wenn kein Symbol für den aktuellen Server definiert ist.
|
* Leer wenn kein Symbol f\u00fcr den aktuellen Server definiert ist.
|
||||||
*/
|
*/
|
||||||
private String getServerSymbol(ProxiedPlayer player) {
|
private String getServerSymbol(ProxiedPlayer player) {
|
||||||
if (serverSymbols.isEmpty() || player.getServer() == null) return "";
|
if (serverSymbols.isEmpty() || player.getServer() == null) return "";
|
||||||
String srvKey = player.getServer().getInfo().getName().toLowerCase();
|
String srvKey = player.getServer().getInfo().getName().toLowerCase();
|
||||||
String raw = serverSymbols.get(srvKey);
|
String raw = serverSymbols.get(srvKey);
|
||||||
if (raw == null || raw.isEmpty()) return "";
|
if (raw == null || raw.isEmpty()) return "";
|
||||||
return c(raw); // Farb-Codes und Hex-Farben auflösen
|
return c(raw); // Farb-Codes und Hex-Farben aufl\u00f6sen
|
||||||
}
|
}
|
||||||
|
|
||||||
private net.md_5.bungee.protocol.data.Property[] fetchSkin(ProxiedPlayer player) {
|
private net.md_5.bungee.protocol.data.Property[] fetchSkin(ProxiedPlayer player) {
|
||||||
@@ -696,7 +707,7 @@ public class TablistModule implements Module, Listener {
|
|||||||
Object cache = usr.getClass().getMethod("getCachedData").invoke(usr);
|
Object cache = usr.getClass().getMethod("getCachedData").invoke(usr);
|
||||||
Object meta = cache.getClass().getMethod("getMetaData", qo).invoke(cache, opts);
|
Object meta = cache.getClass().getMethod("getMetaData", qo).invoke(cache, opts);
|
||||||
Object pfx = meta.getClass().getMethod("getPrefix").invoke(meta);
|
Object pfx = meta.getClass().getMethod("getPrefix").invoke(meta);
|
||||||
// ── HEX-Farben auch im Prefix auflösen ───────────────────────
|
// ── HEX-Farben auch im Prefix aufl\u00f6sen ───────────────────────
|
||||||
if (pfx != null) return c(pfx.toString());
|
if (pfx != null) return c(pfx.toString());
|
||||||
}
|
}
|
||||||
} catch (Exception ignored) {}
|
} catch (Exception ignored) {}
|
||||||
@@ -750,11 +761,11 @@ public class TablistModule implements Module, Listener {
|
|||||||
if (base + row < total) arr[base + row] = text == null ? " " : text; return row + 1;
|
if (base + row < total) arr[base + row] = text == null ? " " : text; return row + 1;
|
||||||
}
|
}
|
||||||
|
|
||||||
// ── Farb-Auflösung ─────────────────────────────────────────────────────────
|
// ── Farb-Aufl\u00f6sung ─────────────────────────────────────────────────────────
|
||||||
|
|
||||||
// ══════════════════════════════════════════════════════════════════════════
|
// ══════════════════════════════════════════════════════════════════════════
|
||||||
// Farb-Parser: Birdflop-kompatibel
|
// Farb-Parser: Birdflop-kompatibel
|
||||||
// Unterstützte Formate (alle gleichzeitig nutzbar):
|
// Unterst\u00fctzte Formate (alle gleichzeitig nutzbar):
|
||||||
//
|
//
|
||||||
// &#RRGGBB → Pro-Zeichen Hex (Birdflop Standard-Output)
|
// &#RRGGBB → Pro-Zeichen Hex (Birdflop Standard-Output)
|
||||||
// {#RRGGBB} → Bracket-Format
|
// {#RRGGBB} → Bracket-Format
|
||||||
@@ -781,7 +792,7 @@ public class TablistModule implements Module, Listener {
|
|||||||
|
|
||||||
private static String parseMiniMessage(String text) {
|
private static String parseMiniMessage(String text) {
|
||||||
if (text == null || !text.contains("<")) return text == null ? "" : text;
|
if (text == null || !text.contains("<")) return text == null ? "" : text;
|
||||||
// gradient-Tags als erstes, weil sie anderen Text enthalten können
|
// gradient-Tags als erstes, weil sie anderen Text enthalten k\u00f6nnen
|
||||||
text = parseGradientTags(text);
|
text = parseGradientTags(text);
|
||||||
// shadow-Tags
|
// shadow-Tags
|
||||||
text = parseShadowTags(text);
|
text = parseShadowTags(text);
|
||||||
@@ -800,7 +811,7 @@ public class TablistModule implements Module, Listener {
|
|||||||
int start = text.indexOf("<gradient:", i);
|
int start = text.indexOf("<gradient:", i);
|
||||||
if (start < 0) { result.append(text, i, text.length()); break; }
|
if (start < 0) { result.append(text, i, text.length()); break; }
|
||||||
result.append(text, i, start);
|
result.append(text, i, start);
|
||||||
// Schließendes > suchen (mit Tiefenzähler für verschachtelte <...>)
|
// Schlie\u00dfendes > suchen (mit Tiefenz\u00e4hler f\u00fcr verschachtelte <...>)
|
||||||
int end = findClosingAngle(text, start + 1);
|
int end = findClosingAngle(text, start + 1);
|
||||||
if (end < 0) { result.append(text, i, text.length()); break; }
|
if (end < 0) { result.append(text, i, text.length()); break; }
|
||||||
String inner = text.substring(start + 1, end); // "gradient:#C1:#C2:TEXT"
|
String inner = text.substring(start + 1, end); // "gradient:#C1:#C2:TEXT"
|
||||||
@@ -811,8 +822,8 @@ public class TablistModule implements Module, Listener {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Parst "gradient:#C1:#C2:#C3:TEXT" → eingefärbten Text.
|
* Parst "gradient:#C1:#C2:#C3:TEXT" → eingef\u00e4rbten Text.
|
||||||
* TEXT darf selbst wieder §-Codes oder &-Codes enthalten (z.B. &l für Bold).
|
* TEXT darf selbst wieder §-Codes oder &-Codes enthalten (z.B. &l f\u00fcr Bold).
|
||||||
*/
|
*/
|
||||||
private static String applyGradientTag(String inner) {
|
private static String applyGradientTag(String inner) {
|
||||||
// inner = "gradient:COLOR:COLOR:...:TEXT"
|
// inner = "gradient:COLOR:COLOR:...:TEXT"
|
||||||
@@ -841,14 +852,14 @@ public class TablistModule implements Module, Listener {
|
|||||||
}
|
}
|
||||||
if (colors.size() < 2) return textSb.toString();
|
if (colors.size() < 2) return textSb.toString();
|
||||||
|
|
||||||
// Shadow-Tags im Text zuerst auflösen (können im Gradient-Text stecken)
|
// Shadow-Tags im Text zuerst aufl\u00f6sen (k\u00f6nnen im Gradient-Text stecken)
|
||||||
String rawText = parseShadowTags(textSb.toString());
|
String rawText = parseShadowTags(textSb.toString());
|
||||||
return applyGradient(rawText, colors);
|
return applyGradient(rawText, colors);
|
||||||
}
|
}
|
||||||
|
|
||||||
private static String applyGradient(String text, java.util.List<String> colorStops) {
|
private static String applyGradient(String text, java.util.List<String> colorStops) {
|
||||||
if (text == null || text.isEmpty()) return text;
|
if (text == null || text.isEmpty()) return text;
|
||||||
// §-Codes und &-Codes aus Text herausfiltern für Längenberechnung
|
// §-Codes und &-Codes aus Text herausfiltern f\u00fcr L\u00e4ngenberechnung
|
||||||
String plain = text
|
String plain = text
|
||||||
.replaceAll("\u00A7[0-9a-fk-orx]", "")
|
.replaceAll("\u00A7[0-9a-fk-orx]", "")
|
||||||
.replaceAll("&[0-9a-fA-Fk-orK-OR]", "")
|
.replaceAll("&[0-9a-fA-Fk-orK-OR]", "")
|
||||||
@@ -866,7 +877,7 @@ public class TablistModule implements Module, Listener {
|
|||||||
while (ci < text.length()) {
|
while (ci < text.length()) {
|
||||||
char ch = text.charAt(ci);
|
char ch = text.charAt(ci);
|
||||||
|
|
||||||
// §x§R§R§G§G§B§B durchreichen (bereits aufgelöste Hex-Farbe z.B. von shadow)
|
// §x§R§R§G§G§B§B durchreichen (bereits aufgel\u00f6ste Hex-Farbe z.B. von shadow)
|
||||||
if (ch == '\u00A7' && ci + 1 < text.length() && text.charAt(ci + 1) == 'x') {
|
if (ch == '\u00A7' && ci + 1 < text.length() && text.charAt(ci + 1) == 'x') {
|
||||||
// Lese die 12 folgenden Zeichen (§x + 6x §digit)
|
// Lese die 12 folgenden Zeichen (§x + 6x §digit)
|
||||||
if (ci + 13 < text.length() + 1) {
|
if (ci + 13 < text.length() + 1) {
|
||||||
@@ -941,7 +952,7 @@ public class TablistModule implements Module, Listener {
|
|||||||
text = text.replace("<st>", "&m").replace("</st>", "&r");
|
text = text.replace("<st>", "&m").replace("</st>", "&r");
|
||||||
text = text.replace("<obf>", "&k").replace("</obf>", "&r");
|
text = text.replace("<obf>", "&k").replace("</obf>", "&r");
|
||||||
text = text.replace("<reset>", "&r").replace("</reset>", "");
|
text = text.replace("<reset>", "&r").replace("</reset>", "");
|
||||||
// Closing-Tags entfernen (werden nach Verarbeitung nicht mehr benötigt)
|
// Closing-Tags entfernen (werden nach Verarbeitung nicht mehr ben\u00f6tigt)
|
||||||
text = text.replaceAll("</gradient>", "");
|
text = text.replaceAll("</gradient>", "");
|
||||||
text = text.replaceAll("</shadow>", "");
|
text = text.replaceAll("</shadow>", "");
|
||||||
text = text.replaceAll("</color>", "");
|
text = text.replaceAll("</color>", "");
|
||||||
@@ -1045,8 +1056,8 @@ public class TablistModule implements Module, Listener {
|
|||||||
private static int clamp(int v) { return Math.max(0, Math.min(255, v)); }
|
private static int clamp(int v) { return Math.max(0, Math.min(255, v)); }
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Findet das schließende '>' für ein Tag das bei fromIndex beginnt.
|
* Findet das schlie\u00dfende '>' f\u00fcr ein Tag das bei fromIndex beginnt.
|
||||||
* Berücksichtigt verschachtelte <...>.
|
* Ber\u00fccksichtigt verschachtelte <...>.
|
||||||
*/
|
*/
|
||||||
private static int findClosingAngle(String text, int fromIndex) {
|
private static int findClosingAngle(String text, int fromIndex) {
|
||||||
int depth = 0;
|
int depth = 0;
|
||||||
@@ -1081,8 +1092,8 @@ public class TablistModule implements Module, Listener {
|
|||||||
"tablist.server_order=\n" +
|
"tablist.server_order=\n" +
|
||||||
"tablist.hidden_servers=\n" +
|
"tablist.hidden_servers=\n" +
|
||||||
"tablist.rank_order=owner,mod,primo,vip,scout,bewohner\n\n" +
|
"tablist.rank_order=owner,mod,primo,vip,scout,bewohner\n\n" +
|
||||||
"# column_header: full = großer Spalten-Header (alte Markierung)\n" +
|
"# column_header: full = gro\u00dfer Spalten-Header (alte Markierung)\n" +
|
||||||
"# none = kein Header, Zeile 0 ist für Spieler frei (MuckiDEE-Wunsch)\n" +
|
"# none = kein Header, Zeile 0 ist f\u00fcr Spieler frei (MuckiDEE-Wunsch)\n" +
|
||||||
"# small = wie none, aber Server-Namen erscheinen im Tab-Footer\n" +
|
"# small = wie none, aber Server-Namen erscheinen im Tab-Footer\n" +
|
||||||
"tablist.column_header=none\n" +
|
"tablist.column_header=none\n" +
|
||||||
"# player_display: server = Server-basiert (default) | custom = alle zusammen nach Rang sortiert\n" +
|
"# player_display: server = Server-basiert (default) | custom = alle zusammen nach Rang sortiert\n" +
|
||||||
@@ -1095,7 +1106,7 @@ public class TablistModule implements Module, Listener {
|
|||||||
"tablist.footer.line3=&8&m" + sep + "\n\n" +
|
"tablist.footer.line3=&8&m" + sep + "\n\n" +
|
||||||
"# ── Compact Layout ──────────────────────────────────────────────────\n" +
|
"# ── Compact Layout ──────────────────────────────────────────────────\n" +
|
||||||
"# Platzhalter: %player% %rank% %server% %world% %time% %balance% %ping% %online%\n" +
|
"# Platzhalter: %player% %rank% %server% %world% %time% %balance% %ping% %online%\n" +
|
||||||
"# spacer=true: leere Zeile = Abstand | spacer=false: leere Zeile = überspringen\n" +
|
"# spacer=true: leere Zeile = Abstand | spacer=false: leere Zeile = \u00fcberspringen\n" +
|
||||||
"tablist.compact.header.line1=&6&lViper Network &8• &2Hallo, &a%player%&7!\n" +
|
"tablist.compact.header.line1=&6&lViper Network &8• &2Hallo, &a%player%&7!\n" +
|
||||||
"tablist.compact.header.line2=&dCitybuild &8• &aSurvival &8• &eMinigames\n" +
|
"tablist.compact.header.line2=&dCitybuild &8• &aSurvival &8• &eMinigames\n" +
|
||||||
"tablist.compact.header.line2.spacer=false\n" +
|
"tablist.compact.header.line2.spacer=false\n" +
|
||||||
@@ -1146,7 +1157,7 @@ public class TablistModule implements Module, Listener {
|
|||||||
"\n# ── Server-Symbole ───────────────────────────────────────────────────\n" +
|
"\n# ── Server-Symbole ───────────────────────────────────────────────────\n" +
|
||||||
"# Format: tablist.symbol.<servername>=&FarbCode Symbol\n" +
|
"# Format: tablist.symbol.<servername>=&FarbCode Symbol\n" +
|
||||||
"# Farben: & + Code (z.B. &6 = Gold) oder &#RRGGBB / {#RRGGBB} / <#RRGGBB>\n" +
|
"# Farben: & + Code (z.B. &6 = Gold) oder &#RRGGBB / {#RRGGBB} / <#RRGGBB>\n" +
|
||||||
"# Emojis und Unicode-Symbole werden unterstützt.\n" +
|
"# Emojis und Unicode-Symbole werden unterst\u00fctzt.\n" +
|
||||||
"# Der Symbol-Text erscheint hinter dem Spielernamen in der Tablist.\n" +
|
"# Der Symbol-Text erscheint hinter dem Spielernamen in der Tablist.\n" +
|
||||||
"tablist.symbol.lobby=&f\uD83C\uDFE0\n" +
|
"tablist.symbol.lobby=&f\uD83C\uDFE0\n" +
|
||||||
"tablist.symbol.sv1=&6\u26CF\uFE0F\n" +
|
"tablist.symbol.sv1=&6\u26CF\uFE0F\n" +
|
||||||
|
|||||||
@@ -22,11 +22,11 @@ import java.util.UUID;
|
|||||||
import java.util.concurrent.ConcurrentHashMap;
|
import java.util.concurrent.ConcurrentHashMap;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* VanishModule für StatusAPI (BungeeCord)
|
* VanishModule f\u00fcr StatusAPI (BungeeCord)
|
||||||
*
|
*
|
||||||
* Features:
|
* Features:
|
||||||
* - /vanish zum Ein-/Ausschalten
|
* - /vanish zum Ein-/Ausschalten
|
||||||
* - /vanish <Spieler> für Admin-Vanish anderer Spieler
|
* - /vanish <Spieler> f\u00fcr Admin-Vanish anderer Spieler
|
||||||
* - /vanishlist – zeigt alle aktuell unsichtbaren Spieler
|
* - /vanishlist – zeigt alle aktuell unsichtbaren Spieler
|
||||||
* - Vanish-Status wird persistent in vanish.dat gespeichert
|
* - Vanish-Status wird persistent in vanish.dat gespeichert
|
||||||
* - Beim Login wird gespeicherter Status wiederhergestellt
|
* - Beim Login wird gespeicherter Status wiederhergestellt
|
||||||
@@ -71,7 +71,7 @@ public class VanishModule implements Module, Listener {
|
|||||||
@Override
|
@Override
|
||||||
public void onDisable(Plugin plugin) {
|
public void onDisable(Plugin plugin) {
|
||||||
save();
|
save();
|
||||||
// Alle als sichtbar markieren beim Shutdown (damit beim nächsten Start
|
// Alle als sichtbar markieren beim Shutdown (damit beim n\u00e4chsten Start
|
||||||
// der VanishProvider sauber ist – load() setzt sie beim Login neu)
|
// der VanishProvider sauber ist – load() setzt sie beim Login neu)
|
||||||
for (UUID uuid : persistentVanished) {
|
for (UUID uuid : persistentVanished) {
|
||||||
VanishProvider.setVanished(uuid, false);
|
VanishProvider.setVanished(uuid, false);
|
||||||
@@ -95,7 +95,7 @@ public class VanishModule implements Module, Listener {
|
|||||||
// den Vanish-Status garantiert vorfindet und keine Join-Nachricht sendet.
|
// den Vanish-Status garantiert vorfindet und keine Join-Nachricht sendet.
|
||||||
VanishProvider.setVanished(player.getUniqueId(), true);
|
VanishProvider.setVanished(player.getUniqueId(), true);
|
||||||
|
|
||||||
// Nur die Bestätigungsnachricht an den Spieler wird verzögert,
|
// Nur die Best\u00e4tigungsnachricht an den Spieler wird verz\u00f6gert,
|
||||||
// damit der Client bereit ist.
|
// damit der Client bereit ist.
|
||||||
plugin.getProxy().getScheduler().schedule(plugin, () -> {
|
plugin.getProxy().getScheduler().schedule(plugin, () -> {
|
||||||
if (player.isConnected()) {
|
if (player.isConnected()) {
|
||||||
@@ -108,7 +108,7 @@ public class VanishModule implements Module, Listener {
|
|||||||
@EventHandler
|
@EventHandler
|
||||||
public void onDisconnect(PlayerDisconnectEvent e) {
|
public void onDisconnect(PlayerDisconnectEvent e) {
|
||||||
// VanishProvider cleanup – der Eintrag in persistentVanished bleibt
|
// VanishProvider cleanup – der Eintrag in persistentVanished bleibt
|
||||||
// erhalten damit der Status beim nächsten Login wiederhergestellt wird
|
// erhalten damit der Status beim n\u00e4chsten Login wiederhergestellt wird
|
||||||
VanishProvider.cleanup(e.getPlayer().getUniqueId());
|
VanishProvider.cleanup(e.getPlayer().getUniqueId());
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -135,7 +135,7 @@ public class VanishModule implements Module, Listener {
|
|||||||
} else {
|
} else {
|
||||||
// Anderen Spieler vanishen
|
// Anderen Spieler vanishen
|
||||||
if (!sender.hasPermission(PERMISSION_OTHER)) {
|
if (!sender.hasPermission(PERMISSION_OTHER)) {
|
||||||
sender.sendMessage(color("&cDu hast keine Berechtigung für /vanish <Spieler>."));
|
sender.sendMessage(color("&cDu hast keine Berechtigung f\u00fcr /vanish <Spieler>."));
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
ProxiedPlayer target = ProxyServer.getInstance().getPlayer(args[0]);
|
ProxiedPlayer target = ProxyServer.getInstance().getPlayer(args[0]);
|
||||||
@@ -176,7 +176,7 @@ public class VanishModule implements Module, Listener {
|
|||||||
/**
|
/**
|
||||||
* Schaltet den Vanish-Status eines Spielers um.
|
* Schaltet den Vanish-Status eines Spielers um.
|
||||||
*
|
*
|
||||||
* @param executor Der Befehlsgeber (für Feedback-Nachrichten)
|
* @param executor Der Befehlsgeber (f\u00fcr Feedback-Nachrichten)
|
||||||
* @param target Der betroffene Spieler
|
* @param target Der betroffene Spieler
|
||||||
*/
|
*/
|
||||||
private void toggleVanish(CommandSender executor, ProxiedPlayer target) {
|
private void toggleVanish(CommandSender executor, ProxiedPlayer target) {
|
||||||
@@ -187,7 +187,7 @@ public class VanishModule implements Module, Listener {
|
|||||||
? "&8[&7Vanish&8] &f" + target.getName() + " &7ist jetzt &cUnsichtbar&7."
|
? "&8[&7Vanish&8] &f" + target.getName() + " &7ist jetzt &cUnsichtbar&7."
|
||||||
: "&8[&7Vanish&8] &f" + target.getName() + " &7ist jetzt &aSichtbar&7.";
|
: "&8[&7Vanish&8] &f" + target.getName() + " &7ist jetzt &aSichtbar&7.";
|
||||||
|
|
||||||
// Feedback an den Ausführenden
|
// Feedback an den Ausf\u00fchrenden
|
||||||
executor.sendMessage(color(statusMsg));
|
executor.sendMessage(color(statusMsg));
|
||||||
|
|
||||||
// Falls jemand anderes gevanisht wurde, auch dem Ziel Bescheid geben
|
// Falls jemand anderes gevanisht wurde, auch dem Ziel Bescheid geben
|
||||||
@@ -198,7 +198,7 @@ public class VanishModule implements Module, Listener {
|
|||||||
target.sendMessage(color(selfMsg));
|
target.sendMessage(color(selfMsg));
|
||||||
}
|
}
|
||||||
|
|
||||||
// Admins mit chat.admin.bypass informieren (außer dem Ausführenden)
|
// Admins mit chat.admin.bypass informieren (au\u00dfer dem Ausf\u00fchrenden)
|
||||||
for (ProxiedPlayer p : ProxyServer.getInstance().getPlayers()) {
|
for (ProxiedPlayer p : ProxyServer.getInstance().getPlayers()) {
|
||||||
if (p.equals(executor) || p.equals(target)) continue;
|
if (p.equals(executor) || p.equals(target)) continue;
|
||||||
if (p.hasPermission("chat.admin.bypass")) {
|
if (p.hasPermission("chat.admin.bypass")) {
|
||||||
@@ -222,7 +222,7 @@ public class VanishModule implements Module, Listener {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Öffentliche API für andere Module.
|
* \u00d6ffentliche API f\u00fcr andere Module.
|
||||||
*/
|
*/
|
||||||
public boolean isVanished(ProxiedPlayer player) {
|
public boolean isVanished(ProxiedPlayer player) {
|
||||||
return VanishProvider.isVanished(player);
|
return VanishProvider.isVanished(player);
|
||||||
|
|||||||
@@ -34,7 +34,7 @@ import java.util.Properties;
|
|||||||
public class VerifyModule implements Module {
|
public class VerifyModule implements Module {
|
||||||
|
|
||||||
private String wpVerifyUrl;
|
private String wpVerifyUrl;
|
||||||
// Keys sind lowercase normalisiert für case-insensitiven Vergleich
|
// Keys sind lowercase normalisiert f\u00fcr case-insensitiven Vergleich
|
||||||
private final Map<String, ServerConfig> serverConfigs = new HashMap<>();
|
private final Map<String, ServerConfig> serverConfigs = new HashMap<>();
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
@@ -84,7 +84,7 @@ public class VerifyModule implements Module {
|
|||||||
ServerConfig config = serverConfigs.computeIfAbsent(serverName, k -> new ServerConfig());
|
ServerConfig config = serverConfigs.computeIfAbsent(serverName, k -> new ServerConfig());
|
||||||
if ("id".equalsIgnoreCase(type)) {
|
if ("id".equalsIgnoreCase(type)) {
|
||||||
try { config.serverId = Integer.parseInt(props.getProperty(key)); }
|
try { config.serverId = Integer.parseInt(props.getProperty(key)); }
|
||||||
catch (NumberFormatException e) { plugin.getLogger().warning("Ungültige Server ID für " + serverName); }
|
catch (NumberFormatException e) { plugin.getLogger().warning("Ung\u00fcltige Server ID f\u00fcr " + serverName); }
|
||||||
} else if ("secret".equalsIgnoreCase(type)) {
|
} else if ("secret".equalsIgnoreCase(type)) {
|
||||||
config.sharedSecret = props.getProperty(key);
|
config.sharedSecret = props.getProperty(key);
|
||||||
}
|
}
|
||||||
@@ -103,11 +103,11 @@ public class VerifyModule implements Module {
|
|||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void execute(CommandSender sender, String[] args) {
|
public void execute(CommandSender sender, String[] args) {
|
||||||
if (!(sender instanceof ProxiedPlayer)) { sender.sendMessage(ChatColor.RED + "Nur Spieler können diesen Befehl benutzen."); return; }
|
if (!(sender instanceof ProxiedPlayer)) { sender.sendMessage(ChatColor.RED + "Nur Spieler k\u00f6nnen diesen Befehl benutzen."); return; }
|
||||||
ProxiedPlayer p = (ProxiedPlayer) sender;
|
ProxiedPlayer p = (ProxiedPlayer) sender;
|
||||||
if (args.length != 1) { p.sendMessage(ChatColor.YELLOW + "Benutzung: /verify <token>"); return; }
|
if (args.length != 1) { p.sendMessage(ChatColor.YELLOW + "Benutzung: /verify <token>"); return; }
|
||||||
|
|
||||||
// FIX #7: Servername lowercase für case-insensitiven Lookup
|
// FIX #7: Servername lowercase f\u00fcr case-insensitiven Lookup
|
||||||
String serverName = p.getServer().getInfo().getName().toLowerCase();
|
String serverName = p.getServer().getInfo().getName().toLowerCase();
|
||||||
ServerConfig config = serverConfigs.get(serverName);
|
ServerConfig config = serverConfigs.get(serverName);
|
||||||
|
|
||||||
|
|||||||
@@ -82,8 +82,8 @@ public class PlayerStats {
|
|||||||
* balance|totalEarned|totalSpent|transactionsCount|
|
* balance|totalEarned|totalSpent|transactionsCount|
|
||||||
* bansCount|mutesCount|warnsCount|lastPunishmentAt|lastPunishmentType|punishmentScore
|
* bansCount|mutesCount|warnsCount|lastPunishmentAt|lastPunishmentType|punishmentScore
|
||||||
*
|
*
|
||||||
* HINWEIS: kills/deaths wurden in Version 1.17.1 als Felder 7 und 8 eingefügt.
|
* HINWEIS: kills/deaths wurden in Version 1.17.1 als Felder 7 und 8 eingef\u00fcgt.
|
||||||
* fromLine() ist rückwärtskompatibel (alte Dateien ohne kills/deaths werden 0 gesetzt).
|
* fromLine() ist r\u00fcckw\u00e4rtskompatibel (alte Dateien ohne kills/deaths werden 0 gesetzt).
|
||||||
*/
|
*/
|
||||||
public synchronized String toLine() {
|
public synchronized String toLine() {
|
||||||
String safeType = (lastPunishmentType == null ? "" : lastPunishmentType).replace("|", "_");
|
String safeType = (lastPunishmentType == null ? "" : lastPunishmentType).replace("|", "_");
|
||||||
@@ -123,7 +123,7 @@ public class PlayerStats {
|
|||||||
// Erkennung ob altes Format (ohne kills/deaths, Economy ab Index 7)
|
// Erkennung ob altes Format (ohne kills/deaths, Economy ab Index 7)
|
||||||
// oder neues Format (kills/deaths ab Index 7, Economy ab Index 9).
|
// oder neues Format (kills/deaths ab Index 7, Economy ab Index 9).
|
||||||
// Altes Format hat 17 Felder (Index 0-16), neues hat 19 (Index 0-18).
|
// Altes Format hat 17 Felder (Index 0-16), neues hat 19 (Index 0-18).
|
||||||
// Heuristik: Wenn parts[7] ein gültiger Integer ist UND parts[9] wie eine
|
// Heuristik: Wenn parts[7] ein g\u00fcltiger Integer ist UND parts[9] wie eine
|
||||||
// Gleitkommazahl aussieht → neues Format. Sonst altes Format.
|
// Gleitkommazahl aussieht → neues Format. Sonst altes Format.
|
||||||
boolean newFormat = false;
|
boolean newFormat = false;
|
||||||
if (parts.length >= 19) {
|
if (parts.length >= 19) {
|
||||||
|
|||||||
@@ -13,14 +13,14 @@ import java.util.concurrent.TimeUnit;
|
|||||||
* StatsModule: Tracking von Spielerdaten (Playtime, Joins, Kills, Deaths).
|
* StatsModule: Tracking von Spielerdaten (Playtime, Joins, Kills, Deaths).
|
||||||
*
|
*
|
||||||
* Fixes:
|
* Fixes:
|
||||||
* - BUG-1: Crash-Recovery für currentSessionStart (verhindert falsche Spielzeit nach Absturz)
|
* - BUG-1: Crash-Recovery f\u00fcr currentSessionStart (verhindert falsche Spielzeit nach Absturz)
|
||||||
* - BUG-2: kills / deaths werden jetzt getrackt und per POST /stats/update aktualisiert
|
* - BUG-2: kills / deaths werden jetzt getrackt und per POST /stats/update aktualisiert
|
||||||
*/
|
*/
|
||||||
public class StatsModule implements Module, Listener {
|
public class StatsModule implements Module, Listener {
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Maximale Sessionlänge nach einem Crash noch gutschreiben (24 Stunden).
|
* Maximale Sessionl\u00e4nge nach einem Crash noch gutschreiben (24 Stunden).
|
||||||
* Längere Differenzen sind unrealistisch → werden ignoriert, currentSessionStart = 0 gesetzt.
|
* L\u00e4ngere Differenzen sind unrealistisch → werden ignoriert, currentSessionStart = 0 gesetzt.
|
||||||
*/
|
*/
|
||||||
private static final long MAX_SESSION_SECONDS = 86_400L;
|
private static final long MAX_SESSION_SECONDS = 86_400L;
|
||||||
|
|
||||||
@@ -47,15 +47,15 @@ public class StatsModule implements Module, Listener {
|
|||||||
// FIX BUG-1: Crash-Recovery – offene Sessions bereinigen.
|
// FIX BUG-1: Crash-Recovery – offene Sessions bereinigen.
|
||||||
//
|
//
|
||||||
// Bei normalem Shutdown setzt onDisable() currentSessionStart = 0 und speichert.
|
// Bei normalem Shutdown setzt onDisable() currentSessionStart = 0 und speichert.
|
||||||
// Bei einem Crash (kill -9, OOM, etc.) passiert das nicht. Beim nächsten Start
|
// Bei einem Crash (kill -9, OOM, etc.) passiert das nicht. Beim n\u00e4chsten Start
|
||||||
// sind alle Spieler offline, aber currentSessionStart enthält noch den alten
|
// sind alle Spieler offline, aber currentSessionStart enth\u00e4lt noch den alten
|
||||||
// Timestamp. getPlaytimeWithCurrentSession() würde dann fälschlicherweise
|
// Timestamp. getPlaytimeWithCurrentSession() w\u00fcrde dann f\u00e4lschlicherweise
|
||||||
// (now - alter_crash_timestamp) zur Spielzeit addieren → massiv falscher Wert.
|
// (now - alter_crash_timestamp) zur Spielzeit addieren → massiv falscher Wert.
|
||||||
//
|
//
|
||||||
// Fix: Nach dem Laden jeden Eintrag prüfen. Falls currentSessionStart > 0:
|
// Fix: Nach dem Laden jeden Eintrag pr\u00fcfen. Falls currentSessionStart > 0:
|
||||||
// - Plausible Differenz (≤ MAX_SESSION_SECONDS) → als echte Zeit gutschreiben
|
// - Plausible Differenz (≤ MAX_SESSION_SECONDS) → als echte Zeit gutschreiben
|
||||||
// - Unplausibel (> MAX_SESSION_SECONDS) → verwerfen, nur zurücksetzen
|
// - Unplausibel (> MAX_SESSION_SECONDS) → verwerfen, nur zur\u00fccksetzen
|
||||||
// - In beiden Fällen: currentSessionStart = 0 setzen
|
// - In beiden F\u00e4llen: currentSessionStart = 0 setzen
|
||||||
// -----------------------------------------------------------------------
|
// -----------------------------------------------------------------------
|
||||||
long now = System.currentTimeMillis() / 1000L;
|
long now = System.currentTimeMillis() / 1000L;
|
||||||
int recovered = 0;
|
int recovered = 0;
|
||||||
@@ -68,9 +68,9 @@ public class StatsModule implements Module, Listener {
|
|||||||
recovered++;
|
recovered++;
|
||||||
} else if (delta > MAX_SESSION_SECONDS) {
|
} else if (delta > MAX_SESSION_SECONDS) {
|
||||||
plugin.getLogger().warning(
|
plugin.getLogger().warning(
|
||||||
"[StatsModule] Unplausibler currentSessionStart für " + ps.name
|
"[StatsModule] Unplausibler currentSessionStart f\u00fcr " + ps.name
|
||||||
+ " (delta=" + delta + "s > " + MAX_SESSION_SECONDS + "s). "
|
+ " (delta=" + delta + "s > " + MAX_SESSION_SECONDS + "s). "
|
||||||
+ "Session wird ohne Gutschrift zurückgesetzt."
|
+ "Session wird ohne Gutschrift zur\u00fcckgesetzt."
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
ps.currentSessionStart = 0;
|
ps.currentSessionStart = 0;
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
name: StatusAPI
|
name: StatusAPI
|
||||||
main: net.viper.status.StatusAPI
|
main: net.viper.status.StatusAPI
|
||||||
version: 4.1.3
|
version: 4.1.4
|
||||||
author: M_Viper
|
author: M_Viper
|
||||||
description: StatusAPI für BungeeCord inkl. Update-Checker, Modul-System und ChatModule
|
description: StatusAPI für BungeeCord inkl. Update-Checker, Modul-System und ChatModule
|
||||||
# Mindestanforderung: Minecraft 1.20 / BungeeCord mit PlayerChatEvent-Unterstützung
|
# Mindestanforderung: Minecraft 1.20 / BungeeCord mit PlayerChatEvent-Unterstützung
|
||||||
@@ -10,6 +10,11 @@ softdepend:
|
|||||||
- Geyser-BungeeCord
|
- Geyser-BungeeCord
|
||||||
|
|
||||||
commands:
|
commands:
|
||||||
|
# ── AfkModule ──────────────────────────────────────────────
|
||||||
|
afk:
|
||||||
|
description: AFK-Modus ein- oder ausschalten
|
||||||
|
usage: /afk
|
||||||
|
|
||||||
# ── HelpModule ────────────────────────────────────────────
|
# ── HelpModule ────────────────────────────────────────────
|
||||||
help:
|
help:
|
||||||
description: Zeigt alle verfügbaren Befehle (Admin-Befehle nur mit Berechtigung)
|
description: Zeigt alle verfügbaren Befehle (Admin-Befehle nur mit Berechtigung)
|
||||||
@@ -205,6 +210,11 @@ commands:
|
|||||||
aliases: [wechsel, switch]
|
aliases: [wechsel, switch]
|
||||||
|
|
||||||
permissions:
|
permissions:
|
||||||
|
# ── AfkModule ──────────────────────────────────────────────
|
||||||
|
statusapi.afk.bypass:
|
||||||
|
description: Automatisches AFK nach Inaktivität umgehen
|
||||||
|
default: op
|
||||||
|
|
||||||
# ── StatusAPI Core ────────────────────────────────────────
|
# ── StatusAPI Core ────────────────────────────────────────
|
||||||
statusapi.admin:
|
statusapi.admin:
|
||||||
description: Zugang zu StatusAPI-Administrationsbefehlen (reload etc.)
|
description: Zugang zu StatusAPI-Administrationsbefehlen (reload etc.)
|
||||||
|
|||||||
Reference in New Issue
Block a user