Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
44a1b6a2b7 | ||
|
|
d9e773b152 | ||
|
|
655b88e5de | ||
|
|
1a53977db0 | ||
|
|
bd0418aa42 | ||
|
|
c37dab5619 | ||
|
|
170efaab6f | ||
|
|
388eb2be66 | ||
|
|
dd183203cd | ||
|
|
41d0d80811 | ||
|
|
45e1e3cbc0 | ||
|
|
8e9d7bec21 | ||
|
|
bb940110bd | ||
|
|
23c2525872 | ||
|
|
c102fb0aa5 | ||
|
|
6333ca22e8 | ||
|
|
c5ad9d6255 | ||
|
|
0fa92d0cbf | ||
|
|
d9ff16fe76 | ||
|
|
096609dba9 | ||
|
|
b502485e51 | ||
|
|
25389f2238 | ||
|
|
8720ba41bb | ||
|
|
31510f7cd2 | ||
|
|
23080355e7 | ||
|
|
f47a5a0729 | ||
|
|
6e49d3b226 | ||
|
|
1b83067c3d | ||
|
|
74faabfdad | ||
|
|
c405376a33 | ||
|
|
254d872139 | ||
|
|
2110c339e7 | ||
|
|
c77411c596 | ||
|
|
f002b2623d | ||
|
|
9460493b27 | ||
|
|
de43b18081 | ||
|
|
3e072d86a6 | ||
|
|
7eb9f45de1 | ||
|
|
38b04ec890 | ||
|
|
f117a949e4 | ||
|
|
5be641bf78 | ||
|
|
7f58d34320 | ||
|
|
f06472edfb | ||
|
|
6dc1809ae5 | ||
|
|
69e896313c | ||
|
|
aeed9bdb0f | ||
|
|
e2bb80ccc7 | ||
|
|
df58149d7e | ||
|
|
e3301e70c2 | ||
|
|
dc30cbd8e1 | ||
|
|
af80800a41 | ||
|
|
1b04892356 | ||
|
|
4cc661ae0e | ||
|
|
2632cff7e2 | ||
|
|
7f134c9a08 | ||
|
|
0049fde0fb | ||
|
|
0c9eadc1ce | ||
|
|
a135efa047 | ||
|
|
8b6d72ddab | ||
|
|
9725a23f7f | ||
|
|
a7b1e211af | ||
|
|
51f10cb00f | ||
|
|
c45d05fb17 | ||
|
|
d04fea7b5c | ||
|
|
cad5564773 | ||
|
|
c33eb9531a | ||
|
|
55fd069246 | ||
|
|
379b54fd9f | ||
|
|
8770d2382d | ||
|
|
304b8c859f | ||
|
|
acd73cb4f9 | ||
|
|
68e81242a1 | ||
|
|
4a522b5a5a | ||
|
|
5ef38326bd | ||
|
|
da8242aba4 | ||
|
|
7c51370293 | ||
|
|
63d65313b2 | ||
|
|
51c4a1cbde | ||
|
|
862d59eca8 | ||
|
|
eb91cc5657 | ||
|
|
9f9f2abc6d | ||
|
|
7ba4cd9161 | ||
|
|
4db99e9dea | ||
|
|
fc2c4bed5f | ||
|
|
4e1f290652 | ||
|
|
80eac74d00 | ||
|
|
43b9701334 | ||
|
|
5fd8578f05 | ||
|
|
673ee29552 | ||
|
|
8b95dd1c04 | ||
|
|
ab07c8e2c3 | ||
|
|
dc5afe9198 | ||
|
|
241aa93747 | ||
|
|
5105998966 | ||
|
|
1663901c2e | ||
|
|
951bacfc2a | ||
|
|
2cabae6cf0 | ||
|
|
0110e27430 | ||
|
|
0c6ae9ba5f | ||
|
|
ef9023adae | ||
|
|
e1cb881d70 | ||
|
|
9e223d6fab | ||
|
|
1876646f12 | ||
|
|
75bd8d6b7d | ||
|
|
6701e9511c | ||
|
|
c99fbb1e98 | ||
|
|
28488306c1 | ||
|
|
7805396501 | ||
|
|
b8ce54967b | ||
|
|
6fd1b7e8c4 | ||
|
|
0d8591213a | ||
|
|
95a6abca4a | ||
|
|
0e450969ec | ||
|
|
981a6e8c18 | ||
|
|
6e23f6c471 | ||
|
|
6805b33c70 | ||
|
|
51b9561b9d | ||
|
|
d1833786c7 | ||
|
|
a40eda3d41 | ||
|
|
a0e1a765fd | ||
|
|
0657a3efce | ||
|
|
8146f8ba45 | ||
|
|
f74452db97 | ||
|
|
f6ec07e779 | ||
|
|
f9ce4ce528 | ||
|
|
267b0e9ad1 | ||
|
|
590747a6fb | ||
|
|
c4a0b62c6c | ||
|
|
40fa5c0e93 | ||
|
|
82185c4376 | ||
|
|
783c15b82f | ||
|
|
f3c103b9e3 | ||
|
|
7876c7e68f | ||
|
|
6a63ed815f | ||
|
|
3769efb283 | ||
|
|
6aa9ff2125 | ||
|
|
4bf580ae2c | ||
|
|
6a925246df | ||
|
|
7fefde51fd | ||
|
|
9b3346c99d | ||
|
|
5e9e05f509 | ||
|
|
c052f01feb | ||
|
|
abcbd0bbae | ||
|
|
627559356b | ||
|
|
5012bcd95b |
Binary file not shown.
+24
-7
@@ -7,15 +7,21 @@
|
||||
|
||||
<groupId>net.viper.bungee</groupId>
|
||||
<artifactId>StatusAPI</artifactId>
|
||||
<version>4.1.0</version>
|
||||
<version>4.1.5</version>
|
||||
<packaging>jar</packaging>
|
||||
|
||||
<name>StatusAPI</name>
|
||||
<description>BungeeCord Status API Plugin</description>
|
||||
|
||||
<properties>
|
||||
<maven.compiler.source>8</maven.compiler.source>
|
||||
<maven.compiler.target>8</maven.compiler.target>
|
||||
<!--
|
||||
Java 21 (LTS) – BungeeCord setzt seit 26.1.x mind. Java 17 voraus,
|
||||
empfohlen ist Java 21 (LTS). Java-21-Bytecode läuft problemlos auf
|
||||
Java 25-Servern (abwärtskompatibel).
|
||||
-->
|
||||
<maven.compiler.source>21</maven.compiler.source>
|
||||
<maven.compiler.target>21</maven.compiler.target>
|
||||
<maven.compiler.release>21</maven.compiler.release>
|
||||
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
|
||||
</properties>
|
||||
|
||||
@@ -42,7 +48,7 @@
|
||||
<dependency>
|
||||
<groupId>com.zaxxer</groupId>
|
||||
<artifactId>HikariCP</artifactId>
|
||||
<version>5.1.0</version>
|
||||
<version>6.2.1</version>
|
||||
<scope>compile</scope>
|
||||
</dependency>
|
||||
|
||||
@@ -50,7 +56,7 @@
|
||||
<dependency>
|
||||
<groupId>com.mysql</groupId>
|
||||
<artifactId>mysql-connector-j</artifactId>
|
||||
<version>9.1.0</version>
|
||||
<version>9.2.0</version>
|
||||
<scope>compile</scope>
|
||||
</dependency>
|
||||
</dependencies>
|
||||
@@ -77,10 +83,21 @@
|
||||
</resources>
|
||||
|
||||
<plugins>
|
||||
<plugin>
|
||||
<groupId>org.apache.maven.plugins</groupId>
|
||||
<artifactId>maven-compiler-plugin</artifactId>
|
||||
<version>3.14.0</version>
|
||||
<configuration>
|
||||
<source>21</source>
|
||||
<target>21</target>
|
||||
<release>21</release>
|
||||
<encoding>UTF-8</encoding>
|
||||
</configuration>
|
||||
</plugin>
|
||||
<plugin>
|
||||
<groupId>org.apache.maven.plugins</groupId>
|
||||
<artifactId>maven-shade-plugin</artifactId>
|
||||
<version>3.5.0</version>
|
||||
<version>3.6.0</version>
|
||||
<executions>
|
||||
<execution>
|
||||
<phase>package</phase>
|
||||
@@ -104,4 +121,4 @@
|
||||
</plugins>
|
||||
</build>
|
||||
|
||||
</project>
|
||||
</project>
|
||||
|
||||
@@ -0,0 +1,72 @@
|
||||
package net.viper.status;
|
||||
|
||||
import net.md_5.bungee.api.connection.ProxiedPlayer;
|
||||
import net.md_5.bungee.api.event.PostLoginEvent;
|
||||
import net.md_5.bungee.api.plugin.Listener;
|
||||
import net.md_5.bungee.api.plugin.Plugin;
|
||||
import net.md_5.bungee.event.EventHandler;
|
||||
|
||||
import java.io.File;
|
||||
import java.io.FileWriter;
|
||||
import java.io.IOException;
|
||||
import java.io.PrintWriter;
|
||||
import java.net.InetSocketAddress;
|
||||
import java.time.ZoneId;
|
||||
import java.time.ZonedDateTime;
|
||||
import java.time.format.DateTimeFormatter;
|
||||
|
||||
/**
|
||||
* PlayerLoginLogger – schreibt bei jedem Join UUID, Name und IP
|
||||
* in die Datei plugins/StatusAPI/player-logins.log
|
||||
*/
|
||||
public class PlayerLoginLogger implements Listener {
|
||||
|
||||
private static final DateTimeFormatter FMT =
|
||||
DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss z");
|
||||
|
||||
private final Plugin plugin;
|
||||
private final File logFile;
|
||||
private final ZoneId zoneId;
|
||||
|
||||
public PlayerLoginLogger(Plugin plugin, ZoneId zoneId) {
|
||||
this.plugin = plugin;
|
||||
this.zoneId = zoneId;
|
||||
this.logFile = new File(plugin.getDataFolder(), "player-logins.log");
|
||||
|
||||
// Sicherstellen, dass das Plugin-Verzeichnis existiert
|
||||
if (!plugin.getDataFolder().exists()) {
|
||||
plugin.getDataFolder().mkdirs();
|
||||
}
|
||||
}
|
||||
|
||||
@EventHandler
|
||||
public void onPostLogin(PostLoginEvent event) {
|
||||
ProxiedPlayer player = event.getPlayer();
|
||||
|
||||
String uuid = player.getUniqueId().toString();
|
||||
String name = player.getName();
|
||||
String ip = "unknown";
|
||||
|
||||
try {
|
||||
InetSocketAddress addr = (InetSocketAddress) player.getSocketAddress();
|
||||
if (addr != null && addr.getAddress() != null) {
|
||||
ip = addr.getAddress().getHostAddress();
|
||||
}
|
||||
} catch (Exception e) {
|
||||
plugin.getLogger().warning("[PlayerLoginLogger] Konnte IP nicht lesen: " + e.getMessage());
|
||||
}
|
||||
|
||||
String timestamp = ZonedDateTime.now(zoneId).format(FMT);
|
||||
String line = String.format("[%s] UUID=%s | Name=%-16s | IP=%s",
|
||||
timestamp, uuid, name, ip);
|
||||
|
||||
plugin.getLogger().info("[LoginLog] " + line);
|
||||
|
||||
// In Datei schreiben (append)
|
||||
try (PrintWriter pw = new PrintWriter(new FileWriter(logFile, true))) {
|
||||
pw.println(line);
|
||||
} catch (IOException e) {
|
||||
plugin.getLogger().warning("[PlayerLoginLogger] Fehler beim Schreiben: " + e.getMessage());
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -3,13 +3,24 @@ package net.viper.status;
|
||||
import net.md_5.bungee.api.ProxyServer;
|
||||
import net.md_5.bungee.api.config.ListenerInfo;
|
||||
import net.md_5.bungee.api.connection.ProxiedPlayer;
|
||||
import net.md_5.bungee.api.chat.ClickEvent;
|
||||
import net.md_5.bungee.api.chat.HoverEvent;
|
||||
import net.md_5.bungee.api.chat.hover.content.Text;
|
||||
import net.md_5.bungee.api.chat.ClickEvent;
|
||||
import net.md_5.bungee.api.chat.HoverEvent;
|
||||
import net.md_5.bungee.api.chat.hover.content.Text;
|
||||
import net.md_5.bungee.api.event.PlayerDisconnectEvent;
|
||||
import net.md_5.bungee.api.event.PostLoginEvent;
|
||||
import net.md_5.bungee.api.plugin.Listener;
|
||||
import net.md_5.bungee.api.plugin.Plugin;
|
||||
import net.md_5.bungee.event.EventHandler;
|
||||
import net.viper.status.module.ModuleManager;
|
||||
import net.viper.status.modules.economy.EconomyModule;
|
||||
import net.viper.status.modules.tablist.TablistModule;
|
||||
import net.viper.status.modules.scoreboard.ScoreboardModule;
|
||||
import net.viper.status.modules.antibot.AntiBotModule;
|
||||
import net.viper.status.modules.network.NetworkInfoModule;
|
||||
import net.viper.status.modules.network.MultiAccountGuard;
|
||||
import net.viper.status.modules.AutoMessage.AutoMessageModule;
|
||||
import net.viper.status.modules.customcommands.CustomCommandModule;
|
||||
import net.viper.status.modules.serverswitcher.ServerSwitcherModule;
|
||||
@@ -20,6 +31,7 @@ import net.viper.status.modules.commandblocker.CommandBlockerModule;
|
||||
import net.viper.status.modules.broadcast.BroadcastModule;
|
||||
import net.viper.status.modules.chat.ChatModule;
|
||||
import net.viper.status.modules.vanish.VanishModule;
|
||||
import net.viper.status.modules.help.HelpModule;
|
||||
|
||||
import java.io.BufferedReader;
|
||||
import java.io.File;
|
||||
@@ -46,7 +58,7 @@ import net.md_5.bungee.api.scheduler.ScheduledTask;
|
||||
/**
|
||||
* StatusAPI - zentraler BungeeCord HTTP-Status- und Broadcast-Endpunkt
|
||||
*/
|
||||
public class StatusAPI extends Plugin implements Runnable {
|
||||
public class StatusAPI extends Plugin implements Runnable, Listener {
|
||||
|
||||
// Welt pro Spieler (UUID -> Weltname), wird von StatusAPIBridge gepusht
|
||||
public static final ConcurrentHashMap<UUID, String> playerWorlds = new ConcurrentHashMap<>();
|
||||
@@ -54,9 +66,24 @@ public class StatusAPI extends Plugin implements Runnable {
|
||||
// Kontostand pro Spieler (UUID -> Balance), wird von StatusAPIBridge gepusht
|
||||
public static final ConcurrentHashMap<UUID, Double> playerBalances = new ConcurrentHashMap<>();
|
||||
|
||||
// PlaceholderAPI-Werte pro Spieler (UUID -> (placeholder -> aufgel\u00f6ster Wert))
|
||||
public static final ConcurrentHashMap<UUID, Map<String, String>> playerPapi = new ConcurrentHashMap<>();
|
||||
|
||||
// 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 = "[]";
|
||||
|
||||
// Debug-Modus (aus verify.properties)
|
||||
public static boolean DEBUG = false;
|
||||
|
||||
/**
|
||||
* Login-Logger: UUID, Name und IP bei jedem Join in player-logins.log schreiben.
|
||||
* Vor dem Kompilieren auf true setzen um den Logger zu aktivieren.
|
||||
*/
|
||||
private static final boolean LOGIN_LOGGER_ENABLED = false;
|
||||
|
||||
/** Gibt eine Info-Meldung nur im Debug-Modus aus */
|
||||
public static void debugLog(Plugin plugin, String message) {
|
||||
if (DEBUG) plugin.getLogger().info(message);
|
||||
@@ -88,7 +115,7 @@ public class StatusAPI extends Plugin implements Runnable {
|
||||
try {
|
||||
port = Integer.parseInt(portStr);
|
||||
} 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;
|
||||
}
|
||||
|
||||
@@ -98,8 +125,10 @@ public class StatusAPI extends Plugin implements Runnable {
|
||||
moduleManager = new ModuleManager();
|
||||
|
||||
// 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 HelpModule());
|
||||
moduleManager.registerModule(new VerifyModule());
|
||||
moduleManager.registerModule(new BroadcastModule());
|
||||
moduleManager.registerModule(new CommandBlockerModule());
|
||||
@@ -107,6 +136,7 @@ public class StatusAPI extends Plugin implements Runnable {
|
||||
moduleManager.registerModule(new ChatModule());
|
||||
moduleManager.registerModule(new AntiBotModule());
|
||||
moduleManager.registerModule(new NetworkInfoModule());
|
||||
moduleManager.registerModule(new MultiAccountGuard());
|
||||
moduleManager.registerModule(new AutoMessageModule());
|
||||
moduleManager.registerModule(new CustomCommandModule());
|
||||
moduleManager.registerModule(new ServerSwitcherModule());
|
||||
@@ -124,6 +154,33 @@ public class StatusAPI extends Plugin implements Runnable {
|
||||
|
||||
moduleManager.enableAll(this);
|
||||
|
||||
// PAPI-Tokens sofort scannen + nochmal nach 5s als Fallback (falls Configs erst beim Enable erstellt)
|
||||
scanAndPublishPapiTokens();
|
||||
ProxyServer.getInstance().getScheduler().schedule(this, this::scanAndPublishPapiTokens, 5, TimeUnit.SECONDS);
|
||||
|
||||
// /statusapi reload Befehl registrieren
|
||||
ProxyServer.getInstance().getPluginManager().registerCommand(this, new StatusAPICommand(this));
|
||||
|
||||
// PlayerLoginLogger: schreibt UUID, Name und IP bei jedem Join in player-logins.log
|
||||
// Aktivieren: LOGIN_LOGGER_ENABLED = true setzen und neu kompilieren
|
||||
if (LOGIN_LOGGER_ENABLED) {
|
||||
Properties loginProps = loadNetworkGuardProperties();
|
||||
String tzRaw = loginProps.getProperty("login.log.timezone", "UTC").trim();
|
||||
java.time.ZoneId loginZone;
|
||||
try {
|
||||
loginZone = java.time.ZoneId.of(tzRaw);
|
||||
} catch (java.time.zone.ZoneRulesException e) {
|
||||
getLogger().warning("[PlayerLoginLogger] Unbekannte Zeitzone '" + tzRaw + "' – fallback auf UTC.");
|
||||
loginZone = java.time.ZoneId.of("UTC");
|
||||
}
|
||||
PlayerLoginLogger loginLogger = new PlayerLoginLogger(this, loginZone);
|
||||
ProxyServer.getInstance().getPluginManager().registerListener(this, loginLogger);
|
||||
getLogger().info("[PlayerLoginLogger] Login-Logging aktiv -> " + getDataFolder() + "/player-logins.log (Zeitzone: " + loginZone + ")");
|
||||
}
|
||||
|
||||
// Memory-Leak-Fix: playerWorlds/playerPapi beim Disconnect bereinigen
|
||||
ProxyServer.getInstance().getPluginManager().registerListener(this, this);
|
||||
|
||||
// FIX: ScoreboardModule mit NetworkInfoModule verbinden (TPS-Fallback)
|
||||
try {
|
||||
net.viper.status.modules.scoreboard.ScoreboardModule sbMod =
|
||||
@@ -247,17 +304,107 @@ public class StatusAPI extends Plugin implements Runnable {
|
||||
updateChecker.checkNow();
|
||||
String currentVersion = getDescription() != null ? getDescription().getVersion() : "0.0.0";
|
||||
if (updateChecker.isUpdateAvailable(currentVersion)) {
|
||||
String newVersion = updateChecker.getLatestVersion();
|
||||
getLogger().warning("----------------------------------------");
|
||||
getLogger().warning("Neue Version verfügbar: " + newVersion);
|
||||
getLogger().warning("Download: " + updateChecker.getLatestUrl());
|
||||
getLogger().warning("----------------------------------------");
|
||||
String newVersion = updateChecker.getLatestVersion();
|
||||
String releaseUrl = updateChecker.getLatestUrl();
|
||||
boolean preRelease = updateChecker.isLatestPreRelease();
|
||||
// Konsolen-Log
|
||||
getLogger().warning((preRelease ? "[Pre-Release] " : "") + "Update verfuegbar: " + newVersion + " -> " + releaseUrl);
|
||||
// Ingame-Meldung an alle Spieler mit statusapi.update.notify
|
||||
broadcastUpdateNotice(newVersion, releaseUrl, preRelease);
|
||||
}
|
||||
} catch (Exception e) {
|
||||
getLogger().severe("Fehler beim Update-Check: " + e.getMessage());
|
||||
}
|
||||
}
|
||||
|
||||
/** Sendet die Update-Meldung an alle online Spieler mit statusapi.update.notify */
|
||||
private void broadcastUpdateNotice(String newVersion, String releaseUrl, boolean preRelease) {
|
||||
String current = getDescription() != null ? getDescription().getVersion() : "?";
|
||||
sendUpdateMessage(ProxyServer.getInstance().getConsole(), newVersion, current, releaseUrl, preRelease);
|
||||
for (net.md_5.bungee.api.connection.ProxiedPlayer p : ProxyServer.getInstance().getPlayers()) {
|
||||
if (p.hasPermission("statusapi.update.notify")) {
|
||||
sendUpdateMessage(p, newVersion, current, releaseUrl, preRelease);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private static final String RELEASES_URL = "https://git.viper.ipv64.net/M_Viper/StatusAPI/releases";
|
||||
|
||||
/** Formatiert und schickt die Update-Meldung an einen einzelnen Empfaenger */
|
||||
private static void sendUpdateMessage(net.md_5.bungee.api.CommandSender target,
|
||||
String newVersion, String currentVersion, String releaseUrl,
|
||||
boolean preRelease) {
|
||||
// Einfache Textzeilen
|
||||
String typeLabel = preRelease ? "&e&lPre-Release" : "&a&lRelease";
|
||||
String typeNotice = preRelease ? "&eVorsicht: Dies ist ein Pre-Release und kann instabil sein!" : "";
|
||||
String[] plainLines = preRelease ? new String[]{
|
||||
"&8&m" + repeat("-", 44),
|
||||
"&6&l StatusAPI &7\u2013 &e&lPre-Release verf\u00fcgbar!",
|
||||
"&7Aktuelle Version: &c" + currentVersion,
|
||||
"&7Neue Version: &e" + newVersion + " &7[Pre-Release]",
|
||||
"&eVorsicht: &7Kann instabil sein, bitte testen!",
|
||||
} : new String[]{
|
||||
"&8&m" + repeat("-", 44),
|
||||
"&6&l StatusAPI &7\u2013 Update verf\u00fcgbar!",
|
||||
"&7Aktuelle Version: &c" + currentVersion,
|
||||
"&7Neue Version: &a" + newVersion,
|
||||
};
|
||||
for (String line : plainLines) {
|
||||
target.sendMessage(new net.md_5.bungee.api.chat.TextComponent(
|
||||
net.md_5.bungee.api.ChatColor.translateAlternateColorCodes('&', line)));
|
||||
}
|
||||
|
||||
// Klickbare Link-Zeile (nur fuer ProxiedPlayer, Konsole bekommt Plaintext)
|
||||
if (target instanceof net.md_5.bungee.api.connection.ProxiedPlayer) {
|
||||
net.md_5.bungee.api.chat.TextComponent prefix = new net.md_5.bungee.api.chat.TextComponent(
|
||||
net.md_5.bungee.api.ChatColor.translateAlternateColorCodes('&', "&7Releases: "));
|
||||
net.md_5.bungee.api.chat.TextComponent link = new net.md_5.bungee.api.chat.TextComponent(
|
||||
net.md_5.bungee.api.ChatColor.translateAlternateColorCodes('&', "&b&n" + RELEASES_URL));
|
||||
link.setClickEvent(new ClickEvent(ClickEvent.Action.OPEN_URL, RELEASES_URL));
|
||||
link.setHoverEvent(new HoverEvent(HoverEvent.Action.SHOW_TEXT,
|
||||
new Text(net.md_5.bungee.api.ChatColor.translateAlternateColorCodes('&',
|
||||
"&7Klicke um die Release-Seite zu \u00f6ffnen"))));
|
||||
prefix.addExtra(link);
|
||||
target.sendMessage(prefix);
|
||||
} else {
|
||||
target.sendMessage(new net.md_5.bungee.api.chat.TextComponent(
|
||||
net.md_5.bungee.api.ChatColor.translateAlternateColorCodes('&', "&7Releases: &b" + RELEASES_URL)));
|
||||
}
|
||||
|
||||
target.sendMessage(new net.md_5.bungee.api.chat.TextComponent(
|
||||
net.md_5.bungee.api.ChatColor.translateAlternateColorCodes('&', "&8&m" + repeat("-", 44))));
|
||||
}
|
||||
|
||||
private static String repeat(String s, int n) {
|
||||
StringBuilder sb = new StringBuilder();
|
||||
for (int i = 0; i < n; i++) sb.append(s);
|
||||
return sb.toString();
|
||||
}
|
||||
|
||||
// Memory-Leak-Fix: playerWorlds und playerPapi beim Spieler-Logout bereinigen.
|
||||
// Diese Maps werden über HTTP von der Bridge befüllt und haben sonst keinen
|
||||
// Disconnect-Handler, was zu unbegrenztem Wachstum führt.
|
||||
@EventHandler
|
||||
public void onPlayerDisconnect(PlayerDisconnectEvent event) {
|
||||
UUID id = event.getPlayer().getUniqueId();
|
||||
playerWorlds.remove(id);
|
||||
playerPapi.remove(id);
|
||||
}
|
||||
|
||||
/** Update-Hinweis beim Login für Spieler mit statusapi.update.notify */
|
||||
@EventHandler
|
||||
public void onPostLogin(PostLoginEvent event) {
|
||||
net.md_5.bungee.api.connection.ProxiedPlayer p = event.getPlayer();
|
||||
if (!p.hasPermission("statusapi.update.notify")) return;
|
||||
String current = getDescription() != null ? getDescription().getVersion() : "0.0.0";
|
||||
if (updateChecker != null && updateChecker.isUpdateAvailable(current)) {
|
||||
// Kurze Verzögerung damit der Spieler erst spawnt
|
||||
ProxyServer.getInstance().getScheduler().schedule(this, () ->
|
||||
sendUpdateMessage(p, updateChecker.getLatestVersion(), current, updateChecker.getLatestUrl(), updateChecker.isLatestPreRelease()),
|
||||
3, TimeUnit.SECONDS);
|
||||
}
|
||||
}
|
||||
|
||||
// --- WebServer ---
|
||||
@Override
|
||||
public void run() {
|
||||
@@ -277,7 +424,7 @@ public class StatusAPI extends Plugin implements Runnable {
|
||||
Socket clientSocket = localServerSocket.accept();
|
||||
submitConnection(clientSocket);
|
||||
} catch (SocketTimeoutException ignored) {
|
||||
// Poll-Schleife für Interrupt/Shutdown
|
||||
// Poll-Schleife f\u00fcr Interrupt/Shutdown
|
||||
} catch (IOException e) {
|
||||
if (!shuttingDown) {
|
||||
getLogger().warning("Fehler beim Akzeptieren einer Verbindung: " + e.getMessage());
|
||||
@@ -547,7 +694,7 @@ public class StatusAPI extends Plugin implements Runnable {
|
||||
playerMap.put("kills", ps.kills);
|
||||
playerMap.put("deaths", ps.deaths);
|
||||
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);
|
||||
Map<String, Object> economy = new LinkedHashMap<>();
|
||||
economy.put("balance", playerBalance);
|
||||
@@ -572,7 +719,7 @@ public class StatusAPI extends Plugin implements Runnable {
|
||||
// Kein Cache – UUID und Balance kommen direkt aus der DB
|
||||
if ("GET".equalsIgnoreCase(method) && "/economy/player".equalsIgnoreCase(pathOnly)) {
|
||||
Map<String, String> qp = parseQueryParams(path);
|
||||
// UUID auflösen aus Query-Param
|
||||
// UUID aufl\u00f6sen aus Query-Param
|
||||
UUID ecoUuid = null;
|
||||
String ecoName = null;
|
||||
String uuidParam = qp.get("uuid");
|
||||
@@ -585,7 +732,7 @@ public class StatusAPI extends Plugin implements Runnable {
|
||||
return;
|
||||
}
|
||||
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);
|
||||
Map<String, Object> payload = new LinkedHashMap<>();
|
||||
payload.put("success", true);
|
||||
@@ -599,11 +746,11 @@ public class StatusAPI extends Plugin implements Runnable {
|
||||
}
|
||||
|
||||
// POST /economy/update
|
||||
// Empfängt Balance-Updates von StatusAPIBridge (Vault/NexEco → HTTP)
|
||||
// Schreibt NUR in playerBalances für Tablist/Scoreboard – KEINE DB-Schreiboperationen
|
||||
// Empf\u00e4ngt Balance-Updates von StatusAPIBridge (Vault/NexEco → HTTP)
|
||||
// Schreibt NUR in playerBalances f\u00fcr Tablist/Scoreboard – KEINE DB-Schreiboperationen
|
||||
if ("POST".equalsIgnoreCase(method) && "/economy/update".equalsIgnoreCase(pathOnly)) {
|
||||
String body = readBody(in, headers);
|
||||
// UUID auflösen
|
||||
// UUID aufl\u00f6sen
|
||||
UUID ecoUpdUuid = null;
|
||||
String uuidBody = extractJsonString(body, "uuid");
|
||||
if (uuidBody != null && !uuidBody.isEmpty()) {
|
||||
@@ -613,8 +760,8 @@ public class StatusAPI extends Plugin implements Runnable {
|
||||
sendHttpResponse(out, "{\"success\":false,\"error\":\"player_not_found\"}", 404);
|
||||
return;
|
||||
}
|
||||
// Balance NUR in playerBalances Map speichern (für Tablist/Scoreboard)
|
||||
// Die echte DB-Verwaltung macht ausschließlich NexEco
|
||||
// Balance NUR in playerBalances Map speichern (f\u00fcr Tablist/Scoreboard)
|
||||
// Die echte DB-Verwaltung macht ausschlie\u00dflich NexEco
|
||||
String balStr = extractJsonString(body, "balance");
|
||||
if (balStr != null && !balStr.isEmpty()) {
|
||||
try {
|
||||
@@ -702,7 +849,7 @@ public class StatusAPI extends Plugin implements Runnable {
|
||||
// Playtime auch updaten
|
||||
String playtimeStr = extractJsonString(body, "playtime");
|
||||
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()) {
|
||||
int v = Integer.parseInt(killsStr.trim());
|
||||
if (v > psUpd.kills) psUpd.kills = v;
|
||||
@@ -744,6 +891,13 @@ public class StatusAPI extends Plugin implements Runnable {
|
||||
if (uuidStr != null && !uuidStr.isEmpty() && compassStr != null && !compassStr.isEmpty()) {
|
||||
try {
|
||||
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());
|
||||
} catch (Exception ignored) {}
|
||||
}
|
||||
@@ -781,6 +935,22 @@ public class StatusAPI extends Plugin implements Runnable {
|
||||
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)
|
||||
if ("POST".equalsIgnoreCase(method) && "/ticket/update".equalsIgnoreCase(pathOnly)) {
|
||||
String body = readBody(in, headers);
|
||||
@@ -822,6 +992,23 @@ public class StatusAPI extends Plugin implements Runnable {
|
||||
String fdS = extractJsonString(body, "food");
|
||||
String spS = extractJsonString(body, "speed");
|
||||
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 (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));
|
||||
@@ -836,6 +1023,61 @@ public class StatusAPI extends Plugin implements Runnable {
|
||||
return;
|
||||
}
|
||||
|
||||
// GET /papi/tokens – liefert alle erkannten %token%-Placeholder als JSON-Array
|
||||
if ("GET".equalsIgnoreCase(method) && "/papi/tokens".equalsIgnoreCase(pathOnly)) {
|
||||
sendHttpResponse(out, papiTokensJson, 200);
|
||||
return;
|
||||
}
|
||||
|
||||
// POST /player/papi – empf\u00e4ngt von StatusAPIBridge aufgel\u00f6ste PAPI-Werte
|
||||
if ("POST".equalsIgnoreCase(method) && "/player/papi".equalsIgnoreCase(pathOnly)) {
|
||||
String body = readBody(in, headers);
|
||||
String uuidStr = extractJsonString(body, "uuid");
|
||||
if (uuidStr != null && !uuidStr.isEmpty()) {
|
||||
try {
|
||||
UUID papiUuid = UUID.fromString(uuidStr.trim());
|
||||
Map<String, String> map = playerPapi.computeIfAbsent(papiUuid, k -> new ConcurrentHashMap<>());
|
||||
// "placeholders"-Objekt manuell parsen
|
||||
int start = body.indexOf("\"placeholders\"");
|
||||
if (start >= 0) {
|
||||
int brace = body.indexOf('{', start + 14);
|
||||
if (brace >= 0) {
|
||||
int i = brace + 1;
|
||||
while (i < body.length()) {
|
||||
while (i < body.length() && Character.isWhitespace(body.charAt(i))) i++;
|
||||
if (i >= body.length() || body.charAt(i) == '}') break;
|
||||
if (body.charAt(i) != '"') { i++; continue; }
|
||||
i++;
|
||||
StringBuilder key = new StringBuilder();
|
||||
while (i < body.length() && body.charAt(i) != '"') {
|
||||
char ch = body.charAt(i++);
|
||||
if (ch == '\\' && i < body.length()) i++; else key.append(ch);
|
||||
}
|
||||
i++;
|
||||
while (i < body.length() && (body.charAt(i) == ':' || Character.isWhitespace(body.charAt(i)))) i++;
|
||||
if (i < body.length() && body.charAt(i) == '"') {
|
||||
i++;
|
||||
StringBuilder val = new StringBuilder();
|
||||
boolean esc = false;
|
||||
while (i < body.length()) {
|
||||
char ch = body.charAt(i++);
|
||||
if (esc) { val.append(ch == 'n' ? '\n' : ch == 't' ? '\t' : ch); esc = false; }
|
||||
else if (ch == '\\') esc = true;
|
||||
else if (ch == '"') break;
|
||||
else val.append(ch);
|
||||
}
|
||||
if (key.length() > 0) map.put(key.toString(), val.toString());
|
||||
}
|
||||
while (i < body.length() && (body.charAt(i) == ',' || Character.isWhitespace(body.charAt(i)))) i++;
|
||||
}
|
||||
}
|
||||
}
|
||||
} catch (Exception ignored) {}
|
||||
}
|
||||
sendHttpResponse(out, "{\"success\":true}", 200);
|
||||
return;
|
||||
}
|
||||
|
||||
// GET – Status-Endpunkt
|
||||
if (inputLine.startsWith("GET")) {
|
||||
Map<String, Object> data = new LinkedHashMap<>();
|
||||
@@ -909,6 +1151,13 @@ public class StatusAPI extends Plugin implements Runnable {
|
||||
}
|
||||
playerInfo.put("prefix", prefix);
|
||||
|
||||
// Aktueller Sub-Server des Spielers (z.B. "Lobby", "Survival")
|
||||
try {
|
||||
if (p.getServer() != null && p.getServer().getInfo() != null) {
|
||||
playerInfo.put("server", p.getServer().getInfo().getName());
|
||||
}
|
||||
} catch (Exception ignored) {}
|
||||
|
||||
if (statsModule != null) {
|
||||
PlayerStats ps = statsModule.getManager().getIfPresent(p.getUniqueId());
|
||||
if (ps != null) {
|
||||
@@ -918,7 +1167,7 @@ public class StatusAPI extends Plugin implements Runnable {
|
||||
playerInfo.put("deaths", ps.deaths);
|
||||
playerInfo.put("first_seen", ps.firstSeen);
|
||||
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);
|
||||
Map<String, Object> eco = new LinkedHashMap<>();
|
||||
eco.put("balance", statusBalance);
|
||||
@@ -972,13 +1221,19 @@ public class StatusAPI extends Plugin implements Runnable {
|
||||
|
||||
/**
|
||||
* Liest den HTTP-Body basierend auf Content-Length.
|
||||
* Max. 1 MB – schützt vor überdimensionierten Requests (DoS via Content-Length).
|
||||
*/
|
||||
private static final int MAX_BODY_SIZE = 1024 * 1024; // 1 MB
|
||||
private String readBody(BufferedReader in, Map<String, String> headers) throws IOException {
|
||||
int contentLength = 0;
|
||||
if (headers.containsKey("content-length")) {
|
||||
try { contentLength = Integer.parseInt(headers.get("content-length")); } catch (NumberFormatException ignored) {}
|
||||
}
|
||||
if (contentLength <= 0) return "";
|
||||
if (contentLength > MAX_BODY_SIZE) {
|
||||
getLogger().warning("[StatusAPI] Request abgelehnt: Content-Length " + contentLength + " > " + MAX_BODY_SIZE + " Bytes.");
|
||||
return "";
|
||||
}
|
||||
char[] bodyChars = new char[contentLength];
|
||||
int read = 0;
|
||||
while (read < contentLength) {
|
||||
@@ -1202,4 +1457,178 @@ public class StatusAPI extends Plugin implements Runnable {
|
||||
String e = event.trim().toLowerCase(Locale.ROOT);
|
||||
return e.contains("ip_rate") || e.contains("vpn") || e.contains("learning_threshold_block") || e.contains("block");
|
||||
}
|
||||
|
||||
// ── PAPI-Token-Erkennung ──────────────────────────────────────────────────
|
||||
|
||||
/** Alle Tokens die StatusAPI selbst aufl\u00f6st – werden nicht an PAPI weitergegeben */
|
||||
private static final Set<String> NATIVE_TOKENS = new HashSet<>(Arrays.asList(
|
||||
"player", "rank", "money", "server", "compass", "health", "hearts", "ping",
|
||||
"online", "maxplayers", "tps", "ram", "time", "playtime", "x", "y", "z",
|
||||
"world", "gamemode", "exp", "food", "foodsym", "speed", "uptime", "servers",
|
||||
"proxymem", "date", "news", "line", "balance",
|
||||
"ticket_my_open", "ticket_open", "ticket_claimed",
|
||||
"ticket_rating_good", "ticket_rating_bad", "ticket_rating_pct"
|
||||
));
|
||||
|
||||
/**
|
||||
* Scannt alle .properties-Dateien im Plugin-Ordner nach %token%-Mustern,
|
||||
* filtert nativ unterst\u00fctzte Tokens heraus und ver\u00f6ffentlicht den Rest
|
||||
* als JSON-Array unter GET /papi/tokens f\u00fcr StatusAPIBridge.
|
||||
*/
|
||||
public void scanAndPublishPapiTokens() {
|
||||
Set<String> tokens = new LinkedHashSet<>();
|
||||
File folder = getDataFolder();
|
||||
if (folder.exists()) {
|
||||
File[] files = folder.listFiles((dir, name) -> name.endsWith(".properties"));
|
||||
if (files != null) {
|
||||
for (File f : files) {
|
||||
try (BufferedReader br = new BufferedReader(
|
||||
new InputStreamReader(new FileInputStream(f), StandardCharsets.UTF_8))) {
|
||||
String line;
|
||||
while ((line = br.readLine()) != null) {
|
||||
extractAllTokensFromText(line, tokens);
|
||||
}
|
||||
} catch (IOException ignored) {}
|
||||
}
|
||||
}
|
||||
}
|
||||
// Ressourcen-Defaults scannen (falls noch keine Dateien im Ordner)
|
||||
for (String resource : new String[]{"scoreboard.properties"}) {
|
||||
try (java.io.InputStream is = getResourceAsStream(resource)) {
|
||||
if (is == null) continue;
|
||||
BufferedReader br = new BufferedReader(new InputStreamReader(is, StandardCharsets.UTF_8));
|
||||
String line;
|
||||
while ((line = br.readLine()) != null) {
|
||||
extractAllTokensFromText(line, tokens);
|
||||
}
|
||||
} catch (IOException ignored) {}
|
||||
}
|
||||
tokens.removeAll(NATIVE_TOKENS);
|
||||
// JSON-Array bauen
|
||||
StringBuilder json = new StringBuilder("[");
|
||||
boolean first = true;
|
||||
for (String token : tokens) {
|
||||
if (!first) json.append(",");
|
||||
json.append("\"").append(token.replace("\\", "\\\\").replace("\"", "\\\"")).append("\"");
|
||||
first = false;
|
||||
}
|
||||
json.append("]");
|
||||
papiTokensJson = json.toString();
|
||||
if (!tokens.isEmpty()) {
|
||||
getLogger().info("[StatusAPI] " + tokens.size() + " PAPI-Token(s) erkannt: " + tokens);
|
||||
}
|
||||
}
|
||||
|
||||
private static void extractAllTokensFromText(String text, Set<String> result) {
|
||||
if (text == null || text.startsWith("#") || !text.contains("%")) return;
|
||||
int eq = text.indexOf('=');
|
||||
String value = eq >= 0 ? text.substring(eq + 1) : text;
|
||||
int i = 0;
|
||||
while (i < value.length()) {
|
||||
int start = value.indexOf('%', i);
|
||||
if (start < 0) break;
|
||||
int end = value.indexOf('%', start + 1);
|
||||
if (end < 0) break;
|
||||
String token = value.substring(start + 1, end);
|
||||
if (!token.isEmpty() && !token.contains(" ") && token.matches("[a-zA-Z0-9_:]+")) {
|
||||
result.add(token);
|
||||
}
|
||||
i = end + 1;
|
||||
}
|
||||
}
|
||||
|
||||
// ── Reload ────────────────────────────────────────────────────────────────
|
||||
|
||||
/**
|
||||
* L\u00e4dt Scoreboard und Tablist neu (Config + Tasks), ohne den HTTP-Server zu ber\u00fchren.
|
||||
* Alle anderen Module (Chat, AntiBot, etc.) bleiben unber\u00fchrt.
|
||||
*/
|
||||
public void reloadModules() {
|
||||
getLogger().info("[StatusAPI] Reload von Scoreboard, Tablist und AFK...");
|
||||
|
||||
net.viper.status.module.Module sbMod = moduleManager.getModule("ScoreboardModule");
|
||||
net.viper.status.module.Module tabMod = moduleManager.getModule("TablistModule");
|
||||
net.viper.status.module.Module afkMod = moduleManager.getModule("AfkModule");
|
||||
|
||||
if (sbMod != null) sbMod.onDisable(this);
|
||||
if (tabMod != null) tabMod.onDisable(this);
|
||||
// AfkModule meldet Listener, Command und Plugin-Channel in onDisable sauber ab
|
||||
// und setzt laufende AFK-Zustaende auf den Spigot-Servern zurueck.
|
||||
if (afkMod != null) afkMod.onDisable(this);
|
||||
|
||||
// Neue Instanzen erstellen und registrieren
|
||||
net.viper.status.modules.scoreboard.ScoreboardModule newSb = new net.viper.status.modules.scoreboard.ScoreboardModule();
|
||||
net.viper.status.modules.tablist.TablistModule newTab = new net.viper.status.modules.tablist.TablistModule();
|
||||
net.viper.status.modules.afk.AfkModule newAfk = new net.viper.status.modules.afk.AfkModule();
|
||||
|
||||
moduleManager.replaceModule("ScoreboardModule", newSb);
|
||||
moduleManager.replaceModule("TablistModule", newTab);
|
||||
moduleManager.replaceModule("AfkModule", newAfk);
|
||||
|
||||
newAfk.onEnable(this);
|
||||
newSb.onEnable(this);
|
||||
newTab.onEnable(this);
|
||||
|
||||
// TPS-Fallback neu verbinden
|
||||
try {
|
||||
net.viper.status.modules.network.NetworkInfoModule nim =
|
||||
(net.viper.status.modules.network.NetworkInfoModule) moduleManager.getModule("NetworkInfoModule");
|
||||
if (nim != null) newSb.setNetworkInfoModule(nim);
|
||||
} catch (Exception ignored) {}
|
||||
|
||||
scanAndPublishPapiTokens();
|
||||
getLogger().info("[StatusAPI] Reload abgeschlossen.");
|
||||
}
|
||||
|
||||
// ── /statusapi Befehl ─────────────────────────────────────────────────────
|
||||
|
||||
private static class StatusAPICommand extends net.md_5.bungee.api.plugin.Command {
|
||||
|
||||
private final StatusAPI plugin;
|
||||
|
||||
StatusAPICommand(StatusAPI plugin) {
|
||||
super("statusapi", "statusapi.admin", "sapi");
|
||||
this.plugin = plugin;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void execute(net.md_5.bungee.api.CommandSender sender, String[] args) {
|
||||
if (args.length == 0 || args[0].equalsIgnoreCase("help")) {
|
||||
boolean isAdmin = sender.hasPermission("statusapi.admin")
|
||||
|| !(sender instanceof net.md_5.bungee.api.connection.ProxiedPlayer);
|
||||
send(sender, "&8&m──────────────────────────────────────────");
|
||||
send(sender, "&6&lStatusAPI &7| Befehle");
|
||||
if (isAdmin) {
|
||||
send(sender, "&e/statusapi reload &7– Scoreboard, Tablist & AFK neu laden");
|
||||
} else {
|
||||
send(sender, "&7Keine weiteren Unterbefehle verf\u00fcgbar.");
|
||||
}
|
||||
send(sender, "&8&m──────────────────────────────────────────");
|
||||
return;
|
||||
}
|
||||
|
||||
if (!sender.hasPermission("statusapi.admin")) {
|
||||
send(sender, "&cKeine Berechtigung.");
|
||||
return;
|
||||
}
|
||||
|
||||
switch (args[0].toLowerCase()) {
|
||||
case "reload":
|
||||
send(sender, "&7Lade &6Scoreboard &7und &6Tablist &7neu...");
|
||||
plugin.reloadModules();
|
||||
send(sender, "&aScoreboard &7und &aTablist &7wurden neu geladen.");
|
||||
send(sender, "&7PAPI-Tokens erkannt: &e" + papiTokensJson);
|
||||
break;
|
||||
|
||||
default:
|
||||
send(sender, "&cUnbekannter Unterbefehl. Nutze &e/statusapi help&c.");
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
private static void send(net.md_5.bungee.api.CommandSender s, String text) {
|
||||
s.sendMessage(new net.md_5.bungee.api.chat.TextComponent(
|
||||
net.md_5.bungee.api.ChatColor.translateAlternateColorCodes('&', text)));
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -16,15 +16,16 @@ public class UpdateChecker {
|
||||
private final String currentVersion;
|
||||
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 volatile String latestVersion = "";
|
||||
private volatile String latestUrl = "";
|
||||
private volatile String latestVersion = "";
|
||||
private volatile String latestUrl = "";
|
||||
private volatile boolean latestPreRelease = false;
|
||||
|
||||
private static final Pattern ASSET_NAME_PATTERN = Pattern.compile("\"name\"\\s*:\\s*\"([^\"]+)\"", Pattern.CASE_INSENSITIVE);
|
||||
private static final Pattern DOWNLOAD_PATTERN = Pattern.compile("\"browser_download_url\"\\s*:\\s*\"([^\"]+)\"", Pattern.CASE_INSENSITIVE);
|
||||
private static final Pattern TAG_NAME_PATTERN = Pattern.compile("\"tag_name\"\\s*:\\s*\"([^\"]+)\"", Pattern.CASE_INSENSITIVE);
|
||||
private static final Pattern TAG_NAME_PATTERN = Pattern.compile("\"tag_name\"\\s*:\\s*\"([^\"]+)\"", Pattern.CASE_INSENSITIVE);
|
||||
private static final Pattern HTML_URL_PATTERN = Pattern.compile("\"html_url\"\\s*:\\s*\"([^\"]+)\"", Pattern.CASE_INSENSITIVE);
|
||||
private static final Pattern PRERELEASE_PATTERN = Pattern.compile("\"prerelease\"\\s*:\\s*(true|false)", Pattern.CASE_INSENSITIVE);
|
||||
|
||||
public UpdateChecker(Plugin plugin, String currentVersion, int intervalHours) {
|
||||
this.plugin = plugin;
|
||||
@@ -55,7 +56,7 @@ public class UpdateChecker {
|
||||
|
||||
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
|
||||
String foundVersion = null;
|
||||
Matcher tagM = TAG_NAME_PATTERN.matcher(body);
|
||||
@@ -71,39 +72,23 @@ public class UpdateChecker {
|
||||
foundVersion = foundVersion.substring(1);
|
||||
}
|
||||
|
||||
String foundUrl = null;
|
||||
|
||||
// Wir suchen im gesamten Body nach der JAR-Datei "StatusAPI.jar"
|
||||
// Da das neueste Release zuerst kommt, brechen wir ab, sobald wir eine passende JAR finden
|
||||
Matcher nameMatcher = ASSET_NAME_PATTERN.matcher(body);
|
||||
Matcher downloadMatcher = DOWNLOAD_PATTERN.matcher(body);
|
||||
|
||||
java.util.List<String> names = new java.util.ArrayList<>();
|
||||
java.util.List<String> urls = new java.util.ArrayList<>();
|
||||
|
||||
while (nameMatcher.find()) {
|
||||
names.add(nameMatcher.group(1));
|
||||
}
|
||||
while (downloadMatcher.find()) {
|
||||
urls.add(downloadMatcher.group(1));
|
||||
// Release-Seiten-URL fuer die Ingame-Meldung
|
||||
String foundUrl = "https://git.viper.ipv64.net/M_Viper/StatusAPI/releases";
|
||||
Matcher urlM = HTML_URL_PATTERN.matcher(body);
|
||||
if (urlM.find()) {
|
||||
foundUrl = urlM.group(1).trim();
|
||||
}
|
||||
|
||||
int pairs = Math.min(names.size(), urls.size());
|
||||
for (int i = 0; i < pairs; i++) {
|
||||
String name = names.get(i).trim();
|
||||
String url = urls.get(i);
|
||||
if ("StatusAPI.jar".equalsIgnoreCase(name)) {
|
||||
foundUrl = url;
|
||||
break; // Erste (also neueste) passende JAR nehmen
|
||||
}
|
||||
// prerelease-Flag aus dem ersten Release-Block lesen
|
||||
boolean foundPreRelease = false;
|
||||
Matcher preM = PRERELEASE_PATTERN.matcher(body);
|
||||
if (preM.find()) {
|
||||
foundPreRelease = Boolean.parseBoolean(preM.group(1).trim());
|
||||
}
|
||||
|
||||
if (foundUrl == null) {
|
||||
plugin.getLogger().warning("Keine StatusAPI.jar im neuesten Release gefunden.");
|
||||
return;
|
||||
}
|
||||
latestVersion = foundVersion;
|
||||
latestUrl = foundUrl;
|
||||
latestVersion = foundVersion;
|
||||
latestUrl = foundUrl;
|
||||
latestPreRelease = foundPreRelease;
|
||||
|
||||
} catch (Exception e) {
|
||||
plugin.getLogger().log(Level.SEVERE, "Fehler beim Update-Check: " + e.getMessage(), e);
|
||||
@@ -118,9 +103,15 @@ public class UpdateChecker {
|
||||
return latestUrl != null ? latestUrl : "";
|
||||
}
|
||||
|
||||
public boolean isLatestPreRelease() {
|
||||
return latestPreRelease;
|
||||
}
|
||||
|
||||
public boolean isUpdateAvailable(String currentVer) {
|
||||
String lv = getLatestVersion();
|
||||
if (lv.isEmpty()) return false;
|
||||
// Nur melden wenn latest STRIKT groesser als current ist.
|
||||
// Laeuft eine Dev-Version (current > latest), kein Hinweis.
|
||||
return compareVersions(lv, currentVer) > 0;
|
||||
}
|
||||
|
||||
|
||||
@@ -3,7 +3,7 @@ package net.viper.status.module;
|
||||
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 {
|
||||
|
||||
|
||||
@@ -8,7 +8,7 @@ import java.util.Map;
|
||||
/**
|
||||
* Verwaltet alle geladenen Module.
|
||||
* 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 {
|
||||
|
||||
@@ -41,12 +41,20 @@ 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) {
|
||||
return modules.get(name.toLowerCase());
|
||||
}
|
||||
|
||||
/**
|
||||
* Ersetzt ein bestehendes Modul durch eine neue Instanz (f\u00fcr Reload).
|
||||
* Das alte Modul muss bereits deaktiviert worden sein.
|
||||
*/
|
||||
public void replaceModule(String name, Module newModule) {
|
||||
modules.put(name.toLowerCase(), newModule);
|
||||
}
|
||||
|
||||
@SuppressWarnings("unchecked")
|
||||
public <T extends Module> T getModule(Class<T> clazz) {
|
||||
for (Module m : modules.values()) {
|
||||
|
||||
+134
-13
@@ -3,6 +3,7 @@ package net.viper.status.modules.AutoMessage;
|
||||
import net.md_5.bungee.api.ChatColor;
|
||||
import net.md_5.bungee.api.CommandSender;
|
||||
import net.md_5.bungee.api.ProxyServer;
|
||||
import net.md_5.bungee.api.chat.BaseComponent;
|
||||
import net.md_5.bungee.api.chat.TextComponent;
|
||||
import net.md_5.bungee.api.plugin.Command;
|
||||
import net.md_5.bungee.api.plugin.Plugin;
|
||||
@@ -13,6 +14,7 @@ import java.io.File;
|
||||
import java.io.IOException;
|
||||
import java.nio.charset.StandardCharsets;
|
||||
import java.nio.file.Files;
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
import java.util.Properties;
|
||||
import java.util.concurrent.TimeUnit;
|
||||
@@ -23,10 +25,10 @@ import java.util.concurrent.atomic.AtomicInteger;
|
||||
*
|
||||
* Fix #5:
|
||||
* - 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)
|
||||
* lädt die Konfiguration neu und setzt den Zähler zurück.
|
||||
* - TextComponent.fromLegacy() → ChatColor.translateAlternateColorCodes für §-Codes.
|
||||
* l\u00e4dt die Konfiguration neu und setzt den Z\u00e4hler zur\u00fcck.
|
||||
* - TextComponent.fromLegacy() → ChatColor.translateAlternateColorCodes f\u00fcr §-Codes.
|
||||
*/
|
||||
public class AutoMessageModule implements Module {
|
||||
|
||||
@@ -34,7 +36,7 @@ public class AutoMessageModule implements Module {
|
||||
private StatusAPI api;
|
||||
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 int intervalSeconds = 300;
|
||||
private volatile String fileName = "messages.txt";
|
||||
@@ -82,7 +84,7 @@ public class AutoMessageModule implements Module {
|
||||
try {
|
||||
Files.write(target.toPath(),
|
||||
("# 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));
|
||||
api.getLogger().info("[AutoMessage] " + fileName + " wurde als leere Vorlage erstellt.");
|
||||
} catch (IOException e) {
|
||||
@@ -95,7 +97,7 @@ public class AutoMessageModule implements Module {
|
||||
enabled = Boolean.parseBoolean(props.getProperty("automessage.enabled", "false"));
|
||||
String rawInterval = props.getProperty("automessage.interval", "300");
|
||||
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");
|
||||
prefix = props.getProperty("automessage.prefix", "");
|
||||
}
|
||||
@@ -129,7 +131,7 @@ public class AutoMessageModule implements Module {
|
||||
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;
|
||||
try {
|
||||
messages = Files.readAllLines(messageFile.toPath(), StandardCharsets.UTF_8);
|
||||
@@ -145,12 +147,13 @@ public class AutoMessageModule implements Module {
|
||||
if (idx >= messages.size()) idx = 0;
|
||||
|
||||
String raw = messages.get(idx);
|
||||
String prefixPart = prefix.isEmpty() ? "" : ChatColor.translateAlternateColorCodes('&', prefix) + " ";
|
||||
// Fix: §-Codes direkt übersetzen (messages.txt nutzt §-Codes)
|
||||
String text = prefixPart + ChatColor.translateAlternateColorCodes('&',
|
||||
raw.replace("\u00a7", "&").replace("§", "&"));
|
||||
String prefixPart = prefix.isEmpty() ? "" : ChatColor.translateAlternateColorCodes('&', applyGradients(prefix)) + " ";
|
||||
// Gradient zuerst auflösen, dann §/&-Codes übersetzen
|
||||
String normalized = raw.replace("\u00a7", "&").replace("§", "&");
|
||||
String text = prefixPart + ChatColor.translateAlternateColorCodes('&', applyGradients(normalized));
|
||||
|
||||
ProxyServer.getInstance().broadcast(new TextComponent(text));
|
||||
BaseComponent[] components = TextComponent.fromLegacyText(text);
|
||||
ProxyServer.getInstance().broadcast(components);
|
||||
}, intervalSeconds, intervalSeconds, TimeUnit.SECONDS).getId();
|
||||
}
|
||||
|
||||
@@ -160,4 +163,122 @@ public class AutoMessageModule implements Module {
|
||||
taskId = -1;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// ── Gradient-Support (%gradient:FARBE1:FARBE2:...:TEXT%) ─────────────────
|
||||
|
||||
private String applyGradients(String input) {
|
||||
if (input == null || !input.contains("%gradient:")) return input;
|
||||
StringBuilder result = new StringBuilder();
|
||||
int i = 0;
|
||||
while (i < input.length()) {
|
||||
int start = input.indexOf("%gradient:", i);
|
||||
if (start < 0) { result.append(input.substring(i)); break; }
|
||||
result.append(input, i, start);
|
||||
int end = input.indexOf("%", start + 10);
|
||||
if (end < 0) { result.append(input.substring(start)); break; }
|
||||
String inner = input.substring(start + 10, end);
|
||||
List<int[]> stops = new ArrayList<>();
|
||||
int colonIdx = 0;
|
||||
while (colonIdx < inner.length()) {
|
||||
int nextColon = inner.indexOf(':', colonIdx);
|
||||
if (nextColon < 0) break;
|
||||
String candidate = inner.substring(colonIdx, nextColon);
|
||||
int[] rgb = parseGradientColor(candidate);
|
||||
if (rgb != null) { stops.add(rgb); colonIdx = nextColon + 1; }
|
||||
else break;
|
||||
}
|
||||
if (stops.size() < 2) { result.append(input, start, end + 1); i = end + 1; continue; }
|
||||
String text = inner.substring(colonIdx);
|
||||
String plain = ChatColor.stripColor(ChatColor.translateAlternateColorCodes('&', text));
|
||||
boolean bold = text.contains("&l") || text.contains("\u00A7l");
|
||||
boolean italic = text.contains("&o") || text.contains("\u00A7o");
|
||||
boolean underline = text.contains("&n") || text.contains("\u00A7n");
|
||||
boolean strike = text.contains("&m") || text.contains("\u00A7m");
|
||||
String fmt = (bold ? "\u00A7l" : "") + (italic ? "\u00A7o" : "")
|
||||
+ (underline ? "\u00A7n" : "") + (strike ? "\u00A7m" : "");
|
||||
int visLen = 0;
|
||||
for (char ch : plain.toCharArray()) if (ch != ' ') visLen++;
|
||||
if (visLen == 0) visLen = 1;
|
||||
int charIdx = 0;
|
||||
int[] lastRgb = stops.get(0);
|
||||
for (char ch : plain.toCharArray()) {
|
||||
if (ch == ' ') {
|
||||
result.append('\u00A7').append('x');
|
||||
result.append('\u00A7').append(String.format("%02X", lastRgb[0]).charAt(0));
|
||||
result.append('\u00A7').append(String.format("%02X", lastRgb[0]).charAt(1));
|
||||
result.append('\u00A7').append(String.format("%02X", lastRgb[1]).charAt(0));
|
||||
result.append('\u00A7').append(String.format("%02X", lastRgb[1]).charAt(1));
|
||||
result.append('\u00A7').append(String.format("%02X", lastRgb[2]).charAt(0));
|
||||
result.append('\u00A7').append(String.format("%02X", lastRgb[2]).charAt(1));
|
||||
result.append(fmt).append(ch);
|
||||
continue;
|
||||
}
|
||||
float pos = visLen <= 1 ? 0f : (float) charIdx / (visLen - 1);
|
||||
lastRgb = interpolateGradient(stops, pos);
|
||||
result.append('\u00A7').append('x');
|
||||
result.append('\u00A7').append(String.format("%02X", lastRgb[0]).charAt(0));
|
||||
result.append('\u00A7').append(String.format("%02X", lastRgb[0]).charAt(1));
|
||||
result.append('\u00A7').append(String.format("%02X", lastRgb[1]).charAt(0));
|
||||
result.append('\u00A7').append(String.format("%02X", lastRgb[1]).charAt(1));
|
||||
result.append('\u00A7').append(String.format("%02X", lastRgb[2]).charAt(0));
|
||||
result.append('\u00A7').append(String.format("%02X", lastRgb[2]).charAt(1));
|
||||
result.append(fmt).append(ch);
|
||||
charIdx++;
|
||||
}
|
||||
// §r nach dem Gradient-Block, damit nachfolgende §8/§7 etc. wieder greifen
|
||||
result.append('\u00A7').append('r');
|
||||
i = end + 1;
|
||||
}
|
||||
return result.toString();
|
||||
}
|
||||
|
||||
private int[] parseGradientColor(String s) {
|
||||
s = s.trim();
|
||||
if (s.startsWith("&#")) s = s.substring(1);
|
||||
if (s.startsWith("#") && s.length() == 7) {
|
||||
try {
|
||||
return new int[]{
|
||||
Integer.parseInt(s.substring(1,3),16),
|
||||
Integer.parseInt(s.substring(3,5),16),
|
||||
Integer.parseInt(s.substring(5,7),16)
|
||||
};
|
||||
} catch (Exception ignored) {}
|
||||
}
|
||||
if (s.startsWith("&") && s.length() == 2) return mcColorToRgb(s.charAt(1));
|
||||
return null;
|
||||
}
|
||||
|
||||
private int[] interpolateGradient(List<int[]> stops, float pos) {
|
||||
if (stops.size() == 1) return stops.get(0);
|
||||
float scaled = pos * (stops.size() - 1);
|
||||
int i0 = Math.min((int) scaled, stops.size() - 2);
|
||||
float t = scaled - i0;
|
||||
return new int[]{
|
||||
(int)(stops.get(i0)[0] * (1-t) + stops.get(i0+1)[0] * t),
|
||||
(int)(stops.get(i0)[1] * (1-t) + stops.get(i0+1)[1] * t),
|
||||
(int)(stops.get(i0)[2] * (1-t) + stops.get(i0+1)[2] * t)
|
||||
};
|
||||
}
|
||||
|
||||
private static int[] mcColorToRgb(char code) {
|
||||
switch (Character.toLowerCase(code)) {
|
||||
case '0': return new int[]{ 0, 0, 0};
|
||||
case '1': return new int[]{ 0, 0, 170};
|
||||
case '2': return new int[]{ 0, 170, 0};
|
||||
case '3': return new int[]{ 0, 170, 170};
|
||||
case '4': return new int[]{170, 0, 0};
|
||||
case '5': return new int[]{170, 0, 170};
|
||||
case '6': return new int[]{255, 170, 0};
|
||||
case '7': return new int[]{170, 170, 170};
|
||||
case '8': return new int[]{ 85, 85, 85};
|
||||
case '9': return new int[]{ 85, 85, 255};
|
||||
case 'a': return new int[]{ 85, 255, 85};
|
||||
case 'b': return new int[]{ 85, 255, 255};
|
||||
case 'c': return new int[]{255, 85, 85};
|
||||
case 'd': return new int[]{255, 85, 255};
|
||||
case 'e': return new int[]{255, 255, 85};
|
||||
case 'f': return new int[]{255, 255, 255};
|
||||
default: return null;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,756 @@
|
||||
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;
|
||||
import java.util.logging.Level;
|
||||
|
||||
/**
|
||||
* 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 Command afkCommand;
|
||||
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);
|
||||
// Plugin-Message-Channel registrieren (BungeeCord → Spigot)
|
||||
ProxyServer.getInstance().registerChannel("statusapi:afk");
|
||||
afkCommand = new Command("afk") {
|
||||
@Override
|
||||
public void execute(CommandSender sender, String[] args) {
|
||||
if (!(sender instanceof ProxiedPlayer)) {
|
||||
sender.sendMessage(plain("&cNur f\u00fcr Spieler."));
|
||||
return;
|
||||
}
|
||||
toggleAfk((ProxiedPlayer) sender);
|
||||
}
|
||||
};
|
||||
ProxyServer.getInstance().getPluginManager().registerCommand(plugin, afkCommand);
|
||||
|
||||
if (idleEnabled) {
|
||||
idleCheckTask = ProxyServer.getInstance().getScheduler().schedule(
|
||||
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();
|
||||
|
||||
// Die Spigot-Seite (StatusAPIBridge) fuehrt eine eigene afkState-Map und
|
||||
// setzt daraus den [AFK]-Nametag. Wird hier nur lokal geleert, bleibt der
|
||||
// Prefix nach einem Reload ueber dem Kopf haengen, weil die Bridge nie ein
|
||||
// Update auf false bekommt. Darum vor dem Leeren explizit zuruecksetzen.
|
||||
for (UUID id : new ArrayList<>(StatusAPI.playerAfk.keySet())) {
|
||||
ProxiedPlayer p = ProxyServer.getInstance().getPlayer(id);
|
||||
if (p != null) { try { clearTitle(p); } catch (Exception ignored) {} }
|
||||
try { broadcastAfkState(id, false); } catch (Exception ignored) {}
|
||||
}
|
||||
StatusAPI.playerAfk.clear();
|
||||
lastActivity.clear();
|
||||
INSTANCE = null;
|
||||
|
||||
// Listener und Command wieder abmelden, damit eine neue Instanz (Reload)
|
||||
// sauber registrieren kann und keine Doppel-Registrierung entsteht.
|
||||
try { ProxyServer.getInstance().getPluginManager().unregisterListener(this); } catch (Exception ignored) {}
|
||||
if (afkCommand != null) {
|
||||
try { ProxyServer.getInstance().getPluginManager().unregisterCommand(afkCommand); } catch (Exception ignored) {}
|
||||
afkCommand = null;
|
||||
}
|
||||
try { ProxyServer.getInstance().unregisterChannel("statusapi:afk"); } catch (Exception ignored) {}
|
||||
}
|
||||
|
||||
// ── Events ───────────────────────────────────────────────────────────────
|
||||
|
||||
@EventHandler
|
||||
public void onChat(ChatEvent e) {
|
||||
if (!(e.getSender() instanceof ProxiedPlayer)) return;
|
||||
ProxiedPlayer p = (ProxiedPlayer) e.getSender();
|
||||
recordActivity(p.getUniqueId());
|
||||
String msg = e.getMessage();
|
||||
if (msg == null) return;
|
||||
if (!msg.toLowerCase(Locale.ROOT).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 ───────────────────────────────────────────────────────────
|
||||
|
||||
/**
|
||||
* Setzt den AFK-Status aus einer externen Quelle (POST /player/afk der Bridge).
|
||||
*
|
||||
* Laeuft ueber denselben Weg wie /afk und die Idle-Erkennung, sonst waere der
|
||||
* Status zwar gesetzt, aber ohne Title und ohne Broadcast an die Spigot-Server –
|
||||
* der [AFK]-Nametag wuerde dann nicht mitziehen.
|
||||
*/
|
||||
public static void setBridgeAfk(UUID uuid, boolean afk) {
|
||||
AfkModule inst = INSTANCE;
|
||||
ProxiedPlayer p = ProxyServer.getInstance().getPlayer(uuid);
|
||||
if (inst != null && p != null) {
|
||||
inst.setAfk(p, afk);
|
||||
return;
|
||||
}
|
||||
// Spieler nicht auf dem Proxy erreichbar: nur den Status nachfuehren.
|
||||
if (afk) {
|
||||
StatusAPI.playerAfk.put(uuid, true);
|
||||
} else {
|
||||
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();
|
||||
// Zustandswechsel atomar: setAfk wird sowohl vom Scheduler (checkIdle) als
|
||||
// auch von den HTTP-Threads der Bridge (Bewegung, Compass) aufgerufen. Ein
|
||||
// getrenntes isAfk()-Vorabpruefen liess beide Threads durchlaufen – Folge
|
||||
// waren doppelte Title-Tasks und doppelte Broadcasts.
|
||||
if (afk) {
|
||||
if (StatusAPI.playerAfk.putIfAbsent(id, Boolean.TRUE) != null) return;
|
||||
startTitleTask(p);
|
||||
} else {
|
||||
if (StatusAPI.playerAfk.remove(id) == null) return;
|
||||
lastActivity.put(id, System.currentTimeMillis());
|
||||
TitlePair pair = activePair.remove(id);
|
||||
stopTitleTask(id);
|
||||
clearTitle(p);
|
||||
if (pair != null) sendTitleEntry(p, pair.unset);
|
||||
}
|
||||
// AFK-Status an alle Spigot-Server broadcasten damit die Bridge
|
||||
// den Nametag-Prefix sofort aktualisieren kann.
|
||||
broadcastAfkState(id, afk);
|
||||
}
|
||||
|
||||
/**
|
||||
* Schickt eine Plugin-Message (Channel "statusapi:afk") an alle Spigot-Server,
|
||||
* auf denen der betroffene Spieler ODER irgendein anderer Spieler eingeloggt ist.
|
||||
* Format: "UUID:true" / "UUID:false"
|
||||
*/
|
||||
private void broadcastAfkState(UUID uuid, boolean afk) {
|
||||
byte[] payload = (uuid.toString() + ":" + afk).getBytes(java.nio.charset.StandardCharsets.UTF_8);
|
||||
Set<net.md_5.bungee.api.config.ServerInfo> reached = new java.util.HashSet<>();
|
||||
// Zuerst: Server des betroffenen Spielers direkt ansprechen
|
||||
ProxiedPlayer self = ProxyServer.getInstance().getPlayer(uuid);
|
||||
if (self != null && self.isConnected() && self.getServer() != null) {
|
||||
net.md_5.bungee.api.config.ServerInfo srv = self.getServer().getInfo();
|
||||
if (srv != null) { srv.sendData("statusapi:afk", payload); reached.add(srv); }
|
||||
}
|
||||
// Dann: alle anderen Online-Spieler – deren Server brauchen das Update
|
||||
// damit der Prefix über dem AFK-Spieler-Kopf auch bei Anderen stimmt.
|
||||
for (ProxiedPlayer other : ProxyServer.getInstance().getPlayers()) {
|
||||
if (!other.isConnected() || other.getServer() == null) continue;
|
||||
net.md_5.bungee.api.config.ServerInfo srv = other.getServer().getInfo();
|
||||
if (srv != null && !reached.contains(srv)) {
|
||||
srv.sendData("statusapi:afk", payload);
|
||||
reached.add(srv);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private void checkIdle() {
|
||||
long now = System.currentTimeMillis();
|
||||
long thresholdMs = idleSeconds * 1000L;
|
||||
for (ProxiedPlayer p : ProxyServer.getInstance().getPlayers()) {
|
||||
try {
|
||||
if (!p.isConnected()) continue;
|
||||
UUID id = p.getUniqueId();
|
||||
if (p.hasPermission(bypassPerm) || isAfk(id)) continue;
|
||||
Long last = lastActivity.get(id);
|
||||
if (last == null) { lastActivity.put(id, now); continue; }
|
||||
if (now - last >= thresholdMs) setAfk(p, true);
|
||||
} catch (Exception ex) {
|
||||
plugin.getLogger().log(Level.FINE, "[AfkModule] Fehler in checkIdle fuer Spieler.", ex);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 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);
|
||||
if (!sendTitleEntry(p, pair.set, true)) { // firstSend=true → TitleTimes mitsenden
|
||||
stopTitleTask(id);
|
||||
return;
|
||||
}
|
||||
|
||||
// Kurz vor Ablauf der Standzeit erneut senden, damit der Title nie ausblendet.
|
||||
// Fruehere Rechnung (titleStay - 20) * 50 lief bei kleinem afk.title.stay ins
|
||||
// Leere: unter 30 Ticks war das Intervall laenger als die Standzeit selbst und
|
||||
// der Title verschwand zwischen zwei Ticks.
|
||||
long stayMs = Math.max(1L, titleStay) * 50L;
|
||||
long intervalMs = Math.max(500L, stayMs - 1000L);
|
||||
ScheduledTask task = ProxyServer.getInstance().getScheduler().schedule(plugin, () -> {
|
||||
try {
|
||||
ProxiedPlayer online = ProxyServer.getInstance().getPlayer(id);
|
||||
if (online == null || !online.isConnected() || !isAfk(id)) {
|
||||
stopTitleTask(id);
|
||||
return;
|
||||
}
|
||||
String[] current = activeTitleEntry.get(id);
|
||||
if (current == null) return;
|
||||
// firstSend=false → kein TitleTimes → kein Blinken beim Repeat
|
||||
if (!sendTitleEntry(online, current, false)) {
|
||||
stopTitleTask(id);
|
||||
return;
|
||||
}
|
||||
// Der Spieler kann sich waehrend des Sendens bewegt haben – setAfk(false)
|
||||
// laeuft auf dem HTTP-Thread der Bridge. Dann klebt jetzt wieder der
|
||||
// AFK-Title auf dem Bildschirm, obwohl der Spieler zurueck ist.
|
||||
if (!isAfk(id)) {
|
||||
clearTitle(online);
|
||||
sendTitleEntry(online, pair.unset);
|
||||
stopTitleTask(id);
|
||||
}
|
||||
} catch (Exception ex) {
|
||||
plugin.getLogger().log(Level.FINE, "[AfkModule] Fehler im Title-Repeat-Task.", ex);
|
||||
stopTitleTask(id);
|
||||
}
|
||||
}, 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);
|
||||
// BUGFIX: activePair hier NICHT entfernen wenn der Spieler noch AFK ist –
|
||||
// setAfk(false) liest activePair noch aus (für den Unset-Title) und entfernt es selbst.
|
||||
// Wenn der Spieler aber NICHT mehr AFK ist (z.B. Exception im Task), muss activePair
|
||||
// hier bereinigt werden, sonst Memory Leak.
|
||||
if (!Boolean.TRUE.equals(StatusAPI.playerAfk.get(id))) {
|
||||
activePair.remove(id);
|
||||
}
|
||||
}
|
||||
|
||||
/** Entfernt den Title sofort vom Bildschirm 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.
|
||||
*/
|
||||
/**
|
||||
* Sendet Title + Subtitle an den Spieler.
|
||||
* @param firstSend true = TitleTimes-Packet mitsenden (erster Aufruf).
|
||||
* false = nur Title/Subtitle refreshen, KEIN TitleTimes →
|
||||
* verhindert das Blinken (fade-in/-out) bei jedem Repeat-Tick.
|
||||
*/
|
||||
private boolean sendTitleEntry(ProxiedPlayer p, String[] entry, boolean firstSend) {
|
||||
if (p == null || !p.isConnected() || entry == null || entry.length == 0) return false;
|
||||
|
||||
String titleRaw = ChatColor.translateAlternateColorCodes('&', applyGradients(
|
||||
entry[0] == null ? "" : entry[0]
|
||||
));
|
||||
String subtitleRaw = entry.length > 1
|
||||
? ChatColor.translateAlternateColorCodes('&', applyGradients(entry[1] == null ? "" : entry[1]))
|
||||
: "";
|
||||
|
||||
try {
|
||||
if (sendPkt == null) throw new IllegalStateException("sendPkt not initialized");
|
||||
|
||||
// TitleTimes NUR beim ersten Senden schicken.
|
||||
// Wird das Packet bei jedem Repeat-Tick gesendet, startet der Client
|
||||
// jedes Mal eine neue fade-in-Animation → sichtbares Blinken.
|
||||
if (firstSend) {
|
||||
net.md_5.bungee.protocol.packet.TitleTimes times = new net.md_5.bungee.protocol.packet.TitleTimes();
|
||||
times.setFadeIn(titleFadeIn);
|
||||
times.setStay(titleStay);
|
||||
times.setFadeOut(titleFadeOut);
|
||||
sendPkt.invoke(p, times);
|
||||
}
|
||||
|
||||
// Title-Packet
|
||||
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);
|
||||
|
||||
return true;
|
||||
|
||||
} catch (Exception e) {
|
||||
// Fallback auf Title-API
|
||||
try {
|
||||
Title title = ProxyServer.getInstance().createTitle();
|
||||
title.title(buildComponents(titleRaw));
|
||||
title.subTitle(buildComponents(subtitleRaw));
|
||||
if (firstSend) {
|
||||
title.fadeIn(titleFadeIn);
|
||||
title.stay(titleStay);
|
||||
title.fadeOut(titleFadeOut);
|
||||
} else {
|
||||
// Kein Fade → bleibt stabil stehen ohne Blink-Effekt
|
||||
title.fadeIn(0);
|
||||
title.stay(titleStay);
|
||||
title.fadeOut(0);
|
||||
}
|
||||
p.sendTitle(title);
|
||||
return true;
|
||||
} catch (Exception ignored) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/** Overload für Aufrufe ohne firstSend-Flag (z.B. unset-Title beim Zurückkommen). */
|
||||
private boolean sendTitleEntry(ProxiedPlayer p, String[] entry) {
|
||||
return sendTitleEntry(p, entry, true);
|
||||
}
|
||||
|
||||
/** Fasst BaseComponent[] in eine TextComponent zusammen (f\u00fcr Title/Subtitle setText). */
|
||||
private static net.md_5.bungee.api.chat.BaseComponent mergeComponents(BaseComponent[] parts) {
|
||||
TextComponent root = new TextComponent("");
|
||||
for (BaseComponent bc : parts) root.addExtra(bc);
|
||||
return root;
|
||||
}
|
||||
|
||||
// ── 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;
|
||||
|
||||
/**
|
||||
* Eigenständiger AntiBot/Attack-Guard.
|
||||
* Eigenst\u00e4ndiger AntiBot/Attack-Guard.
|
||||
*
|
||||
* Fixes:
|
||||
* - cleanupExpired() nutzt jetzt removeIf() statt Iteration + remove() (Bug #3)
|
||||
@@ -246,28 +246,73 @@ public class AntiBotModule implements Module, Listener {
|
||||
}
|
||||
}
|
||||
|
||||
// FIX Netzwerk-Timeout: VPN-Check asynchron ausführen damit der BungeeCord-
|
||||
// Netzwerk-Thread nicht blockiert wird. Ein blockierender HTTP-Request (bis zu
|
||||
// vpnTimeoutMs = 2500 ms) im PreLoginEvent-Handler verhindert, dass KeepAlive-
|
||||
// Pakete an bereits eingeloggte Spieler verarbeitet werden → Disconnect.
|
||||
if (vpnCheckEnabled) {
|
||||
VpnCheckResult info = getVpnInfo(ip, now);
|
||||
if (info != null) {
|
||||
boolean shouldBlock = (vpnBlockProxy && info.proxy) || (vpnBlockHosting && info.hosting);
|
||||
if (shouldBlock) {
|
||||
logSecurityEvent("vpn_detected", ip, event.getConnection(), "proxy=" + info.proxy + ", hosting=" + info.hosting);
|
||||
if (learningModeEnabled) {
|
||||
if (vpnBlockProxy && info.proxy) addLearningScore(ip, now, learningVpnProxyPoints, "vpn-proxy", false);
|
||||
if (vpnBlockHosting && info.hosting) addLearningScore(ip, now, learningVpnHostingPoints, "vpn-hosting", false);
|
||||
int current = getLearningScore(ip, now);
|
||||
if (current >= learningScoreThreshold) {
|
||||
// Cache-Treffer: sofort prüfen, kein async nötig
|
||||
VpnCacheEntry cached = vpnCache.get(ip);
|
||||
if (cached != null && cached.expiresAt > now) {
|
||||
VpnCheckResult info = cached.result;
|
||||
if (info != null) {
|
||||
boolean shouldBlock = (vpnBlockProxy && info.proxy) || (vpnBlockHosting && info.hosting);
|
||||
if (shouldBlock) {
|
||||
logSecurityEvent("vpn_detected", ip, event.getConnection(), "proxy=" + info.proxy + ", hosting=" + info.hosting);
|
||||
if (learningModeEnabled) {
|
||||
if (vpnBlockProxy && info.proxy) addLearningScore(ip, now, learningVpnProxyPoints, "vpn-proxy", false);
|
||||
if (vpnBlockHosting && info.hosting) addLearningScore(ip, now, learningVpnHostingPoints, "vpn-hosting", false);
|
||||
int current = getLearningScore(ip, now);
|
||||
if (current >= learningScoreThreshold) {
|
||||
blockIp(ip, now);
|
||||
logSecurityEvent("learning_threshold_block", ip, event.getConnection(), "reason=vpn, score=" + current);
|
||||
recordLearningEvent("BLOCK " + ip + " reason=vpn score=" + current);
|
||||
blockEvent(event);
|
||||
}
|
||||
} else {
|
||||
blockIp(ip, now);
|
||||
logSecurityEvent("learning_threshold_block", ip, event.getConnection(), "reason=vpn, score=" + current);
|
||||
recordLearningEvent("BLOCK " + ip + " reason=vpn score=" + current);
|
||||
logSecurityEvent("vpn_block", ip, event.getConnection(), "mode=direct, proxy=" + info.proxy + ", hosting=" + info.hosting);
|
||||
blockEvent(event);
|
||||
}
|
||||
} else {
|
||||
blockIp(ip, now);
|
||||
logSecurityEvent("vpn_block", ip, event.getConnection(), "mode=direct, proxy=" + info.proxy + ", hosting=" + info.hosting);
|
||||
blockEvent(event);
|
||||
}
|
||||
}
|
||||
} else {
|
||||
// Kein Cache-Treffer → HTTP-Request async ausführen
|
||||
event.registerIntent(plugin);
|
||||
final long nowFinal = now;
|
||||
ProxyServer.getInstance().getScheduler().runAsync(plugin, () -> {
|
||||
try {
|
||||
VpnCheckResult info = requestIpApi(ip);
|
||||
if (info != null) {
|
||||
VpnCacheEntry entry = new VpnCacheEntry();
|
||||
entry.result = info;
|
||||
entry.expiresAt = nowFinal + Math.max(1, vpnCacheMinutes) * 60_000L;
|
||||
vpnCache.put(ip, entry);
|
||||
|
||||
boolean shouldBlock = (vpnBlockProxy && info.proxy) || (vpnBlockHosting && info.hosting);
|
||||
if (shouldBlock) {
|
||||
logSecurityEvent("vpn_detected", ip, event.getConnection(), "proxy=" + info.proxy + ", hosting=" + info.hosting);
|
||||
if (learningModeEnabled) {
|
||||
if (vpnBlockProxy && info.proxy) addLearningScore(ip, nowFinal, learningVpnProxyPoints, "vpn-proxy", false);
|
||||
if (vpnBlockHosting && info.hosting) addLearningScore(ip, nowFinal, learningVpnHostingPoints, "vpn-hosting", false);
|
||||
int current = getLearningScore(ip, nowFinal);
|
||||
if (current >= learningScoreThreshold) {
|
||||
blockIp(ip, nowFinal);
|
||||
logSecurityEvent("learning_threshold_block", ip, event.getConnection(), "reason=vpn, score=" + current);
|
||||
recordLearningEvent("BLOCK " + ip + " reason=vpn score=" + current);
|
||||
blockEvent(event);
|
||||
}
|
||||
} else {
|
||||
blockIp(ip, nowFinal);
|
||||
logSecurityEvent("vpn_block", ip, event.getConnection(), "mode=direct, proxy=" + info.proxy + ", hosting=" + info.hosting);
|
||||
blockEvent(event);
|
||||
}
|
||||
}
|
||||
}
|
||||
} finally {
|
||||
event.completeIntent(plugin);
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -329,6 +374,19 @@ public class AntiBotModule implements Module, Listener {
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Sperrt eine IP f\u00fcr die konfigurierte Block-Dauer (antibot.ip.block_seconds).
|
||||
* Kann von anderen Modulen aufgerufen werden (z. B. MultiAccountGuard).
|
||||
* @param ip Die zu sperrende IP-Adresse
|
||||
* @param durationSeconds Sperrdauer in Sekunden (0 = antibot-Standard verwenden)
|
||||
*/
|
||||
public void blockIpExternal(String ip, int durationSeconds) {
|
||||
long now = System.currentTimeMillis();
|
||||
long duration = durationSeconds > 0 ? durationSeconds : Math.max(1, ipBlockSeconds);
|
||||
blockedIpsUntil.put(ip, now + duration * 1000L);
|
||||
blockedConnectionsTotal.incrementAndGet();
|
||||
}
|
||||
|
||||
private void blockIp(String ip, long now) {
|
||||
blockedIpsUntil.put(ip, now + Math.max(1, ipBlockSeconds) * 1000L);
|
||||
blockedConnectionsTotal.incrementAndGet();
|
||||
@@ -710,19 +768,6 @@ public class AntiBotModule implements Module, Listener {
|
||||
try { return Integer.parseInt(s == null ? "" : s.trim()); } catch (Exception ignored) { return fallback; }
|
||||
}
|
||||
|
||||
private VpnCheckResult getVpnInfo(String ip, long now) {
|
||||
VpnCacheEntry cached = vpnCache.get(ip);
|
||||
if (cached != null && cached.expiresAt > now) return cached.result;
|
||||
VpnCheckResult fresh = requestIpApi(ip);
|
||||
if (fresh != null) {
|
||||
VpnCacheEntry entry = new VpnCacheEntry();
|
||||
entry.result = fresh;
|
||||
entry.expiresAt = now + Math.max(1, vpnCacheMinutes) * 60_000L;
|
||||
vpnCache.put(ip, entry);
|
||||
}
|
||||
return fresh;
|
||||
}
|
||||
|
||||
private VpnCheckResult requestIpApi(String ip) {
|
||||
HttpURLConnection conn = null;
|
||||
try {
|
||||
@@ -837,4 +882,4 @@ public class AntiBotModule implements Module, Listener {
|
||||
sender.sendMessage(ChatColor.YELLOW + "/antibot reload");
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -31,7 +31,7 @@ import java.util.concurrent.TimeUnit;
|
||||
* Fixes:
|
||||
* - loadSchedules(): ID-Split nutzt jetzt indexOf/lastIndexOf statt split("\\.") mit length==2-Check.
|
||||
* 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(): 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;
|
||||
}
|
||||
|
||||
// FIX #1: &-Farbcodes auch in der Nachricht selbst übersetzen
|
||||
// FIX #1: &-Farbcodes auch in der Nachricht selbst \u00fcbersetzen
|
||||
String translatedMessage = ChatColor.translateAlternateColorCodes('&', message);
|
||||
String coloredMessage = (messageColorCode.isEmpty() ? "" : messageColorCode) + translatedMessage;
|
||||
|
||||
@@ -150,17 +150,17 @@ public class BroadcastModule implements Module, Listener {
|
||||
for (ProxiedPlayer p : plugin.getProxy().getPlayers()) {
|
||||
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;
|
||||
}
|
||||
|
||||
/**
|
||||
* Baut ein BaseComponent-Array aus einem formatierten String.
|
||||
* 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) {
|
||||
// Regex für URLs
|
||||
// Regex f\u00fcr URLs
|
||||
java.util.regex.Pattern urlPattern = java.util.regex.Pattern.compile(
|
||||
"(https?://[^\\s\\n]+)", java.util.regex.Pattern.CASE_INSENSITIVE);
|
||||
|
||||
|
||||
@@ -6,12 +6,12 @@ import java.util.concurrent.ConcurrentHashMap;
|
||||
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:
|
||||
* 1. Spieler tippt /linkdiscord oder /linktelegram → Token wird generiert
|
||||
* 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):
|
||||
* 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 Logger logger;
|
||||
|
||||
// UUID → verknüpfte Accounts
|
||||
// UUID → verkn\u00fcpfte Accounts
|
||||
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<>();
|
||||
|
||||
public AccountLinkManager(File dataFolder, Logger logger) {
|
||||
@@ -37,10 +37,10 @@ public class AccountLinkManager {
|
||||
public static class LinkedAccount {
|
||||
public UUID minecraftUUID;
|
||||
public String minecraftName;
|
||||
public String discordUserId = ""; // leer = nicht verknüpft
|
||||
public String telegramUserId = ""; // leer = nicht verknüpft
|
||||
public String telegramUsername = ""; // @username für Anzeige
|
||||
public String discordUsername = ""; // für Anzeige
|
||||
public String discordUserId = ""; // leer = nicht verkn\u00fcpft
|
||||
public String telegramUserId = ""; // leer = nicht verkn\u00fcpft
|
||||
public String telegramUsername = ""; // @username f\u00fcr Anzeige
|
||||
public String discordUsername = ""; // f\u00fcr Anzeige
|
||||
}
|
||||
|
||||
private static class PendingToken {
|
||||
@@ -64,8 +64,8 @@ public class AccountLinkManager {
|
||||
// ===== Token-Generierung =====
|
||||
|
||||
/**
|
||||
* Generiert einen neuen Verknüpfungs-Token für einen Spieler.
|
||||
* Bestehende Token für denselben Spieler+Typ werden überschrieben.
|
||||
* Generiert einen neuen Verkn\u00fcpfungs-Token f\u00fcr einen Spieler.
|
||||
* Bestehende Token f\u00fcr denselben Spieler+Typ werden \u00fcberschrieben.
|
||||
*
|
||||
* @param uuid UUID des Spielers
|
||||
* @param playerName Anzeigename
|
||||
@@ -73,7 +73,7 @@ public class AccountLinkManager {
|
||||
* @return 6-stelliger alphanumerischer Token (z.B. "A3F9K2")
|
||||
*/
|
||||
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 ->
|
||||
e.getValue().uuid.equals(uuid) && e.getValue().type.equals(type));
|
||||
|
||||
@@ -97,15 +97,15 @@ public class AccountLinkManager {
|
||||
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 externalId Discord User-ID oder Telegram User-ID (als String)
|
||||
* @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) {
|
||||
token = token.trim().toUpperCase();
|
||||
@@ -115,7 +115,7 @@ public class AccountLinkManager {
|
||||
pendingTokens.remove(token);
|
||||
return null;
|
||||
}
|
||||
// Typ muss übereinstimmen
|
||||
// Typ muss \u00fcbereinstimmen
|
||||
if (!pending.type.equals(type)) return null;
|
||||
|
||||
pendingTokens.remove(token);
|
||||
@@ -161,19 +161,19 @@ public class AccountLinkManager {
|
||||
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) {
|
||||
LinkedAccount a = getByDiscordId(discordUserId);
|
||||
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) {
|
||||
LinkedAccount a = getByTelegramId(telegramUserId);
|
||||
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) {
|
||||
for (PendingToken t : pendingTokens.values()) {
|
||||
if (t.uuid.equals(uuid) && t.type.equals(type) && !t.isExpired()) return true;
|
||||
@@ -181,7 +181,7 @@ public class AccountLinkManager {
|
||||
return false;
|
||||
}
|
||||
|
||||
// ===== Verknüpfung aufheben =====
|
||||
// ===== Verkn\u00fcpfung aufheben =====
|
||||
|
||||
public boolean unlinkDiscord(UUID 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.
|
||||
* Wrapper für redeemToken mit type="telegram".
|
||||
* L\u00f6st einen Telegram-Token ein.
|
||||
* Wrapper f\u00fcr redeemToken mit type="telegram".
|
||||
*/
|
||||
public LinkedAccount redeemTelegram(String token, String telegramUserId, String telegramUsername) {
|
||||
return redeemToken(token, telegramUserId, telegramUsername, "telegram");
|
||||
}
|
||||
|
||||
/**
|
||||
* Löst einen Discord-Token ein.
|
||||
* Wrapper für redeemToken mit type="discord".
|
||||
* L\u00f6st einen Discord-Token ein.
|
||||
* Wrapper f\u00fcr redeemToken mit type="discord".
|
||||
*/
|
||||
public LinkedAccount redeemDiscord(String token, String discordUserId, String discordUsername) {
|
||||
return redeemToken(token, discordUserId, discordUsername, "discord");
|
||||
}
|
||||
|
||||
/**
|
||||
* Gibt den Anzeigenamen für einen Telegram-Nutzer zurück.
|
||||
* Wenn verknüpft: "MinecraftName (@telegram)", sonst: "@telegram"
|
||||
* Gibt den Anzeigenamen f\u00fcr einen Telegram-Nutzer zur\u00fcck.
|
||||
* Wenn verkn\u00fcpft: "MinecraftName (@telegram)", sonst: "@telegram"
|
||||
*/
|
||||
public String resolveTelegramName(String telegramUserId, String fallbackName) {
|
||||
String mc = getMinecraftNameByTelegramId(telegramUserId);
|
||||
@@ -238,8 +238,8 @@ public class AccountLinkManager {
|
||||
}
|
||||
|
||||
/**
|
||||
* Gibt den Anzeigenamen für einen Discord-Nutzer zurück.
|
||||
* Wenn verknüpft: Minecraft-Name, sonst: Discord-Username
|
||||
* Gibt den Anzeigenamen f\u00fcr einen Discord-Nutzer zur\u00fcck.
|
||||
* Wenn verkn\u00fcpft: Minecraft-Name, sonst: Discord-Username
|
||||
*/
|
||||
public String resolveDiscordName(String discordUserId, String fallbackName) {
|
||||
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})
|
||||
*/
|
||||
public boolean isLinkedTelegram(String telegramUserId) {
|
||||
|
||||
@@ -35,7 +35,7 @@ public class BlockManager {
|
||||
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) {
|
||||
Set<UUID> set = blocked.get(blocker);
|
||||
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.
|
||||
*/
|
||||
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.
|
||||
* Gibt false zurück, wenn einer der beiden den anderen blockiert.
|
||||
* Pr\u00fcft ob eine Nachricht von `sender` an `receiver` zugestellt werden soll.
|
||||
* Gibt false zur\u00fcck, wenn einer der beiden den anderen blockiert.
|
||||
*/
|
||||
public boolean canReceive(UUID sender, UUID receiver) {
|
||||
// receiver hat sender blockiert → keine Nachricht
|
||||
@@ -66,7 +66,7 @@ public class BlockManager {
|
||||
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) {
|
||||
Set<UUID> set = blocked.get(blocker);
|
||||
if (set == null) return Collections.emptySet();
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
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 {
|
||||
|
||||
@@ -51,12 +51,12 @@ public class ChatChannel {
|
||||
public int getTelegramThreadId() { return telegramThreadId; }
|
||||
public boolean isUseAdminBridge() { return useAdminBridge; }
|
||||
|
||||
/** Prüft ob der Kanal eine Permission erfordert. */
|
||||
/** Pr\u00fcft ob der Kanal eine Permission erfordert. */
|
||||
public boolean hasPermission() {
|
||||
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() {
|
||||
return net.md_5.bungee.api.ChatColor.translateAlternateColorCodes('&',
|
||||
color + "[" + symbol + "]");
|
||||
|
||||
@@ -10,12 +10,12 @@ import java.nio.file.Files;
|
||||
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
|
||||
* 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
|
||||
* als Fallback ergänzen, nicht umgekehrt.
|
||||
* als Fallback erg\u00e4nzen, nicht umgekehrt.
|
||||
*/
|
||||
public class ChatConfig {
|
||||
|
||||
@@ -103,13 +103,13 @@ public class ChatConfig {
|
||||
config = new Configuration();
|
||||
}
|
||||
parseConfig();
|
||||
plugin.getLogger().fine("[ChatModule] " + channels.size() + " Kanäle geladen.");
|
||||
plugin.getLogger().fine("[ChatModule] " + channels.size() + " Kan\u00e4le geladen.");
|
||||
}
|
||||
|
||||
private void parseConfig() {
|
||||
defaultChannel = config.getString("default-channel", "global");
|
||||
|
||||
// --- Kanäle ---
|
||||
// --- Kan\u00e4le ---
|
||||
channels.clear();
|
||||
Configuration chSection = config.getSection("channels");
|
||||
if (chSection != null) {
|
||||
@@ -209,10 +209,10 @@ public class ChatConfig {
|
||||
linkingEnabled = al == null || al.getBoolean("enabled", true);
|
||||
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}";
|
||||
linkSuccessDiscord = al != null ? al.getString("success-discord", "&aDiscord verknüpft!") : "&aDiscord verknüpft!";
|
||||
linkSuccessTelegram = al != null ? al.getString("success-telegram", "&aTelegram verknüpft!") : "&aTelegram verknüpft!";
|
||||
linkBotSuccessDiscord = al != null ? al.getString("bot-success-discord", "✅ Verknüpft: {player}") : "✅ Verknüpft: {player}";
|
||||
linkBotSuccessTelegram = al != null ? al.getString("bot-success-telegram", "✅ Verknüpft: {player}") : "✅ Verknüpft: {player}";
|
||||
linkSuccessDiscord = al != null ? al.getString("success-discord", "&aDiscord verkn\u00fcpft!") : "&aDiscord verkn\u00fcpft!";
|
||||
linkSuccessTelegram = al != null ? al.getString("success-telegram", "&aTelegram verkn\u00fcpft!") : "&aTelegram verkn\u00fcpft!";
|
||||
linkBotSuccessDiscord = al != null ? al.getString("bot-success-discord", "✅ Verkn\u00fcpft: {player}") : "✅ Verkn\u00fcpft: {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}";
|
||||
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.spamMessage = spam.getString("message", "&cNicht so schnell!");
|
||||
// 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.
|
||||
filterConfig.globalRateLimitWindowMs = Math.max(500L, filterConfig.spamCooldownMs);
|
||||
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;
|
||||
pmRateLimitWindowMs = 5000L;
|
||||
pmRateLimitMaxActions = 4;
|
||||
@@ -283,7 +283,7 @@ public class ChatConfig {
|
||||
if (rl != null) {
|
||||
Configuration rlChat = rl.getSection("chat");
|
||||
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.globalRateLimitWindowMs = rlChat.getLong("window-ms", filterConfig.globalRateLimitWindowMs);
|
||||
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.
|
||||
*
|
||||
* Reihenfolge der Prüfungen in processChat():
|
||||
* Reihenfolge der Pr\u00fcfungen in processChat():
|
||||
* 1. Spam-Cooldown (zu schnell geschrieben?)
|
||||
* 2. Gleiche Nachricht wiederholt?
|
||||
* 3. Zu viele Großbuchstaben?
|
||||
* 4. Verbotene Wörter → ersetzen durch ****
|
||||
* 3. Zu viele Gro\u00dfbuchstaben?
|
||||
* 4. Verbotene W\u00f6rter → ersetzen durch ****
|
||||
* 5. Farbcodes (& Codes) → nur mit Permission erlaubt
|
||||
*/
|
||||
public class ChatFilter {
|
||||
@@ -21,10 +21,10 @@ public class ChatFilter {
|
||||
private final ChatFilterConfig cfg;
|
||||
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<>();
|
||||
|
||||
// Kompilierte Regex-Pattern für Blacklist-Wörter
|
||||
// Kompilierte Regex-Pattern f\u00fcr Blacklist-W\u00f6rter
|
||||
private final List<Pattern> blacklistPatterns = new ArrayList<>();
|
||||
|
||||
public ChatFilter(ChatFilterConfig cfg) {
|
||||
@@ -46,7 +46,7 @@ public class ChatFilter {
|
||||
public enum FilterResult {
|
||||
ALLOWED, // Nachricht darf durch
|
||||
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 {
|
||||
@@ -68,7 +68,7 @@ public class ChatFilter {
|
||||
*
|
||||
* @param uuid UUID des sendenden Spielers
|
||||
* @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 hasFormatPerm true → &l, &o etc. erlaubt
|
||||
* @return FilterResponse mit Ergebnis und ggf. modifizierter Nachricht
|
||||
@@ -160,7 +160,7 @@ public class ChatFilter {
|
||||
}
|
||||
|
||||
private String applyCapsFilter(String message) {
|
||||
// Zähle Großbuchstaben
|
||||
// Z\u00e4hle Gro\u00dfbuchstaben
|
||||
int total = 0, upper = 0;
|
||||
for (char c : message.toCharArray()) {
|
||||
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 (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; }
|
||||
// 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; }
|
||||
}
|
||||
sb.append(c);
|
||||
@@ -215,7 +215,7 @@ public class ChatFilter {
|
||||
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.
|
||||
*
|
||||
* Erkennt:
|
||||
@@ -314,15 +314,15 @@ public class ChatFilter {
|
||||
|
||||
// Caps
|
||||
public boolean capsFilterEnabled = true;
|
||||
public int capsMinLength = 6; // Mindestlänge für Caps-Check
|
||||
public int capsMaxPercent = 70; // Max. % Großbuchstaben
|
||||
public int capsMinLength = 6; // Mindestl\u00e4nge f\u00fcr Caps-Check
|
||||
public int capsMaxPercent = 70; // Max. % Gro\u00dfbuchstaben
|
||||
|
||||
// Anti-Werbung
|
||||
public boolean antiAdEnabled = true;
|
||||
public String antiAdMessage = "&cWerbung ist in diesem Chat nicht erlaubt!";
|
||||
// Domains/Substrings die NICHT geblockt werden (z.B. eigene Serveradresse)
|
||||
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(
|
||||
"net", "com", "de", "org", "gg", "io", "eu", "tv", "xyz",
|
||||
"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
|
||||
* 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).
|
||||
*/
|
||||
public class ChatLogger {
|
||||
@@ -22,8 +22,11 @@ public class ChatLogger {
|
||||
private final int retentionDays;
|
||||
private final AtomicInteger counter = new AtomicInteger(0);
|
||||
|
||||
private static final SimpleDateFormat DATE_FMT = new SimpleDateFormat("yyyy-MM-dd");
|
||||
private static final SimpleDateFormat TIME_FMT = new SimpleDateFormat("HH:mm:ss");
|
||||
// ThreadLocal: SimpleDateFormat ist NICHT thread-sicher – jeder Thread bekommt seine eigene Instanz.
|
||||
private static final ThreadLocal<SimpleDateFormat> DATE_FMT =
|
||||
ThreadLocal.withInitial(() -> new SimpleDateFormat("yyyy-MM-dd"));
|
||||
private static final ThreadLocal<SimpleDateFormat> TIME_FMT =
|
||||
ThreadLocal.withInitial(() -> new SimpleDateFormat("HH:mm:ss"));
|
||||
|
||||
public ChatLogger(File dataFolder, Logger logger, int retentionDays) {
|
||||
this.logDir = new File(dataFolder, "chatlogs");
|
||||
@@ -37,7 +40,7 @@ public class ChatLogger {
|
||||
|
||||
/**
|
||||
* 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() {
|
||||
int seq = counter.incrementAndGet();
|
||||
@@ -49,7 +52,7 @@ public class ChatLogger {
|
||||
// ===== 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 server Servername des Absenders
|
||||
@@ -58,8 +61,8 @@ public class ChatLogger {
|
||||
* @param message Nachrichtentext (Rohtext, ohne Farbcodes)
|
||||
*/
|
||||
public void log(String msgId, String server, String channel, String player, String message) {
|
||||
String date = DATE_FMT.format(new Date());
|
||||
String time = TIME_FMT.format(new Date());
|
||||
String date = DATE_FMT.get().format(new Date());
|
||||
String time = TIME_FMT.get().format(new Date());
|
||||
|
||||
// Minecraft-Farbcodes aus dem Log entfernen
|
||||
String cleanMsg = stripColor(message);
|
||||
@@ -80,7 +83,7 @@ public class ChatLogger {
|
||||
// ===== 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.
|
||||
*/
|
||||
public void cleanup() {
|
||||
@@ -92,7 +95,7 @@ public class ChatLogger {
|
||||
for (File f : files) {
|
||||
if (f.lastModified() < cutoff) {
|
||||
if (f.delete()) {
|
||||
logger.info("[ChatLogger] Altes Log gelöscht: " + f.getName());
|
||||
logger.info("[ChatLogger] Altes Log gel\u00f6scht: " + f.getName());
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -112,14 +115,14 @@ public class ChatLogger {
|
||||
|
||||
/**
|
||||
* 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 maxLines Maximale Anzahl zurückgegebener Zeilen
|
||||
* @return Liste der Logzeilen (älteste zuerst)
|
||||
* @param playerFilter Spielername (case-insensitiv) oder null f\u00fcr alle
|
||||
* @param maxLines Maximale Anzahl zur\u00fcckgegebener Zeilen
|
||||
* @return Liste der Logzeilen (\u00e4lteste zuerst)
|
||||
*/
|
||||
public List<String> readLastLines(String playerFilter, int maxLines) {
|
||||
String date = DATE_FMT.format(new Date());
|
||||
String date = DATE_FMT.get().format(new Date());
|
||||
File logFile = new File(logDir, "chatlog_" + date + ".log");
|
||||
if (!logFile.exists()) return Collections.emptyList();
|
||||
|
||||
@@ -145,7 +148,7 @@ public class ChatLogger {
|
||||
logger.warning("[ChatLogger] Fehler beim Lesen: " + e.getMessage());
|
||||
}
|
||||
|
||||
// Letzte maxLines zurückgeben
|
||||
// Letzte maxLines zur\u00fcckgeben
|
||||
if (allLines.size() <= maxLines) return allLines;
|
||||
return allLines.subList(allLines.size() - maxLines, allLines.size());
|
||||
}
|
||||
|
||||
@@ -23,25 +23,25 @@ import java.util.concurrent.TimeUnit;
|
||||
import java.util.logging.Logger;
|
||||
|
||||
/**
|
||||
* ChatModule für StatusAPI (BungeeCord)
|
||||
* ChatModule f\u00fcr StatusAPI (BungeeCord)
|
||||
*
|
||||
* Features:
|
||||
* ✅ Mehrere Kanäle mit Permissions
|
||||
* ✅ Mehrere Kan\u00e4le mit Permissions
|
||||
* ✅ Server-Erkennung im Chat-Format
|
||||
* ✅ /helpop für Spieler
|
||||
* ✅ Emoji-Unterstützung (:smile: → 😊)
|
||||
* ✅ /helpop f\u00fcr Spieler
|
||||
* ✅ Emoji-Unterst\u00fctzung (:smile: → 😊)
|
||||
* ✅ Admin-Mute / Spieler-eigener Chat-Mute
|
||||
* ✅ CMI & Plugin-kompatibel (kein Eingriff in SubServer-Befehle)
|
||||
* ✅ Privat-Nachrichten (/msg, /r)
|
||||
* ✅ Spieler-Blocking (/ignore, /unignore)
|
||||
* ✅ Discord & Telegram Integration
|
||||
* ✅ Admin-Bypass (kann nicht gemutet/geblockt werden)
|
||||
* ✅ /broadcast für Admins
|
||||
* ✅ /broadcast f\u00fcr Admins
|
||||
* ✅ Secure-Chat kompatibel (1.19+ & Bedrock/Geyser)
|
||||
* ✅ Chat-Log mit konfigurierbarer Aufbewahrung (7 / 14 Tage)
|
||||
* ✅ Nachrichten-IDs (klickbar → Zwischenablage)
|
||||
* ✅ 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)
|
||||
* ✅ Join / Leave Nachrichten (mit Vanish-Support)
|
||||
* ✅ BungeeCord-Vanish-Integration via VanishProvider
|
||||
@@ -69,7 +69,7 @@ public class ChatModule implements Module, Listener {
|
||||
// UUIDs die ihren eigenen Chat-Empfang deaktiviert haben
|
||||
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<>());
|
||||
|
||||
// HelpOp Cooldown: UUID → letzter Zeitstempel (Sekunden)
|
||||
@@ -78,13 +78,13 @@ public class ChatModule implements Module, Listener {
|
||||
// Report-Cooldown: UUID → letzter Report-Zeitstempel (Sekunden)
|
||||
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<>();
|
||||
|
||||
// UUIDs die gerade auf Plugin-Chat-Eingabe warten
|
||||
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 = ".";
|
||||
|
||||
@Override
|
||||
@@ -126,7 +126,7 @@ public class ChatModule implements Module, Listener {
|
||||
linkManager = new AccountLinkManager(plugin.getDataFolder(), logger);
|
||||
linkManager.load();
|
||||
|
||||
// Externe Brücken
|
||||
// Externe Br\u00fccken
|
||||
if (config.isDiscordEnabled() || config.isReportWebhookEnabled()) {
|
||||
discordBridge = new DiscordBridge(plugin, config);
|
||||
discordBridge.setLinkManager(linkManager);
|
||||
@@ -144,7 +144,7 @@ public class ChatModule implements Module, Listener {
|
||||
ProxyServer.getInstance().getPluginManager().registerListener(plugin, this);
|
||||
registerCommands();
|
||||
|
||||
logger.fine("[ChatModule] Aktiviert – " + config.getChannels().size() + " Kanäle geladen.");
|
||||
logger.fine("[ChatModule] Aktiviert – " + config.getChannels().size() + " Kan\u00e4le geladen.");
|
||||
}
|
||||
|
||||
@Override
|
||||
@@ -166,17 +166,17 @@ public class ChatModule implements Module, Listener {
|
||||
// CHAT-EVENTS (BungeeCord original, 1.20+)
|
||||
//
|
||||
// Das Bypass-Problem mit Paper:
|
||||
// Wenn BungeeCord eine signierte Nachricht unverändert durchlässt
|
||||
// (kein setCancelled), prüft Paper die Signatur → ungültig → Fehler.
|
||||
// Wenn BungeeCord eine signierte Nachricht unver\u00e4ndert durchl\u00e4sst
|
||||
// (kein setCancelled), pr\u00fcft Paper die Signatur → ung\u00fcltig → Fehler.
|
||||
// Wenn wir setCancelled(true) setzen und die Nachricht selbst senden,
|
||||
// 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:
|
||||
// reject-chat-unsigned: false
|
||||
// Das erlaubt unsignierte Nachrichten vom Proxy durch.
|
||||
// Alternativ: In spigot.yml → settings: bungeecord: true (bereits nötig)
|
||||
// kombiniert mit BungeeCord IP-Forwarding deaktiviert Paper die Prüfung.
|
||||
// Alternativ: In spigot.yml → settings: bungeecord: true (bereits n\u00f6tig)
|
||||
// kombiniert mit BungeeCord IP-Forwarding deaktiviert Paper die Pr\u00fcfung.
|
||||
// =========================================================
|
||||
|
||||
@EventHandler(priority = EventPriority.HIGHEST)
|
||||
@@ -215,7 +215,7 @@ public class ChatModule implements Module, Listener {
|
||||
if (channel == null) channel = config.getDefaultChannel();
|
||||
|
||||
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();
|
||||
channel = config.getDefaultChannel();
|
||||
playerChannels.put(uuid, channelId);
|
||||
@@ -281,7 +281,7 @@ public class ChatModule implements Module, Listener {
|
||||
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);
|
||||
|
||||
final ChatChannel finalChannel = channel;
|
||||
@@ -314,7 +314,7 @@ public class ChatModule implements Module, Listener {
|
||||
|
||||
if (isMentioned) {
|
||||
recipient.sendMessage(color(config.getMentionsNotifyPrefix()
|
||||
+ "&7" + finalSenderName + " &7hat dich erwähnt!"));
|
||||
+ "&7" + finalSenderName + " &7hat dich erw\u00e4hnt!"));
|
||||
sendMentionSound(recipient, config.getMentionsSound());
|
||||
}
|
||||
|
||||
@@ -353,7 +353,7 @@ public class ChatModule implements Module, Listener {
|
||||
broadcastJoinLeave(player, true);
|
||||
}
|
||||
|
||||
// ── Offene Reports für Admins anzeigen ──
|
||||
// ── Offene Reports f\u00fcr Admins anzeigen ──
|
||||
if (reportManager != null
|
||||
&& (player.hasPermission(config.getAdminNotifyPermission())
|
||||
|| player.hasPermission(config.getAdminBypassPermission()))) {
|
||||
@@ -361,7 +361,7 @@ public class ChatModule implements Module, Listener {
|
||||
if (count > 0) {
|
||||
player.sendMessage(color("&8▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬"));
|
||||
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▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬"));
|
||||
}
|
||||
}
|
||||
@@ -457,7 +457,7 @@ public class ChatModule implements Module, Listener {
|
||||
ProxyServer.getInstance().getConsole().sendMessage(color(
|
||||
isVanished ? "&8" + logMsg : "&7" + logMsg));
|
||||
|
||||
// Brücken (nur für nicht-vanished Spieler)
|
||||
// Br\u00fccken (nur f\u00fcr nicht-vanished Spieler)
|
||||
if (!isVanished) {
|
||||
String cleanMsg = ChatColor.stripColor(ChatColor.translateAlternateColorCodes('&', normalMsg));
|
||||
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; }
|
||||
ProxiedPlayer p = (ProxiedPlayer) sender;
|
||||
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()) {
|
||||
boolean hasPerm = !ch.hasPermission() || p.hasPermission(ch.getPermission());
|
||||
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);
|
||||
if (ch == null) { p.sendMessage(color("&cKanal &f" + args[0] + " &cnicht gefunden.")); return; }
|
||||
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());
|
||||
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());
|
||||
if (last != null && (now - last) < config.getHelpopCooldown()) {
|
||||
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;
|
||||
}
|
||||
helpopCooldowns.put(p.getUniqueId(), now);
|
||||
@@ -550,8 +550,8 @@ public class ChatModule implements Module, Listener {
|
||||
ProxyServer.getInstance().getPluginManager().registerCommand(plugin, helpop);
|
||||
|
||||
// /msg <spieler> <nachricht>
|
||||
// Vanish: Vanished Spieler sind für normale Spieler nicht erreichbar.
|
||||
// Admins können vanished Spieler per PM kontaktieren.
|
||||
// Vanish: Vanished Spieler sind f\u00fcr normale Spieler nicht erreichbar.
|
||||
// Admins k\u00f6nnen vanished Spieler per PM kontaktieren.
|
||||
Command msgCmd = new Command("msg", null, "tell", "w", "whisper") {
|
||||
@Override
|
||||
public void execute(CommandSender sender, String[] args) {
|
||||
@@ -562,7 +562,7 @@ public class ChatModule implements Module, Listener {
|
||||
ProxiedPlayer from = (ProxiedPlayer) sender;
|
||||
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);
|
||||
if (to == null || !to.isConnected()) {
|
||||
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]);
|
||||
if (target == null) { p.sendMessage(color("&cSpieler nicht gefunden.")); return; }
|
||||
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())) {
|
||||
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;
|
||||
}
|
||||
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);
|
||||
@@ -641,14 +641,14 @@ public class ChatModule implements Module, Listener {
|
||||
int duration = config.getDefaultMuteDuration();
|
||||
if (args.length >= 2) {
|
||||
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);
|
||||
String durationStr = duration <= 0 ? "permanent" : duration + " Minuten";
|
||||
target.sendMessage(color("&cDu wurdest für " + durationStr + " stummgeschaltet."));
|
||||
sender.sendMessage(color("&a" + target.getName() + " wurde für " + durationStr + " gemutet."));
|
||||
target.sendMessage(color("&cDu wurdest f\u00fcr " + durationStr + " stummgeschaltet."));
|
||||
sender.sendMessage(color("&a" + target.getName() + " wurde f\u00fcr " + durationStr + " gemutet."));
|
||||
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);
|
||||
@@ -746,7 +746,7 @@ public class ChatModule implements Module, Listener {
|
||||
};
|
||||
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") {
|
||||
@Override
|
||||
public void execute(CommandSender sender, String[] args) {
|
||||
@@ -769,9 +769,9 @@ public class ChatModule implements Module, Listener {
|
||||
|
||||
AccountLinkManager.LinkedAccount link = linkManager.getByUUID(tUUID);
|
||||
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())
|
||||
? "&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";
|
||||
|
||||
@@ -834,7 +834,7 @@ public class ChatModule implements Module, Listener {
|
||||
sender.sendMessage(color("&8▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬"));
|
||||
|
||||
if (history.isEmpty()) {
|
||||
sender.sendMessage(color("&7Keine Einträge gefunden."));
|
||||
sender.sendMessage(color("&7Keine Eintr\u00e4ge gefunden."));
|
||||
} else {
|
||||
for (String line : history) {
|
||||
sender.sendMessage(color("&8" + line));
|
||||
@@ -866,7 +866,7 @@ public class ChatModule implements Module, Listener {
|
||||
};
|
||||
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") {
|
||||
@Override
|
||||
public void execute(CommandSender sender, String[] args) {
|
||||
@@ -878,14 +878,14 @@ public class ChatModule implements Module, Listener {
|
||||
p.sendMessage(color("&aChatModule &l✔ &aaktiv."));
|
||||
} else {
|
||||
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."));
|
||||
}
|
||||
}
|
||||
};
|
||||
ProxyServer.getInstance().getPluginManager().registerCommand(plugin, bypassCmd);
|
||||
|
||||
// /discordlink – Discord-Account verknüpfen
|
||||
// /discordlink – Discord-Account verkn\u00fcpfen
|
||||
Command discordLinkCmd = new Command("discordlink", null, "dlink") {
|
||||
@Override
|
||||
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");
|
||||
|
||||
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("&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▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬"));
|
||||
}
|
||||
};
|
||||
ProxyServer.getInstance().getPluginManager().registerCommand(plugin, discordLinkCmd);
|
||||
|
||||
// /telegramlink – Telegram-Account verknüpfen
|
||||
// /telegramlink – Telegram-Account verkn\u00fcpfen
|
||||
Command telegramLinkCmd = new Command("telegramlink", null, "tlink") {
|
||||
@Override
|
||||
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");
|
||||
|
||||
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("&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▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬"));
|
||||
}
|
||||
};
|
||||
ProxyServer.getInstance().getPluginManager().registerCommand(plugin, telegramLinkCmd);
|
||||
|
||||
// /unlink – Verknüpfung aufheben
|
||||
// /unlink – Verkn\u00fcpfung aufheben
|
||||
Command unlinkCmd = new Command("unlink") {
|
||||
@Override
|
||||
public void execute(CommandSender sender, String[] args) {
|
||||
@@ -940,21 +940,21 @@ public class ChatModule implements Module, Listener {
|
||||
switch (args[0].toLowerCase()) {
|
||||
case "discord":
|
||||
if (linkManager.unlinkDiscord(p.getUniqueId()))
|
||||
p.sendMessage(color("&aDiscord-Verknüpfung aufgehoben."));
|
||||
p.sendMessage(color("&aDiscord-Verkn\u00fcpfung aufgehoben."));
|
||||
else
|
||||
p.sendMessage(color("&cKein Discord-Account verknüpft."));
|
||||
p.sendMessage(color("&cKein Discord-Account verkn\u00fcpft."));
|
||||
break;
|
||||
case "telegram":
|
||||
if (linkManager.unlinkTelegram(p.getUniqueId()))
|
||||
p.sendMessage(color("&aTelegram-Verknüpfung aufgehoben."));
|
||||
p.sendMessage(color("&aTelegram-Verkn\u00fcpfung aufgehoben."));
|
||||
else
|
||||
p.sendMessage(color("&cKein Telegram-Account verknüpft."));
|
||||
p.sendMessage(color("&cKein Telegram-Account verkn\u00fcpft."));
|
||||
break;
|
||||
case "all":
|
||||
boolean d = linkManager.unlinkDiscord(p.getUniqueId());
|
||||
boolean t = linkManager.unlinkTelegram(p.getUniqueId());
|
||||
if (d || t) p.sendMessage(color("&aAlle Verknüpfungen aufgehoben."));
|
||||
else p.sendMessage(color("&cKeine Verknüpfungen vorhanden."));
|
||||
if (d || t) p.sendMessage(color("&aAlle Verkn\u00fcpfungen aufgehoben."));
|
||||
else p.sendMessage(color("&cKeine Verkn\u00fcpfungen vorhanden."));
|
||||
break;
|
||||
default:
|
||||
p.sendMessage(color("&cBenutzung: /unlink <discord|telegram|all>"));
|
||||
@@ -974,7 +974,7 @@ public class ChatModule implements Module, Listener {
|
||||
|
||||
String reqPerm = config.getReportPermission();
|
||||
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) {
|
||||
@@ -986,7 +986,7 @@ public class ChatModule implements Module, Listener {
|
||||
Long last = reportCooldowns.get(p.getUniqueId());
|
||||
if (last != null && (now - last) < config.getReportCooldown()) {
|
||||
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;
|
||||
}
|
||||
|
||||
@@ -1040,7 +1040,7 @@ public class ChatModule implements Module, Listener {
|
||||
};
|
||||
ProxyServer.getInstance().getPluginManager().registerCommand(plugin, reportCmd);
|
||||
|
||||
// /reports [all] – Admin-Übersicht
|
||||
// /reports [all] – Admin-\u00dcbersicht
|
||||
Command reportsCmd = new Command("reports", config.getReportViewPermission()) {
|
||||
@Override
|
||||
public void execute(CommandSender sender, String[] args) {
|
||||
@@ -1054,7 +1054,7 @@ public class ChatModule implements Module, Listener {
|
||||
sender.sendMessage(color("&8▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬"));
|
||||
sender.sendMessage(color("&c&l⚑ REPORTS &8| &f" + list.size()
|
||||
+ (showAll ? " gesamt" : " offen")
|
||||
+ " &8| &7/reports all für alle"));
|
||||
+ " &8| &7/reports all f\u00fcr alle"));
|
||||
sender.sendMessage(color("&8▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬"));
|
||||
|
||||
if (list.isEmpty()) {
|
||||
@@ -1102,7 +1102,7 @@ public class ChatModule implements Module, Listener {
|
||||
|
||||
sender.sendMessage(color("&8▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬"));
|
||||
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.
|
||||
* Setzt den Bypass-Status für einen Spieler.
|
||||
* \u00d6ffentliche API f\u00fcr Sub-Server-Plugins oder BungeeCord-eigene Plugins.
|
||||
* Setzt den Bypass-Status f\u00fcr einen Spieler.
|
||||
*
|
||||
* Beispiel aus einem anderen BungeeCord-Plugin:
|
||||
* 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 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) {
|
||||
ProxiedPlayer target = ProxyServer.getInstance().getPlayer(name);
|
||||
@@ -1193,8 +1193,8 @@ public class ChatModule implements Module, Listener {
|
||||
String player, String suffix, String message) {
|
||||
String serverColor = config.getServerColor(server);
|
||||
String serverDisplay = config.getServerDisplay(server);
|
||||
// Nur den Servernamen-Teil vorübersetzen damit &#RRGGBB im Display-Namen
|
||||
// korrekt sitzt; der Rest wird am Ausgabepunkt via translateColors() übersetzt.
|
||||
// Nur den Servernamen-Teil vor\u00fcbersetzen damit &#RRGGBB im Display-Namen
|
||||
// korrekt sitzt; der Rest wird am Ausgabepunkt via translateColors() \u00fcbersetzt.
|
||||
String coloredServer = serverColor + serverDisplay + "&r";
|
||||
|
||||
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) {
|
||||
if (text == null) return "";
|
||||
@@ -1238,7 +1238,7 @@ public class ChatModule implements Module, Listener {
|
||||
i += 8;
|
||||
continue;
|
||||
} 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,
|
||||
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) {
|
||||
|
||||
@@ -5,8 +5,8 @@ import java.util.Map;
|
||||
/**
|
||||
* Ersetzt Emoji-Shortcuts (:smile:, :heart:, …) durch Unicode-Zeichen.
|
||||
*
|
||||
* Bedrock-Spieler (Geyser) unterstützen Unicode-Emojis ebenfalls,
|
||||
* da sie als reguläre UTF-8 Zeichen in TextComponents übertragen werden.
|
||||
* Bedrock-Spieler (Geyser) unterst\u00fctzen Unicode-Emojis ebenfalls,
|
||||
* da sie als regul\u00e4re UTF-8 Zeichen in TextComponents \u00fcbertragen werden.
|
||||
*/
|
||||
public class EmojiParser {
|
||||
|
||||
@@ -20,7 +20,7 @@ public class EmojiParser {
|
||||
|
||||
/**
|
||||
* 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
|
||||
* @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() {
|
||||
if (mappings.isEmpty()) return "&cKeine Emojis konfiguriert.";
|
||||
StringBuilder sb = new StringBuilder();
|
||||
sb.append("&eVerfügbare Emojis:\n");
|
||||
sb.append("&eVerf\u00fcgbare Emojis:\n");
|
||||
int i = 0;
|
||||
for (Map.Entry<String, String> entry : mappings.entrySet()) {
|
||||
sb.append("&7").append(entry.getKey()).append(" &f→ ").append(entry.getValue());
|
||||
|
||||
@@ -10,7 +10,7 @@ import java.util.logging.Logger;
|
||||
* Verwaltet Mutes von Spielern.
|
||||
* 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 {
|
||||
|
||||
@@ -28,7 +28,7 @@ public class MuteManager {
|
||||
// ===== Mute-Logik =====
|
||||
|
||||
/**
|
||||
* Mutet einen Spieler für durationMinutes Minuten.
|
||||
* Mutet einen Spieler f\u00fcr durationMinutes Minuten.
|
||||
* durationMinutes = 0 → permanent
|
||||
*/
|
||||
public void mute(UUID uuid, int durationMinutes) {
|
||||
@@ -45,7 +45,7 @@ public class MuteManager {
|
||||
save();
|
||||
}
|
||||
|
||||
/** Prüft ob ein Spieler aktuell gemutet ist. */
|
||||
/** Pr\u00fcft ob ein Spieler aktuell gemutet ist. */
|
||||
public boolean isMuted(UUID uuid) {
|
||||
Long expiry = mutes.get(uuid);
|
||||
if (expiry == null) return false;
|
||||
@@ -60,8 +60,8 @@ public class MuteManager {
|
||||
}
|
||||
|
||||
/**
|
||||
* Gibt die verbleibende Zeit als lesbaren String zurück.
|
||||
* Gibt "permanent" zurück bei dauerhaftem Mute.
|
||||
* Gibt die verbleibende Zeit als lesbaren String zur\u00fcck.
|
||||
* Gibt "permanent" zur\u00fcck bei dauerhaftem Mute.
|
||||
*/
|
||||
public String getRemainingTime(UUID uuid) {
|
||||
Long expiry = mutes.get(uuid);
|
||||
|
||||
@@ -18,7 +18,7 @@ public class PrivateMsgManager {
|
||||
private final BlockManager blockManager;
|
||||
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<>();
|
||||
|
||||
// UUIDs die Social-Spy aktiviert haben
|
||||
@@ -36,7 +36,7 @@ public class PrivateMsgManager {
|
||||
* @param receiver Der empfangende Spieler
|
||||
* @param message Die Nachricht
|
||||
* @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
|
||||
*/
|
||||
public boolean send(ProxiedPlayer sender, ProxiedPlayer receiver,
|
||||
@@ -84,7 +84,7 @@ public class PrivateMsgManager {
|
||||
sender.sendMessage(color(toSender));
|
||||
receiver.sendMessage(color(toReceiver));
|
||||
|
||||
// Letzte Partner speichern (für /r)
|
||||
// Letzte Partner speichern (f\u00fcr /r)
|
||||
lastPartner.put(sender.getUniqueId(), receiver.getUniqueId());
|
||||
lastPartner.put(receiver.getUniqueId(), sender.getUniqueId());
|
||||
|
||||
@@ -97,7 +97,7 @@ public class PrivateMsgManager {
|
||||
|
||||
/**
|
||||
* 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) {
|
||||
UUID partnerUuid = lastPartner.get(sender.getUniqueId());
|
||||
@@ -140,10 +140,10 @@ public class PrivateMsgManager {
|
||||
/**
|
||||
* Formatiert eine PM-Nachricht.
|
||||
* {sender} → Name des Absenders
|
||||
* {receiver} → Name des Empfängers
|
||||
* {player} → Gesprächspartner aus Sicht des jeweiligen Empfängers:
|
||||
* Beim Sender: der Empfänger (an wen schreibt er?)
|
||||
* Beim Empfänger: der Sender (von wem kommt es?)
|
||||
* {receiver} → Name des Empf\u00e4ngers
|
||||
* {player} → Gespr\u00e4chspartner aus Sicht des jeweiligen Empf\u00e4ngers:
|
||||
* Beim Sender: der Empf\u00e4nger (an wen schreibt er?)
|
||||
* Beim Empf\u00e4nger: der Sender (von wem kommt es?)
|
||||
*
|
||||
* @param viewerIsSender true wenn der aktuelle Betrachter der Absender ist
|
||||
*/
|
||||
@@ -153,7 +153,7 @@ public class PrivateMsgManager {
|
||||
return template
|
||||
.replace("{sender}", sender)
|
||||
.replace("{receiver}", receiver)
|
||||
.replace("{player}", partner) // Gesprächspartner aus Sicht des Betrachters
|
||||
.replace("{player}", partner) // Gespr\u00e4chspartner aus Sicht des Betrachters
|
||||
.replace("{message}", message);
|
||||
}
|
||||
|
||||
|
||||
@@ -11,11 +11,11 @@ import java.util.logging.Logger;
|
||||
* Verwaltet Spieler-Reports (/report).
|
||||
*
|
||||
* 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.
|
||||
* Offline-Admins erhalten eine verzögerte Benachrichtigung beim nächsten Login
|
||||
* (gesteuert von außen via getPendingNotificationFor()).
|
||||
* Offline-Admins erhalten eine verz\u00f6gerte Benachrichtigung beim n\u00e4chsten Login
|
||||
* (gesteuert von au\u00dfen via getPendingNotificationFor()).
|
||||
*
|
||||
* Speicherformat (chat_reports.dat):
|
||||
* id|reporter|reporterUUID|reported|server|messageContext|reason|timestamp|closed|closedBy
|
||||
@@ -28,10 +28,12 @@ public class ReportManager {
|
||||
/** Alle Reports (offen und geschlossen). */
|
||||
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 static final SimpleDateFormat DATE_FMT = new SimpleDateFormat("dd.MM.yyyy HH:mm:ss");
|
||||
// ThreadLocal: SimpleDateFormat ist NICHT thread-sicher.
|
||||
private static final ThreadLocal<SimpleDateFormat> DATE_FMT =
|
||||
ThreadLocal.withInitial(() -> new SimpleDateFormat("dd.MM.yyyy HH:mm:ss"));
|
||||
|
||||
// ===== Report-Datenklasse =====
|
||||
|
||||
@@ -45,10 +47,10 @@ public class ReportManager {
|
||||
public String reason;
|
||||
public long timestamp;
|
||||
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() {
|
||||
return DATE_FMT.format(new Date(timestamp));
|
||||
return DATE_FMT.get().format(new Date(timestamp));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -68,8 +70,8 @@ public class ReportManager {
|
||||
* @param reporterUUID UUID des meldenden Spielers
|
||||
* @param reportedName Name des gemeldeten Spielers
|
||||
* @param server Server, auf dem sich der Reporter befand
|
||||
* @param messageContext Letzte bekannte Nachricht des Gemeldeten (für Kontext)
|
||||
* @param reason Freitext-Begründung
|
||||
* @param messageContext Letzte bekannte Nachricht des Gemeldeten (f\u00fcr Kontext)
|
||||
* @param reason Freitext-Begr\u00fcndung
|
||||
* @return die neue Report-ID (z.B. RPT-0001)
|
||||
*/
|
||||
public String createReport(String reporterName, UUID reporterUUID,
|
||||
@@ -95,10 +97,10 @@ public class ReportManager {
|
||||
}
|
||||
|
||||
/**
|
||||
* Schließt einen Report.
|
||||
* Schlie\u00dft einen Report.
|
||||
*
|
||||
* @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
|
||||
*/
|
||||
public boolean closeReport(String id, String adminName) {
|
||||
@@ -110,13 +112,13 @@ public class ReportManager {
|
||||
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) {
|
||||
if (id == null) return null;
|
||||
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() {
|
||||
List<ChatReport> list = new ArrayList<>();
|
||||
for (ChatReport r : reports.values()) {
|
||||
@@ -126,7 +128,7 @@ public class ReportManager {
|
||||
return list;
|
||||
}
|
||||
|
||||
/** Gibt alle Reports chronologisch zurück (auch geschlossene). */
|
||||
/** Gibt alle Reports chronologisch zur\u00fcck (auch geschlossene). */
|
||||
public List<ChatReport> getAllReports() {
|
||||
List<ChatReport> list = new ArrayList<>(reports.values());
|
||||
list.sort(Comparator.comparingLong(r -> r.timestamp));
|
||||
@@ -192,7 +194,7 @@ public class ReportManager {
|
||||
r.closedBy = unesc(p[9]);
|
||||
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-")) {
|
||||
try {
|
||||
int num = Integer.parseInt(r.id.substring(4));
|
||||
@@ -208,7 +210,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) {
|
||||
if (s == null) return "";
|
||||
|
||||
@@ -11,9 +11,9 @@ import java.util.concurrent.ConcurrentHashMap;
|
||||
* Zentrale Schnittstelle zwischen dem VanishModule und dem ChatModule.
|
||||
*
|
||||
* 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
|
||||
* Privat-Nachrichten zulässt.
|
||||
* Privat-Nachrichten zul\u00e4sst.
|
||||
*
|
||||
* Verwendung im VanishModule:
|
||||
* VanishProvider.setVanished(player.getUniqueId(), true); // beim Verschwinden
|
||||
@@ -64,7 +64,7 @@ public final class VanishProvider {
|
||||
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() {
|
||||
return Collections.unmodifiableSet(vanishedPlayers);
|
||||
}
|
||||
|
||||
@@ -16,10 +16,10 @@ import java.util.concurrent.atomic.AtomicLong;
|
||||
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.
|
||||
* 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 {
|
||||
|
||||
@@ -49,7 +49,7 @@ public class DiscordBridge {
|
||||
running = true;
|
||||
int interval = Math.max(2, config.getDiscordPollInterval());
|
||||
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; }
|
||||
@@ -141,8 +141,8 @@ public class DiscordBridge {
|
||||
String token = msg.content.substring(6).trim().toUpperCase();
|
||||
if (linkManager != null) {
|
||||
AccountLinkManager.LinkedAccount acc = linkManager.redeemDiscord(token, msg.authorId, msg.authorName);
|
||||
if (acc != null) sendToChannel(channelId, "✅ Verknüpfung erfolgreich! Minecraft-Account: **" + acc.minecraftName + "**");
|
||||
else sendToChannel(channelId, "❌ Ungültiger oder abgelaufener Token. Bitte `/discordlink` im Spiel erneut ausführen.");
|
||||
if (acc != null) sendToChannel(channelId, "✅ Verkn\u00fcpfung erfolgreich! Minecraft-Account: **" + acc.minecraftName + "**");
|
||||
else sendToChannel(channelId, "❌ Ung\u00fcltiger oder abgelaufener Token. Bitte `/discordlink` im Spiel erneut ausf\u00fchren.");
|
||||
}
|
||||
continue;
|
||||
}
|
||||
@@ -158,7 +158,7 @@ public class DiscordBridge {
|
||||
() -> ProxyServer.getInstance().broadcast(new TextComponent(formatted)));
|
||||
}
|
||||
} 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
|
||||
* statt den Vorgänger-Char zu vergleichen (der bei '\\' + '"' versagt).
|
||||
* Gibt immer einen leeren String zurück wenn der Key nicht gefunden wird (nie null).
|
||||
* statt den Vorg\u00e4nger-Char zu vergleichen (der bei '\\' + '"' versagt).
|
||||
* Gibt immer einen leeren String zur\u00fcck wenn der Key nicht gefunden wird (nie null).
|
||||
*/
|
||||
private String extractJsonString(String json, String key) {
|
||||
if (json == null || key == null) return "";
|
||||
@@ -279,7 +279,7 @@ public class DiscordBridge {
|
||||
if (valStart >= json.length()) return "";
|
||||
char first = json.charAt(valStart);
|
||||
if (first == '"') {
|
||||
// FIX: Expliziter Escape-Flag statt Vorgänger-Char-Vergleich
|
||||
// FIX: Expliziter Escape-Flag statt Vorg\u00e4nger-Char-Vergleich
|
||||
int end = valStart + 1;
|
||||
boolean escaped = false;
|
||||
while (end < json.length()) {
|
||||
|
||||
@@ -17,7 +17,7 @@ import java.util.concurrent.atomic.AtomicLong;
|
||||
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)
|
||||
* Telegram → Minecraft: Via Long-Polling (getUpdates)
|
||||
@@ -25,8 +25,8 @@ import java.util.logging.Logger;
|
||||
* Voraussetzungen:
|
||||
* - Telegram Bot via @BotFather erstellen
|
||||
* - Bot-Token in chat.yml eintragen
|
||||
* - Bot in die gewünschten Gruppen/Kanäle einladen
|
||||
* - Bot zu Admin machen (für Gruppen-Nachrichten empfangen)
|
||||
* - Bot in die gew\u00fcnschten Gruppen/Kan\u00e4le einladen
|
||||
* - Bot zu Admin machen (f\u00fcr Gruppen-Nachrichten empfangen)
|
||||
*/
|
||||
public class TelegramBridge {
|
||||
|
||||
@@ -37,7 +37,7 @@ public class TelegramBridge {
|
||||
private final Logger logger;
|
||||
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 volatile boolean running = false;
|
||||
@@ -67,7 +67,7 @@ public class TelegramBridge {
|
||||
plugin.getProxy().getScheduler().schedule(plugin, this::pollUpdates,
|
||||
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() {
|
||||
@@ -78,7 +78,7 @@ public class TelegramBridge {
|
||||
|
||||
/**
|
||||
* 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) {
|
||||
sendToTelegram(chatId, 0, message);
|
||||
@@ -107,7 +107,7 @@ public class TelegramBridge {
|
||||
|
||||
/**
|
||||
* 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) {
|
||||
sendFormattedToTelegram(chatId, 0, header, content);
|
||||
@@ -167,7 +167,7 @@ public class TelegramBridge {
|
||||
if (update.text == null || update.text.isEmpty()) continue;
|
||||
if (update.isBot) continue;
|
||||
|
||||
// ── Token-Einlösung: /link <TOKEN> ──
|
||||
// ── Token-Einl\u00f6sung: /link <TOKEN> ──
|
||||
if (update.text.startsWith("/link ") || update.text.startsWith("/link@")) {
|
||||
String[] parts = update.text.split("\\s+", 2);
|
||||
if (parts.length == 2 && linkManager != null) {
|
||||
@@ -176,11 +176,11 @@ public class TelegramBridge {
|
||||
linkManager.redeemTelegram(token, update.fromId, update.fromName);
|
||||
if (acc != null) {
|
||||
sendToTelegram(update.chatId, update.threadId,
|
||||
"✅ Verknüpfung erfolgreich! Minecraft-Account: <b>"
|
||||
"✅ Verkn\u00fcpfung erfolgreich! Minecraft-Account: <b>"
|
||||
+ escapeHtml(acc.minecraftName) + "</b>");
|
||||
} else {
|
||||
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
|
||||
@@ -189,17 +189,17 @@ public class TelegramBridge {
|
||||
// Bot-Befehle ignorieren
|
||||
if (update.text.startsWith("/")) continue;
|
||||
|
||||
// ── Account-Name auflösen ──
|
||||
// ── Account-Name aufl\u00f6sen ──
|
||||
String displayName = (linkManager != null)
|
||||
? linkManager.resolveTelegramName(update.fromId, 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())
|
||||
&& (config.getTelegramAdminTopicId() == 0
|
||||
|| 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);
|
||||
|
||||
if (!matchesChannel && !isAdminChat) continue;
|
||||
@@ -257,11 +257,11 @@ public class TelegramBridge {
|
||||
private static class TelegramUpdate {
|
||||
long updateId;
|
||||
String chatId = "";
|
||||
String fromId = ""; // Telegram User-ID (für Account-Link)
|
||||
String fromId = ""; // Telegram User-ID (f\u00fcr Account-Link)
|
||||
String fromName = "";
|
||||
String text = "";
|
||||
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) {
|
||||
@@ -289,11 +289,11 @@ public class TelegramBridge {
|
||||
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) {
|
||||
for (net.viper.status.modules.chat.ChatChannel ch : config.getChannels().values()) {
|
||||
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;
|
||||
return true;
|
||||
}
|
||||
|
||||
+1
-1
@@ -23,7 +23,7 @@ import org.yaml.snakeyaml.Yaml;
|
||||
public class CommandBlockerModule implements Module, Listener {
|
||||
|
||||
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 File file;
|
||||
|
||||
+3
-3
@@ -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.plugin.Command;
|
||||
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.ConfigurationProvider;
|
||||
import net.md_5.bungee.config.YamlConfiguration;
|
||||
@@ -109,8 +109,8 @@ public class CustomCommandModule implements Module, Listener {
|
||||
|
||||
@Override
|
||||
public void onDisable(Plugin plugin) {
|
||||
// Optional: Cleanup logic, falls nötig.
|
||||
// Wir nutzen hier das übergebene 'plugin' Argument (oder this.plugin, ist egal)
|
||||
// Optional: Cleanup logic, falls n\u00f6tig.
|
||||
// Wir nutzen hier das \u00fcbergebene 'plugin' Argument (oder this.plugin, ist egal)
|
||||
// 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.
|
||||
* NexEco /eco auf dem Spigot-Server übernimmt Admin-Befehle.
|
||||
* NexEco /eco auf dem Spigot-Server \u00fcbernimmt Admin-Befehle.
|
||||
*/
|
||||
public class EcoAdminCommand extends Command {
|
||||
|
||||
|
||||
@@ -14,8 +14,8 @@ import java.util.logging.Logger;
|
||||
*
|
||||
* Fixes:
|
||||
* - balance-Spalte als DOUBLE(30,2) statt VARCHAR → kompatibel mit NexEco & SurvivalPlus
|
||||
* - atomare Transaktion für withdraw+deposit → kein Geldverlust bei Absturz
|
||||
* - FOR UPDATE Lock → kein Race-Condition-Bug bei gleichzeitigen Überweisungen
|
||||
* - atomare Transaktion f\u00fcr withdraw+deposit → kein Geldverlust bei Absturz
|
||||
* - FOR UPDATE Lock → kein Race-Condition-Bug bei gleichzeitigen \u00dcberweisungen
|
||||
*/
|
||||
public class EconomyDatabase {
|
||||
|
||||
@@ -67,7 +67,7 @@ public class EconomyDatabase {
|
||||
"MODIFY COLUMN `balance` DOUBLE(30,2) NOT NULL DEFAULT 0.00"
|
||||
);
|
||||
} 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")) {
|
||||
log.warning("[Economy] Tabellen-Setup bc_accounts: " + e.getMessage());
|
||||
}
|
||||
@@ -98,7 +98,7 @@ public class EconomyDatabase {
|
||||
|
||||
// ── 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) {
|
||||
if (!isConnected()) return -1;
|
||||
try (Connection con = dataSource.getConnection();
|
||||
@@ -109,7 +109,7 @@ public class EconomyDatabase {
|
||||
if (rs.next()) return rs.getDouble("balance");
|
||||
}
|
||||
} 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;
|
||||
}
|
||||
@@ -125,14 +125,14 @@ public class EconomyDatabase {
|
||||
ps.setDouble(2, balance);
|
||||
ps.executeUpdate();
|
||||
} 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.
|
||||
* 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) {
|
||||
if (!isConnected()) return false;
|
||||
@@ -162,7 +162,7 @@ public class EconomyDatabase {
|
||||
ps.executeUpdate();
|
||||
}
|
||||
|
||||
// Empfänger gutschreiben (Konto anlegen falls nötig)
|
||||
// Empf\u00e4nger gutschreiben (Konto anlegen falls n\u00f6tig)
|
||||
try (PreparedStatement ps = con.prepareStatement(
|
||||
"INSERT INTO `" + TABLE + "` (`player_name`, `balance`) VALUES (?, ?) " +
|
||||
"ON DUPLICATE KEY UPDATE `balance` = `balance` + ?")) {
|
||||
|
||||
@@ -9,20 +9,20 @@ import net.md_5.bungee.event.EventHandler;
|
||||
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
|
||||
* (Spigot) die über Vault/NexEco den Kontostand per HTTP an die StatusAPI sendet.
|
||||
* Das Bef\u00fcllen der Map geschieht ausschlie\u00dflich durch die StatusAPIBridge
|
||||
* (Spigot) die \u00fcber Vault/NexEco den Kontostand per HTTP an die StatusAPI sendet.
|
||||
*/
|
||||
public class EconomyListener implements Listener {
|
||||
|
||||
public EconomyListener(Plugin plugin, EconomyManager manager) {
|
||||
// EconomyManager wird nicht mehr benötigt
|
||||
// EconomyManager wird nicht mehr ben\u00f6tigt
|
||||
}
|
||||
|
||||
@EventHandler
|
||||
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
|
||||
@@ -32,6 +32,6 @@ public class EconomyListener implements Listener {
|
||||
}
|
||||
|
||||
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.
|
||||
* Economy wird ausschließlich über NexEco (Spigot) verwaltet.
|
||||
* Economy wird ausschlie\u00dflich \u00fcber NexEco (Spigot) verwaltet.
|
||||
*/
|
||||
public class EconomyManager {
|
||||
|
||||
|
||||
@@ -6,13 +6,13 @@ import net.viper.status.module.Module;
|
||||
/**
|
||||
* EconomyModule – DEAKTIVIERT.
|
||||
*
|
||||
* Die Economy wird ausschließlich über NexEco (Spigot) verwaltet.
|
||||
* Die StatusAPIBridge (Spigot-Plugin) liest den Kontostand über Vault/NexEco
|
||||
* Die Economy wird ausschlie\u00dflich \u00fcber NexEco (Spigot) verwaltet.
|
||||
* Die StatusAPIBridge (Spigot-Plugin) liest den Kontostand \u00fcber Vault/NexEco
|
||||
* und pushed ihn per HTTP an die StatusAPI → playerBalances Map.
|
||||
*
|
||||
* Damit gibt es nur EINE Datenquelle für Kontostände: NexEco / money_accounts.
|
||||
* Das alte EconomyModule schrieb in bc_accounts – das führte zu doppelten,
|
||||
* inkonsistenten Kontoständen.
|
||||
* Damit gibt es nur EINE Datenquelle f\u00fcr Kontost\u00e4nde: NexEco / money_accounts.
|
||||
* Das alte EconomyModule schrieb in bc_accounts – das f\u00fchrte zu doppelten,
|
||||
* inkonsistenten Kontost\u00e4nden.
|
||||
*/
|
||||
public class EconomyModule implements Module {
|
||||
|
||||
@@ -21,8 +21,8 @@ public class EconomyModule implements Module {
|
||||
|
||||
@Override
|
||||
public void onEnable(Plugin plugin) {
|
||||
plugin.getLogger().info("[Economy] EconomyModule ist deaktiviert – NexEco ist zuständig.");
|
||||
plugin.getLogger().info("[Economy] Kontostände kommen via StatusAPIBridge (Vault → NexEco → HTTP).");
|
||||
plugin.getLogger().info("[Economy] EconomyModule ist deaktiviert – NexEco ist zust\u00e4ndig.");
|
||||
plugin.getLogger().info("[Economy] Kontost\u00e4nde kommen via StatusAPIBridge (Vault → NexEco → HTTP).");
|
||||
}
|
||||
|
||||
@Override
|
||||
|
||||
@@ -6,8 +6,8 @@ import net.md_5.bungee.api.plugin.Plugin;
|
||||
|
||||
/**
|
||||
* /pay – wird NICHT mehr auf BungeeCord registriert.
|
||||
* NexEco auf dem Spigot-Server übernimmt /pay direkt.
|
||||
* Diese Klasse existiert nur noch für Kompilier-Kompatibilität.
|
||||
* NexEco auf dem Spigot-Server \u00fcbernimmt /pay direkt.
|
||||
* Diese Klasse existiert nur noch f\u00fcr Kompilier-Kompatibilit\u00e4t.
|
||||
*/
|
||||
public class PayCommand extends Command {
|
||||
|
||||
|
||||
@@ -39,8 +39,8 @@ import java.util.concurrent.TimeUnit;
|
||||
/**
|
||||
* ForumBridgeModule: Verbindet das WP Business Forum mit dem Minecraft-Server.
|
||||
*
|
||||
* Fix #13: extractJsonString() gibt jetzt immer einen leeren String statt null zurück.
|
||||
* Alle Aufrufer müssen nicht mehr auf null prüfen, was NullPointerExceptions verhindert.
|
||||
* Fix #13: extractJsonString() gibt jetzt immer einen leeren String statt null zur\u00fcck.
|
||||
* Alle Aufrufer m\u00fcssen nicht mehr auf null pr\u00fcfen, was NullPointerExceptions verhindert.
|
||||
*/
|
||||
public class ForumBridgeModule implements Module, Listener {
|
||||
|
||||
@@ -106,7 +106,7 @@ public class ForumBridgeModule implements Module, Listener {
|
||||
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 type = extractJsonString(body, "type");
|
||||
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 (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);
|
||||
|
||||
ProxiedPlayer online = ProxyServer.getInstance().getPlayer(uuid);
|
||||
@@ -160,7 +160,7 @@ public class ForumBridgeModule implements Module, Listener {
|
||||
TextComponent link = new TextComponent("§a ➜ Im Forum ansehen");
|
||||
link.setClickEvent(new ClickEvent(ClickEvent.Action.OPEN_URL, notif.getUrl()));
|
||||
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(new TextComponent("§8§m "));
|
||||
@@ -197,16 +197,16 @@ public class ForumBridgeModule implements Module, Listener {
|
||||
|
||||
@Override
|
||||
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;
|
||||
if (args.length != 1) {
|
||||
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;
|
||||
}
|
||||
String token = args[0].trim().toUpperCase();
|
||||
if (wpBaseUrl.isEmpty()) { p.sendMessage(new TextComponent("§cForum-Verknüpfung ist nicht konfiguriert.")); return; }
|
||||
p.sendMessage(new TextComponent("§7Überprüfe Token..."));
|
||||
if (wpBaseUrl.isEmpty()) { p.sendMessage(new TextComponent("§cForum-Verkn\u00fcpfung ist nicht konfiguriert.")); return; }
|
||||
p.sendMessage(new TextComponent("§7\u00dcberpr\u00fcfe Token..."));
|
||||
|
||||
plugin.getProxy().getScheduler().runAsync(plugin, () -> {
|
||||
try {
|
||||
@@ -236,17 +236,17 @@ public class ForumBridgeModule implements Module, Listener {
|
||||
String username = extractJsonString(resp, "username");
|
||||
String show = !displayName.isEmpty() ? displayName : username;
|
||||
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));
|
||||
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 "));
|
||||
} else {
|
||||
String error = extractJsonString(resp, "error");
|
||||
String message = extractJsonString(resp, "message");
|
||||
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 ("invalid_token".equals(error)) p.sendMessage(new TextComponent("§c✗ Ungültiger Token."));
|
||||
else p.sendMessage(new TextComponent("§c✗ Verknüpfung fehlgeschlagen: " + (!error.isEmpty() ? error : "Unbekannter Fehler")));
|
||||
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\u00fcltiger Token."));
|
||||
else p.sendMessage(new TextComponent("§c✗ Verkn\u00fcpfung fehlgeschlagen: " + (!error.isEmpty() ? error : "Unbekannter Fehler")));
|
||||
}
|
||||
} catch (Exception ex) {
|
||||
p.sendMessage(new TextComponent("§c✗ Fehler bei der Verbindung zum Forum."));
|
||||
@@ -261,16 +261,16 @@ public class ForumBridgeModule implements Module, Listener {
|
||||
|
||||
@Override
|
||||
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;
|
||||
List<ForumNotification> pending = storage.getPending(p.getUniqueId());
|
||||
|
||||
if (pending.isEmpty()) {
|
||||
p.sendMessage(new TextComponent("§7Keine neuen Forum-Benachrichtigungen."));
|
||||
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.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);
|
||||
}
|
||||
return;
|
||||
@@ -287,7 +287,7 @@ public class ForumBridgeModule implements Module, Listener {
|
||||
TextComponent detail = new TextComponent(!n.getTitle().isEmpty() ? "§f" + n.getTitle() : "§fvon " + n.getAuthor());
|
||||
if (!n.getUrl().isEmpty()) {
|
||||
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);
|
||||
p.sendMessage(line);
|
||||
@@ -305,7 +305,7 @@ public class ForumBridgeModule implements Module, Listener {
|
||||
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.
|
||||
*/
|
||||
private static String extractJsonString(String json, String key) {
|
||||
|
||||
@@ -9,7 +9,7 @@ import java.util.logging.Logger;
|
||||
/**
|
||||
* Speichert ausstehende Forum-Benachrichtigungen (Datei-basiert).
|
||||
* 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 {
|
||||
|
||||
@@ -26,7 +26,7 @@ public class ForumNotifStorage {
|
||||
}
|
||||
|
||||
/**
|
||||
* Fügt eine Benachrichtigung hinzu.
|
||||
* F\u00fcgt eine Benachrichtigung hinzu.
|
||||
*/
|
||||
public void add(ForumNotification notification) {
|
||||
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) {
|
||||
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) {
|
||||
long cutoff = System.currentTimeMillis() - ((long) maxDays * 24 * 60 * 60 * 1000);
|
||||
|
||||
@@ -25,7 +25,7 @@ public class ForumNotification {
|
||||
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) {
|
||||
this.playerUuid = playerUuid;
|
||||
this.type = type;
|
||||
@@ -48,12 +48,12 @@ public class ForumNotification {
|
||||
public void setDelivered(boolean d) { this.delivered = d; }
|
||||
|
||||
/**
|
||||
* Deutsches Label für den Benachrichtigungstyp.
|
||||
* Deutsches Label f\u00fcr den Benachrichtigungstyp.
|
||||
*/
|
||||
public String getTypeLabel() {
|
||||
switch (type) {
|
||||
case "reply": return "Neue Antwort";
|
||||
case "mention": return "Erwähnung";
|
||||
case "mention": return "Erw\u00e4hnung";
|
||||
case "message": return "Neue PN";
|
||||
case "thread": return "Neuer Thread";
|
||||
case "poll": return "Neue Umfrage";
|
||||
@@ -70,15 +70,15 @@ public class ForumNotification {
|
||||
case "reply": return "§b"; // Aqua
|
||||
case "mention": return "§e"; // Gelb
|
||||
case "message": return "§d"; // Rosa
|
||||
case "thread": return "§a"; // Grün
|
||||
case "thread": return "§a"; // Gr\u00fcn
|
||||
case "poll": return "§3"; // Dunkel-Aqua
|
||||
case "answer": return "§2"; // Dunkel-Grün
|
||||
default: return "§f"; // Weiß
|
||||
case "answer": return "§2"; // Dunkel-Gr\u00fcn
|
||||
default: return "§f"; // Wei\u00df
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Serialisierung für Datei-Speicherung.
|
||||
* Serialisierung f\u00fcr Datei-Speicherung.
|
||||
* Format: uuid|type|title|author|url|timestamp|delivered
|
||||
*/
|
||||
public String toLine() {
|
||||
|
||||
@@ -0,0 +1,252 @@
|
||||
package net.viper.status.modules.help;
|
||||
|
||||
import net.md_5.bungee.api.ChatColor;
|
||||
import net.md_5.bungee.api.CommandSender;
|
||||
import net.md_5.bungee.api.ProxyServer;
|
||||
import net.md_5.bungee.api.chat.ClickEvent;
|
||||
import net.md_5.bungee.api.chat.HoverEvent;
|
||||
import net.md_5.bungee.api.chat.TextComponent;
|
||||
import net.md_5.bungee.api.chat.ComponentBuilder;
|
||||
import net.md_5.bungee.api.connection.ProxiedPlayer;
|
||||
import net.md_5.bungee.api.plugin.Command;
|
||||
import net.md_5.bungee.api.plugin.Plugin;
|
||||
import net.viper.status.StatusAPI;
|
||||
import net.viper.status.module.Module;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
import java.util.Properties;
|
||||
|
||||
/**
|
||||
* HelpModule – seitenbasierte Ingame-Hilfe.
|
||||
*
|
||||
* verify.properties:
|
||||
* statusapi.help=vn → /vn help [seite]
|
||||
* statusapi.help.permission=statusapi.admin
|
||||
*/
|
||||
public class HelpModule implements Module {
|
||||
|
||||
private String commandName = "help";
|
||||
private String adminPermission = "statusapi.admin";
|
||||
|
||||
@Override
|
||||
public String getName() { return "HelpModule"; }
|
||||
|
||||
@Override
|
||||
public void onEnable(Plugin plugin) {
|
||||
Properties props = ((StatusAPI) plugin).getVerifyProperties();
|
||||
if (props != null) {
|
||||
String cn = props.getProperty("statusapi.help", "help").trim();
|
||||
if (!cn.isEmpty()) commandName = cn;
|
||||
String ap = props.getProperty("statusapi.help.permission", "statusapi.admin").trim();
|
||||
if (!ap.isEmpty()) adminPermission = ap;
|
||||
}
|
||||
ProxyServer.getInstance().getPluginManager().registerCommand(plugin,
|
||||
new HelpCommand(commandName, adminPermission));
|
||||
plugin.getLogger().info("[HelpModule] /" + commandName + " help registriert (Admin-Permission: " + adminPermission + ")");
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onDisable(Plugin plugin) {}
|
||||
|
||||
// ─────────────────────────────────────────────────────────────────────────
|
||||
|
||||
private static class HelpCommand extends Command {
|
||||
|
||||
private final String adminPerm;
|
||||
|
||||
// Jede Seite ist eine Liste von Zeilen
|
||||
// Seiten werden zur Laufzeit je nach Berechtigung zusammengebaut
|
||||
HelpCommand(String name, String adminPerm) {
|
||||
super(name, null);
|
||||
this.adminPerm = adminPerm;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void execute(CommandSender sender, String[] args) {
|
||||
if (args.length == 0) {
|
||||
send(sender, "&7Nutze &e/" + getName() + " help &7f\u00fcr eine Befehls\u00fcbersicht.");
|
||||
return;
|
||||
}
|
||||
if (!args[0].equalsIgnoreCase("help")) {
|
||||
send(sender, "&cUnbekannter Unterbefehl. Nutze &e/" + getName() + " help&c.");
|
||||
return;
|
||||
}
|
||||
|
||||
boolean isAdmin = !(sender instanceof ProxiedPlayer)
|
||||
|| sender.hasPermission(adminPerm)
|
||||
|| sender.hasPermission("statusapi.admin");
|
||||
|
||||
// Seiten aufbauen
|
||||
List<List<String>> pages = buildPages(isAdmin);
|
||||
|
||||
int totalPages = pages.size();
|
||||
int page = 1;
|
||||
|
||||
if (args.length >= 2) {
|
||||
try {
|
||||
page = Integer.parseInt(args[1].trim());
|
||||
} catch (NumberFormatException e) {
|
||||
send(sender, "&cUng\u00fcltige Seitenzahl. Nutze &e/" + getName() + " help <1-" + totalPages + ">&c.");
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
if (page < 1 || page > totalPages) {
|
||||
send(sender, "&cSeite &e" + page + " &cexistiert nicht. Verf\u00fcgbar: &e1&c-&e" + totalPages + "&c.");
|
||||
return;
|
||||
}
|
||||
|
||||
// Header
|
||||
send(sender, "");
|
||||
send(sender, "&8&m━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━");
|
||||
send(sender, " &6&lStatusAPI &8| &7Hilfe &8– &7Seite &e" + page + "&8/&e" + totalPages);
|
||||
send(sender, "&8&m━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━");
|
||||
send(sender, "");
|
||||
|
||||
// Seiteninhalte
|
||||
for (String line : pages.get(page - 1)) {
|
||||
send(sender, line);
|
||||
}
|
||||
|
||||
send(sender, "");
|
||||
|
||||
// Navigation
|
||||
sendNavigation(sender, getName(), page, totalPages);
|
||||
send(sender, "");
|
||||
}
|
||||
|
||||
/** Baut alle Seiten zusammen. Admins bekommen zus\u00e4tzliche Seiten. */
|
||||
private List<List<String>> buildPages(boolean isAdmin) {
|
||||
List<List<String>> pages = new ArrayList<>();
|
||||
|
||||
// ── Seite 1: Allgemein & Chat ─────────────────────────────────────
|
||||
List<String> p1 = new ArrayList<>();
|
||||
p1.add(" &e&lAllgemein");
|
||||
p1.add(" &a/verify <token> &8– &7Account verifizieren");
|
||||
p1.add(" &a/forumlink &8(&7/fl&8) &8– &7Forum-Account verkn\u00fcpfen");
|
||||
p1.add(" &a/forum &8– &7Forum-Benachrichtigungen");
|
||||
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("");
|
||||
p1.add(" &e&lChat");
|
||||
p1.add(" &a/msg <Spieler> <Text> &8(&7/w, /tell&8) &8– &7Private Nachricht");
|
||||
p1.add(" &a/r <Text> &8(&7/reply, /antwort&8) &8– &7Auf PN antworten");
|
||||
p1.add(" &a/ignore <Spieler> &8(&7/block&8) &8– &7Spieler ignorieren");
|
||||
p1.add(" &a/unignore <Spieler> &8(&7/unblock&8) &8– &7Ignorierung aufheben");
|
||||
p1.add(" &a/channel [kanal] &8(&7/ch, /kanal&8) &8– &7Kanal wechseln");
|
||||
p1.add(" &a/chataus &8(&7/togglechat&8) &8– &7Chat-Empfang umschalten");
|
||||
pages.add(p1);
|
||||
|
||||
// ── Seite 2: Chat (weiter) & Account-Verkn\u00fcpfungen ───────────────
|
||||
List<String> p2 = new ArrayList<>();
|
||||
p2.add(" &e&lChat (Fortsetzung)");
|
||||
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/helpop <Nachricht> &8– &7Team um Hilfe bitten");
|
||||
p2.add(" &a/report <Spieler> <Grund> &8– &7Spieler melden");
|
||||
p2.add(" &a/chatbypass &8(&7/cbp&8) &8– &7ChatModule \u00fcberspringen");
|
||||
p2.add("");
|
||||
p2.add(" &e&lAccount-Verkn\u00fcpfungen");
|
||||
p2.add(" &a/discordlink &8(&7/dlink&8) &8– &7Discord verkn\u00fcpfen");
|
||||
p2.add(" &a/telegramlink &8(&7/tlink&8) &8– &7Telegram verkn\u00fcpfen");
|
||||
p2.add(" &a/unlink <discord|telegram|all> &8– &7Verkn\u00fcpfung aufheben");
|
||||
pages.add(p2);
|
||||
|
||||
// ── Admin-Seiten nur f\u00fcr Berechtigte ──────────────────────────────
|
||||
if (isAdmin) {
|
||||
// ── Seite 3: StatusAPI, AntiBot, Vanish ───────────────────────
|
||||
List<String> p3 = new ArrayList<>();
|
||||
p3.add(" &c&lAdmin &8– &eStatusAPI & AntiBot");
|
||||
p3.add(" &c/statusapi reload &8(&7/sapi reload&8) &8– &7Scoreboard & Tablist neu laden");
|
||||
p3.add(" &c/netinfo &8– &7Proxy- & Systeminfos");
|
||||
p3.add("");
|
||||
p3.add(" &c/antibot status &8– &7AntiBot-Status anzeigen");
|
||||
p3.add(" &c/antibot clearblocks &8– &7IP-Blockliste leeren");
|
||||
p3.add(" &c/antibot unblock <IP> &8– &7IP entsperren");
|
||||
p3.add(" &c/antibot profile &8– &7Schutzprofil wechseln");
|
||||
p3.add(" &c/antibot reload &8– &7AntiBot neu laden");
|
||||
p3.add("");
|
||||
p3.add(" &c&lAdmin &8– &eVanish");
|
||||
p3.add(" &c/vanish [Spieler] &8(&7/v&8) &8– &7Unsichtbar schalten");
|
||||
p3.add(" &c/vanishlist &8(&7/vlist&8) &8– &7Unsichtbare Spieler anzeigen");
|
||||
pages.add(p3);
|
||||
|
||||
// ── Seite 4: Chat-Admin, Reports, sonstige ────────────────────
|
||||
List<String> p4 = new ArrayList<>();
|
||||
p4.add(" &c&lAdmin &8– &eChat-Administration");
|
||||
p4.add(" &c/broadcast <Text> &8(&7/bc, /alert&8) &8– &7Broadcast an alle");
|
||||
p4.add(" &c/chatmute <Spieler> [Min.] &8(&7/gmute&8) &8– &7Spieler muten");
|
||||
p4.add(" &c/chatunmute <Spieler> &8(&7/gunmute&8) &8– &7Mute aufheben");
|
||||
p4.add(" &c/socialspy &8(&7/spy&8) &8– &7Private Nachrichten mitlesen");
|
||||
p4.add(" &c/chatinfo <Spieler> &8– &7Chat-Info eines Spielers");
|
||||
p4.add(" &c/chathist [Spieler] [n] &8– &7Chat-Verlauf anzeigen");
|
||||
p4.add(" &c/chatreload &8– &7Chat-Konfiguration neu laden");
|
||||
p4.add("");
|
||||
p4.add(" &c&lAdmin &8– &eReports, Tools");
|
||||
p4.add(" &c/reports [all] &8– &7Offene Reports anzeigen");
|
||||
p4.add(" &c/reportclose <ID> &8– &7Report schlie\u00dfen");
|
||||
p4.add(" &c/automessage reload &8– &7AutoMessage neu laden");
|
||||
p4.add(" &c/bcmds reload &8– &7Custom-Commands neu laden");
|
||||
p4.add(" &c/cb <Befehl> &8– &7Command-Blocker verwalten");
|
||||
p4.add(" &c/scoreboard admin|player &8– &7Admin/Spieler-Ansicht wechseln");
|
||||
pages.add(p4);
|
||||
}
|
||||
|
||||
return pages;
|
||||
}
|
||||
|
||||
/** Sendet eine klickbare Navigationszeile mit ◀ Seite X/Y ▶ */
|
||||
private void sendNavigation(CommandSender sender, String cmd, int page, int total) {
|
||||
// F\u00fcr Konsole: einfacher Text
|
||||
if (!(sender instanceof ProxiedPlayer)) {
|
||||
String nav = " ";
|
||||
if (page > 1) nav += "&7[&e◀&7] ";
|
||||
nav += "&8Seite &e" + page + "&8/&e" + total;
|
||||
if (page < total) nav += " &7[&e▶&7]";
|
||||
send(sender, nav);
|
||||
return;
|
||||
}
|
||||
|
||||
// F\u00fcr Spieler: klickbare Buttons
|
||||
TextComponent line = new TextComponent(" ");
|
||||
|
||||
// ◀ zur\u00fcck
|
||||
if (page > 1) {
|
||||
TextComponent prev = new TextComponent(
|
||||
ChatColor.translateAlternateColorCodes('&', "&7[&e◀&7] "));
|
||||
prev.setClickEvent(new ClickEvent(ClickEvent.Action.RUN_COMMAND,
|
||||
"/" + cmd + " help " + (page - 1)));
|
||||
prev.setHoverEvent(new HoverEvent(HoverEvent.Action.SHOW_TEXT,
|
||||
new ComponentBuilder(ChatColor.translateAlternateColorCodes('&',
|
||||
"&7Seite &e" + (page - 1) + " &7anzeigen")).create()));
|
||||
line.addExtra(prev);
|
||||
}
|
||||
|
||||
// Seitenanzeige
|
||||
TextComponent mid = new TextComponent(
|
||||
ChatColor.translateAlternateColorCodes('&',
|
||||
"&8Seite &e" + page + "&8/&e" + total));
|
||||
line.addExtra(mid);
|
||||
|
||||
// ▶ vor
|
||||
if (page < total) {
|
||||
TextComponent next = new TextComponent(
|
||||
ChatColor.translateAlternateColorCodes('&', " &7[&e▶&7]"));
|
||||
next.setClickEvent(new ClickEvent(ClickEvent.Action.RUN_COMMAND,
|
||||
"/" + cmd + " help " + (page + 1)));
|
||||
next.setHoverEvent(new HoverEvent(HoverEvent.Action.SHOW_TEXT,
|
||||
new ComponentBuilder(ChatColor.translateAlternateColorCodes('&',
|
||||
"&7Seite &e" + (page + 1) + " &7anzeigen")).create()));
|
||||
line.addExtra(next);
|
||||
}
|
||||
|
||||
sender.sendMessage(line);
|
||||
}
|
||||
|
||||
private static void send(CommandSender s, String text) {
|
||||
s.sendMessage(new TextComponent(
|
||||
ChatColor.translateAlternateColorCodes('&', text)));
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,445 @@
|
||||
package net.viper.status.modules.network;
|
||||
|
||||
import net.md_5.bungee.api.ChatColor;
|
||||
import net.md_5.bungee.api.ProxyServer;
|
||||
import net.md_5.bungee.api.chat.TextComponent;
|
||||
import net.md_5.bungee.api.connection.ProxiedPlayer;
|
||||
import net.md_5.bungee.api.event.PostLoginEvent;
|
||||
import net.md_5.bungee.api.plugin.Listener;
|
||||
import net.md_5.bungee.api.plugin.Plugin;
|
||||
import net.md_5.bungee.event.EventHandler;
|
||||
import net.md_5.bungee.event.EventPriority;
|
||||
import net.luckperms.api.LuckPermsProvider;
|
||||
import net.luckperms.api.model.user.User;
|
||||
import net.luckperms.api.node.Node;
|
||||
import net.luckperms.api.node.types.PermissionNode;
|
||||
import net.viper.status.StatusAPI;
|
||||
import net.viper.status.module.Module;
|
||||
import net.viper.status.modules.antibot.AntiBotModule;
|
||||
|
||||
import java.io.*;
|
||||
import java.net.*;
|
||||
import java.nio.charset.StandardCharsets;
|
||||
import java.nio.file.*;
|
||||
import java.time.*;
|
||||
import java.time.format.DateTimeFormatter;
|
||||
import java.util.*;
|
||||
import java.util.concurrent.ConcurrentHashMap;
|
||||
import java.util.logging.Logger;
|
||||
|
||||
/**
|
||||
* MultiAccountGuard
|
||||
*
|
||||
* Features:
|
||||
* - IP-Check: blockiert zweiten Account von gleicher IP
|
||||
* - Bypass NUR \u00fcber LuckPerms (OP z\u00e4hlt nicht)
|
||||
* - Persistentes Log in multiaccountguard.log
|
||||
* - Staff-Benachrichtigung ingame (Permission: statusapi.staff.notify)
|
||||
* - Tempor\u00e4rer IP-Bann nach X Versuchen (Integration mit AntiBotModule)
|
||||
* - Discord-Webhook bei Konflikt
|
||||
*/
|
||||
public class MultiAccountGuard implements Module, Listener {
|
||||
|
||||
private static final String CONFIG_FILE = "network-guard.properties";
|
||||
private static final String LOG_FILE = "multiaccountguard.log";
|
||||
public static final String BYPASS_PERM = "statusapi.multiaccountguard.bypass";
|
||||
public static final String STAFF_PERM = "statusapi.staff.notify";
|
||||
|
||||
private static final DateTimeFormatter LOG_FMT =
|
||||
DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss").withZone(ZoneId.systemDefault());
|
||||
|
||||
private Plugin plugin;
|
||||
private Logger log;
|
||||
private File logFile;
|
||||
|
||||
// Config
|
||||
private boolean enabled = true;
|
||||
private boolean checkIp = true;
|
||||
private boolean kickExisting = false;
|
||||
private String kickMessage = "&cDu bist bereits mit einem anderen Account online!\n&7Bitte trenne deinen anderen Account zuerst.";
|
||||
|
||||
// Staff-Benachrichtigung
|
||||
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}";
|
||||
|
||||
// Tempor\u00e4rer IP-Bann
|
||||
private boolean tempBanEnabled = true;
|
||||
private int tempBanMaxAttempts = 3;
|
||||
private int tempBanDurationSecs = 300;
|
||||
/** IP → Anzahl Konflikte seit letztem Reset */
|
||||
private final Map<String, Integer> attemptsByIp = new ConcurrentHashMap<>();
|
||||
|
||||
// Webhook
|
||||
private boolean webhookEnabled = true;
|
||||
private String webhookUrl = "";
|
||||
private String webhookUsername = "StatusAPI";
|
||||
private String webhookThumbnailUrl = "";
|
||||
|
||||
@Override public String getName() { return "MultiAccountGuard"; }
|
||||
|
||||
// -------------------------------------------------------------------------
|
||||
// Enable / Disable
|
||||
// -------------------------------------------------------------------------
|
||||
|
||||
@Override
|
||||
public void onEnable(Plugin plugin) {
|
||||
this.plugin = plugin;
|
||||
this.log = plugin.getLogger();
|
||||
this.logFile = new File(plugin.getDataFolder(), LOG_FILE);
|
||||
|
||||
loadConfig();
|
||||
|
||||
if (!enabled) {
|
||||
log.info("[MultiAccountGuard] Deaktiviert.");
|
||||
return;
|
||||
}
|
||||
|
||||
ProxyServer.getInstance().getPluginManager().registerListener(plugin, this);
|
||||
|
||||
log.info("[MultiAccountGuard] Aktiv | IP-Check=" + checkIp
|
||||
+ " | kickExisting=" + kickExisting
|
||||
+ " | staffNotify=" + staffNotifyEnabled
|
||||
+ " | tempBan=" + tempBanEnabled + "(max=" + tempBanMaxAttempts + ", " + tempBanDurationSecs + "s)"
|
||||
+ " | Webhook=" + (webhookEnabled && !webhookUrl.isEmpty()));
|
||||
log.info("[MultiAccountGuard] Bypass NUR via LuckPerms: /lp user <Name> permission set " + BYPASS_PERM + " true");
|
||||
log.info("[MultiAccountGuard] Log-Datei: " + logFile.getAbsolutePath());
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onDisable(Plugin plugin) {}
|
||||
|
||||
// -------------------------------------------------------------------------
|
||||
// Event
|
||||
// -------------------------------------------------------------------------
|
||||
|
||||
@EventHandler(priority = EventPriority.HIGHEST)
|
||||
public void onPostLogin(PostLoginEvent event) {
|
||||
if (!enabled) return;
|
||||
|
||||
ProxiedPlayer joining = event.getPlayer();
|
||||
|
||||
if (hasBypass(joining)) {
|
||||
log.info("[MultiAccountGuard] " + joining.getName() + " hat Bypass (LuckPerms) – \u00fcbersprungen.");
|
||||
return;
|
||||
}
|
||||
|
||||
UUID joiningUuid = joining.getUniqueId();
|
||||
String joiningIp = extractIp(joining.getSocketAddress());
|
||||
|
||||
if (joiningIp == null) {
|
||||
log.warning("[MultiAccountGuard] Konnte IP von " + joining.getName() + " nicht lesen – \u00fcbersprungen.");
|
||||
return;
|
||||
}
|
||||
|
||||
log.info("[MultiAccountGuard] Login-Check: " + joining.getName()
|
||||
+ " | UUID=" + joiningUuid + " | IP=" + joiningIp);
|
||||
|
||||
// Alle anderen Spieler (sich selbst per UUID ausschlie\u00dfen)
|
||||
List<ProxiedPlayer> others = new ArrayList<>();
|
||||
for (ProxiedPlayer p : ProxyServer.getInstance().getPlayers()) {
|
||||
if (p.getUniqueId().equals(joiningUuid)) continue;
|
||||
others.add(p);
|
||||
}
|
||||
|
||||
for (ProxiedPlayer online : others) {
|
||||
if (hasBypass(online)) continue;
|
||||
|
||||
String onlineIp = extractIp(online.getSocketAddress());
|
||||
if (onlineIp == null) continue;
|
||||
|
||||
if (checkIp && joiningIp.equals(onlineIp)) {
|
||||
log.warning("[MultiAccountGuard] KONFLIKT: "
|
||||
+ joining.getName() + " (" + joiningUuid + ")"
|
||||
+ " <-> " + online.getName() + " (" + online.getUniqueId() + ")"
|
||||
+ " IP=" + joiningIp);
|
||||
handleConflict(joining, online, joiningIp);
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
log.info("[MultiAccountGuard] " + joining.getName() + " – kein Konflikt.");
|
||||
}
|
||||
|
||||
// -------------------------------------------------------------------------
|
||||
// Konflikt behandeln
|
||||
// -------------------------------------------------------------------------
|
||||
|
||||
private void handleConflict(ProxiedPlayer joining, ProxiedPlayer existing, String ip) {
|
||||
|
||||
TextComponent msg = new TextComponent(
|
||||
ChatColor.translateAlternateColorCodes('&', kickMessage));
|
||||
|
||||
final String blockedName, allowedName;
|
||||
final UUID blockedUuid, allowedUuid;
|
||||
|
||||
if (kickExisting) {
|
||||
existing.disconnect(msg);
|
||||
blockedName = existing.getName(); blockedUuid = existing.getUniqueId();
|
||||
allowedName = joining.getName(); allowedUuid = joining.getUniqueId();
|
||||
log.warning("[MultiAccountGuard] Bestehender Account " + existing.getName() + " getrennt.");
|
||||
} else {
|
||||
joining.disconnect(msg);
|
||||
blockedName = joining.getName(); blockedUuid = joining.getUniqueId();
|
||||
allowedName = existing.getName(); allowedUuid = existing.getUniqueId();
|
||||
log.warning("[MultiAccountGuard] Neuer Account " + joining.getName() + " blockiert.");
|
||||
}
|
||||
|
||||
// 1. Persistentes Log
|
||||
writeLog(blockedName, blockedUuid, allowedName, allowedUuid, ip);
|
||||
|
||||
// 2. Staff-Benachrichtigung
|
||||
if (staffNotifyEnabled) {
|
||||
notifyStaff(blockedName, allowedName, ip);
|
||||
}
|
||||
|
||||
// 3. Tempor\u00e4rer IP-Bann
|
||||
if (tempBanEnabled) {
|
||||
int attempts = attemptsByIp.merge(ip, 1, Integer::sum);
|
||||
log.info("[MultiAccountGuard] IP " + ip + " hat " + attempts + "/" + tempBanMaxAttempts + " Versuche.");
|
||||
if (attempts >= tempBanMaxAttempts) {
|
||||
attemptsByIp.remove(ip);
|
||||
banIp(ip);
|
||||
}
|
||||
}
|
||||
|
||||
// 4. Discord-Webhook (async)
|
||||
if (webhookEnabled && webhookUrl != null && !webhookUrl.isEmpty()) {
|
||||
final String bn = blockedName, an = allowedName;
|
||||
final UUID bu = blockedUuid, au = allowedUuid;
|
||||
final int att = attemptsByIp.getOrDefault(ip, tempBanMaxAttempts);
|
||||
ProxyServer.getInstance().getScheduler().runAsync(plugin,
|
||||
() -> sendWebhook(bn, bu, an, au, ip, att));
|
||||
}
|
||||
}
|
||||
|
||||
// -------------------------------------------------------------------------
|
||||
// 1. Persistentes Log
|
||||
// -------------------------------------------------------------------------
|
||||
|
||||
private void writeLog(String blockedName, UUID blockedUuid,
|
||||
String allowedName, UUID allowedUuid, String ip) {
|
||||
try {
|
||||
if (!logFile.getParentFile().exists()) logFile.getParentFile().mkdirs();
|
||||
|
||||
String line = String.format("[%s] KONFLIKT | Geblockt: %s (%s) | Online: %s (%s) | IP: %s%n",
|
||||
LOG_FMT.format(Instant.now()),
|
||||
blockedName, blockedUuid,
|
||||
allowedName, allowedUuid,
|
||||
ip);
|
||||
|
||||
Files.write(logFile.toPath(), line.getBytes(StandardCharsets.UTF_8),
|
||||
StandardOpenOption.CREATE, StandardOpenOption.APPEND);
|
||||
|
||||
} catch (Exception e) {
|
||||
log.warning("[MultiAccountGuard] Log-Fehler: " + e.getMessage());
|
||||
}
|
||||
}
|
||||
|
||||
// -------------------------------------------------------------------------
|
||||
// 2. Staff-Benachrichtigung
|
||||
// -------------------------------------------------------------------------
|
||||
|
||||
private void notifyStaff(String blockedName, String existingName, String ip) {
|
||||
String raw = staffNotifyFormat
|
||||
.replace("{blocked}", blockedName)
|
||||
.replace("{existing}", existingName)
|
||||
.replace("{ip}", ip);
|
||||
String formatted = ChatColor.translateAlternateColorCodes('&', raw);
|
||||
TextComponent msg = new TextComponent(formatted);
|
||||
|
||||
int notified = 0;
|
||||
for (ProxiedPlayer p : ProxyServer.getInstance().getPlayers()) {
|
||||
if (p.hasPermission(STAFF_PERM)) {
|
||||
p.sendMessage(msg);
|
||||
notified++;
|
||||
}
|
||||
}
|
||||
log.info("[MultiAccountGuard] Staff-Benachrichtigung gesendet an " + notified + " Spieler.");
|
||||
}
|
||||
|
||||
// -------------------------------------------------------------------------
|
||||
// 3. Tempor\u00e4rer IP-Bann via AntiBotModule
|
||||
// -------------------------------------------------------------------------
|
||||
|
||||
private void banIp(String ip) {
|
||||
try {
|
||||
StatusAPI statusApi = (StatusAPI) ProxyServer.getInstance()
|
||||
.getPluginManager().getPlugin("StatusAPI");
|
||||
if (statusApi == null) {
|
||||
log.warning("[MultiAccountGuard] StatusAPI nicht gefunden – IP-Bann nicht m\u00f6glich.");
|
||||
return;
|
||||
}
|
||||
AntiBotModule antiBot = statusApi.getModuleManager().getModule(AntiBotModule.class);
|
||||
if (antiBot == null) {
|
||||
log.warning("[MultiAccountGuard] AntiBotModule nicht gefunden – IP-Bann nicht m\u00f6glich.");
|
||||
return;
|
||||
}
|
||||
antiBot.blockIpExternal(ip, tempBanDurationSecs);
|
||||
log.warning("[MultiAccountGuard] IP " + ip + " f\u00fcr " + tempBanDurationSecs + "s gebannt (zu viele Multi-Account-Versuche).");
|
||||
|
||||
// Staff \u00fcber den Bann informieren
|
||||
String banMsg = ChatColor.translateAlternateColorCodes('&',
|
||||
"&8[&cMAG&8] &7IP &f" + ip + " &7wurde f\u00fcr &c" + tempBanDurationSecs + "s &7gebannt &8(zu viele Versuche).");
|
||||
for (ProxiedPlayer p : ProxyServer.getInstance().getPlayers()) {
|
||||
if (p.hasPermission(STAFF_PERM)) {
|
||||
p.sendMessage(new TextComponent(banMsg));
|
||||
}
|
||||
}
|
||||
|
||||
// In Log schreiben
|
||||
try {
|
||||
String line = String.format("[%s] IP-BANN | IP: %s | Dauer: %ds%n",
|
||||
LOG_FMT.format(Instant.now()), ip, tempBanDurationSecs);
|
||||
Files.write(logFile.toPath(), line.getBytes(StandardCharsets.UTF_8),
|
||||
StandardOpenOption.CREATE, StandardOpenOption.APPEND);
|
||||
} catch (Exception ignored) {}
|
||||
|
||||
} catch (Exception e) {
|
||||
log.warning("[MultiAccountGuard] IP-Bann Fehler: " + e.getMessage());
|
||||
}
|
||||
}
|
||||
|
||||
// -------------------------------------------------------------------------
|
||||
// 4. Discord-Webhook
|
||||
// -------------------------------------------------------------------------
|
||||
|
||||
private void sendWebhook(String blockedName, UUID blockedUuid,
|
||||
String allowedName, UUID allowedUuid,
|
||||
String ip, int attempts) {
|
||||
StringBuilder fields = new StringBuilder();
|
||||
appendField(fields, "\uD83D\uDEAB Geblockter Account",
|
||||
blockedName + "\n`" + blockedUuid + "`", false);
|
||||
appendField(fields, "\u2705 Verbundener Account",
|
||||
allowedName + "\n`" + allowedUuid + "`", false);
|
||||
appendField(fields, "\uD83C\uDF10 IP", "`" + ip + "`", true);
|
||||
appendField(fields, "Aktion",
|
||||
kickExisting ? "Alter Account getrennt" : "Neuer Account blockiert", true);
|
||||
if (tempBanEnabled) {
|
||||
appendField(fields, "\u26A0\uFE0F Versuche",
|
||||
attempts + " / " + tempBanMaxAttempts
|
||||
+ (attempts >= tempBanMaxAttempts ? " \u2192 IP gebannt!" : ""), true);
|
||||
}
|
||||
|
||||
String body = "{\"username\":\"" + esc(webhookUsername) + "\","
|
||||
+ "\"embeds\":[{"
|
||||
+ "\"title\":\"\uD83D\uDD12 Multi-Account erkannt\","
|
||||
+ "\"description\":\"Ein Spieler hat versucht mit einem zweiten Account beizutreten.\","
|
||||
+ "\"color\":15158332,"
|
||||
+ "\"fields\":[" + fields + "],"
|
||||
+ "\"footer\":{\"text\":\"StatusAPI \u2022 MultiAccountGuard\"},"
|
||||
+ "\"timestamp\":\"" + Instant.now() + "\""
|
||||
+ (webhookThumbnailUrl != null && !webhookThumbnailUrl.isEmpty()
|
||||
? ",\"thumbnail\":{\"url\":\"" + esc(webhookThumbnailUrl) + "\"}" : "")
|
||||
+ "}]}";
|
||||
|
||||
HttpURLConnection conn = null;
|
||||
try {
|
||||
byte[] bytes = body.getBytes(StandardCharsets.UTF_8);
|
||||
conn = (HttpURLConnection) new URL(webhookUrl).openConnection();
|
||||
conn.setRequestMethod("POST");
|
||||
conn.setConnectTimeout(5000);
|
||||
conn.setReadTimeout(8000);
|
||||
conn.setDoOutput(true);
|
||||
conn.setRequestProperty("Content-Type", "application/json; charset=UTF-8");
|
||||
conn.setRequestProperty("Content-Length", String.valueOf(bytes.length));
|
||||
try (OutputStream os = conn.getOutputStream()) { os.write(bytes); }
|
||||
int code = conn.getResponseCode();
|
||||
if (code < 200 || code >= 300) log.warning("[MultiAccountGuard] Webhook HTTP " + code);
|
||||
else log.info("[MultiAccountGuard] Webhook gesendet (HTTP " + code + ")");
|
||||
} catch (Exception e) {
|
||||
log.warning("[MultiAccountGuard] Webhook-Fehler: " + e.getMessage());
|
||||
} finally {
|
||||
if (conn != null) conn.disconnect();
|
||||
}
|
||||
}
|
||||
|
||||
private void appendField(StringBuilder sb, String name, String value, boolean inline) {
|
||||
if (sb.length() > 0) sb.append(",");
|
||||
sb.append("{\"name\":\"").append(esc(name))
|
||||
.append("\",\"value\":\"").append(esc(value))
|
||||
.append("\",\"inline\":").append(inline).append("}");
|
||||
}
|
||||
|
||||
private String esc(String s) {
|
||||
if (s == null) return "";
|
||||
return s.replace("\\","\\\\").replace("\"","\\\"")
|
||||
.replace("\n","\\n").replace("\r","\\r");
|
||||
}
|
||||
|
||||
// -------------------------------------------------------------------------
|
||||
// Bypass – NUR LuckPerms, kein OP-Fallback
|
||||
// -------------------------------------------------------------------------
|
||||
|
||||
private boolean hasBypass(ProxiedPlayer player) {
|
||||
try {
|
||||
User user = LuckPermsProvider.get().getUserManager().getUser(player.getUniqueId());
|
||||
if (user == null) return false;
|
||||
for (Node node : user.getNodes()) {
|
||||
if (node instanceof PermissionNode) {
|
||||
PermissionNode pn = (PermissionNode) node;
|
||||
if (pn.getPermission().equalsIgnoreCase(BYPASS_PERM) && pn.getValue()) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
}
|
||||
return false;
|
||||
} catch (Exception e) {
|
||||
log.warning("[MultiAccountGuard] LuckPerms-Check fehlgeschlagen f\u00fcr " + player.getName() + ": " + e.getMessage());
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
// -------------------------------------------------------------------------
|
||||
// Config
|
||||
// -------------------------------------------------------------------------
|
||||
|
||||
private void loadConfig() {
|
||||
File file = new File(plugin.getDataFolder(), CONFIG_FILE);
|
||||
if (!file.exists()) {
|
||||
log.info("[MultiAccountGuard] Config nicht gefunden – Defaults werden verwendet.");
|
||||
return;
|
||||
}
|
||||
Properties p = new Properties();
|
||||
try (FileInputStream fis = new FileInputStream(file);
|
||||
InputStreamReader r = new InputStreamReader(fis, StandardCharsets.UTF_8)) {
|
||||
p.load(r);
|
||||
|
||||
enabled = Boolean.parseBoolean(p.getProperty("multiaccountguard.enabled", "true"));
|
||||
checkIp = Boolean.parseBoolean(p.getProperty("multiaccountguard.check_ip", "true"));
|
||||
kickExisting = Boolean.parseBoolean(p.getProperty("multiaccountguard.kick_existing", "false"));
|
||||
kickMessage = p.getProperty("multiaccountguard.kick_message", kickMessage);
|
||||
|
||||
staffNotifyEnabled = Boolean.parseBoolean(p.getProperty("multiaccountguard.staff_notify.enabled", "true"));
|
||||
staffNotifyFormat = p.getProperty("multiaccountguard.staff_notify.format", staffNotifyFormat);
|
||||
|
||||
tempBanEnabled = Boolean.parseBoolean(p.getProperty("multiaccountguard.tempban.enabled", "true"));
|
||||
tempBanMaxAttempts = parseInt(p.getProperty("multiaccountguard.tempban.max_attempts", "3"), 3);
|
||||
tempBanDurationSecs = parseInt(p.getProperty("multiaccountguard.tempban.duration_secs", "300"), 300);
|
||||
|
||||
webhookEnabled = Boolean.parseBoolean(p.getProperty("multiaccountguard.webhook.enabled", "true"));
|
||||
webhookUrl = p.getProperty("networkinfo.webhook.url", "").trim();
|
||||
webhookUsername = p.getProperty("networkinfo.webhook.username", "StatusAPI").trim();
|
||||
webhookThumbnailUrl = p.getProperty("networkinfo.webhook.thumbnail_url", "").trim();
|
||||
|
||||
} catch (Exception e) {
|
||||
log.warning("[MultiAccountGuard] Config-Fehler: " + e.getMessage());
|
||||
}
|
||||
}
|
||||
|
||||
private int parseInt(String s, int fallback) {
|
||||
try { return Integer.parseInt(s == null ? "" : s.trim()); }
|
||||
catch (Exception ignored) { return fallback; }
|
||||
}
|
||||
|
||||
// -------------------------------------------------------------------------
|
||||
|
||||
private String extractIp(SocketAddress addr) {
|
||||
if (addr instanceof InetSocketAddress)
|
||||
return ((InetSocketAddress) addr).getAddress().getHostAddress();
|
||||
return null;
|
||||
}
|
||||
|
||||
public boolean isEnabled() { return enabled; }
|
||||
public boolean isCheckIp() { return checkIp; }
|
||||
public boolean isKickExisting() { return kickExisting; }
|
||||
}
|
||||
@@ -33,7 +33,7 @@ import java.util.UUID;
|
||||
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 {
|
||||
|
||||
@@ -66,7 +66,7 @@ public class NetworkInfoModule implements Module {
|
||||
private long lastTpsAlertAt = 0L;
|
||||
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; }
|
||||
private long lastTpsSampleAtMs = 0L;
|
||||
private ScheduledTask alertTask;
|
||||
@@ -139,7 +139,7 @@ public class NetworkInfoModule implements Module {
|
||||
return sendWebhookEmbed(
|
||||
webhookUrl,
|
||||
"✅ 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,
|
||||
null,
|
||||
false
|
||||
@@ -153,7 +153,7 @@ public class NetworkInfoModule implements Module {
|
||||
return sendWebhookEmbed(
|
||||
webhookUrl,
|
||||
"✅ NetworkInfo gestartet",
|
||||
"Überwachung und Webhook-Alerts sind jetzt aktiv.",
|
||||
"\u00dcberwachung und Webhook-Alerts sind jetzt aktiv.",
|
||||
0x2ECC71,
|
||||
fields.toString(),
|
||||
false
|
||||
@@ -165,7 +165,7 @@ public class NetworkInfoModule implements Module {
|
||||
return sendWebhookEmbed(
|
||||
webhookUrl,
|
||||
"🛑 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,
|
||||
null,
|
||||
false
|
||||
@@ -179,7 +179,7 @@ public class NetworkInfoModule implements Module {
|
||||
return sendWebhookEmbed(
|
||||
webhookUrl,
|
||||
"🛑 NetworkInfo gestoppt",
|
||||
"Die NetworkInfo-Überwachung wurde gestoppt.",
|
||||
"Die NetworkInfo-\u00dcberwachung wurde gestoppt.",
|
||||
0xE74C3C,
|
||||
fields.toString(),
|
||||
false
|
||||
@@ -222,7 +222,7 @@ public class NetworkInfoModule implements Module {
|
||||
color = 0x2ECC71;
|
||||
} else {
|
||||
title = "🚨 Attack Detected";
|
||||
shortText = "Ungewöhnlich hoher Verbindungs-Traffic erkannt.";
|
||||
shortText = "Ungew\u00f6hnlich hoher Verbindungs-Traffic erkannt.";
|
||||
color = 0xE74C3C;
|
||||
}
|
||||
|
||||
@@ -312,11 +312,19 @@ public class NetworkInfoModule implements Module {
|
||||
out.put("features", buildFeatureSummary());
|
||||
|
||||
if (includePlayerNames) {
|
||||
List<String> names = new ArrayList<String>();
|
||||
List<Map<String, Object>> playerNames = new ArrayList<Map<String, Object>>();
|
||||
for (ProxiedPlayer p : ProxyServer.getInstance().getPlayers()) {
|
||||
names.add(p.getName());
|
||||
Map<String, Object> entry = new LinkedHashMap<String, Object>();
|
||||
entry.put("name", p.getName());
|
||||
try { entry.put("uuid", p.getUniqueId().toString()); } catch (Exception ignored) {}
|
||||
try {
|
||||
if (p.getServer() != null && p.getServer().getInfo() != null) {
|
||||
entry.put("server", p.getServer().getInfo().getName());
|
||||
}
|
||||
} catch (Exception ignored) {}
|
||||
playerNames.add(entry);
|
||||
}
|
||||
out.put("player_names", names);
|
||||
out.put("player_names", playerNames);
|
||||
}
|
||||
|
||||
return out;
|
||||
@@ -557,7 +565,7 @@ public class NetworkInfoModule implements Module {
|
||||
sendWebhookEmbed(
|
||||
webhookUrl,
|
||||
"⚠️ Hohe RAM-Auslastung",
|
||||
"Ein Schwellwert wurde überschritten.",
|
||||
"Ein Schwellwert wurde \u00fcberschritten.",
|
||||
0xF39C12,
|
||||
fields.toString()
|
||||
);
|
||||
|
||||
+639
-101
File diff suppressed because it is too large
Load Diff
+4
-4
@@ -53,7 +53,7 @@ public class ServerSwitcherModule implements Module {
|
||||
private List<String> aliases = new ArrayList<>(Arrays.asList("wechsel", "switch"));
|
||||
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 colorOnline = "&a";
|
||||
private String colorOffline = "&c";
|
||||
@@ -103,7 +103,7 @@ public class ServerSwitcherModule implements Module {
|
||||
"# Optionale Whitelist (leer = alle BungeeCord-Server)\n" +
|
||||
"# Beispiel: serverswitcher.servers=lobby,citybuild,survival\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.online=&a\n" +
|
||||
"serverswitcher.color.offline=&c\n" +
|
||||
@@ -178,7 +178,7 @@ public class ServerSwitcherModule implements Module {
|
||||
@Override
|
||||
public void execute(CommandSender sender, String[] args) {
|
||||
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;
|
||||
}
|
||||
|
||||
@@ -242,7 +242,7 @@ public class ServerSwitcherModule implements Module {
|
||||
}
|
||||
|
||||
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"));
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -22,11 +22,11 @@ import java.util.UUID;
|
||||
import java.util.concurrent.ConcurrentHashMap;
|
||||
|
||||
/**
|
||||
* VanishModule für StatusAPI (BungeeCord)
|
||||
* VanishModule f\u00fcr StatusAPI (BungeeCord)
|
||||
*
|
||||
* Features:
|
||||
* - /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
|
||||
* - Vanish-Status wird persistent in vanish.dat gespeichert
|
||||
* - Beim Login wird gespeicherter Status wiederhergestellt
|
||||
@@ -71,7 +71,7 @@ public class VanishModule implements Module, Listener {
|
||||
@Override
|
||||
public void onDisable(Plugin plugin) {
|
||||
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)
|
||||
for (UUID uuid : persistentVanished) {
|
||||
VanishProvider.setVanished(uuid, false);
|
||||
@@ -91,9 +91,12 @@ public class VanishModule implements Module, Listener {
|
||||
public void onLogin(PostLoginEvent e) {
|
||||
ProxiedPlayer player = e.getPlayer();
|
||||
if (persistentVanished.contains(player.getUniqueId())) {
|
||||
// Status SOFORT setzen – kein Delay, damit das ChatModule (2s-Task)
|
||||
// den Vanish-Status garantiert vorfindet und keine Join-Nachricht sendet.
|
||||
VanishProvider.setVanished(player.getUniqueId(), true);
|
||||
// Kurze Bestätigung an den Spieler selbst (nach kurzem Delay damit
|
||||
// der Client bereit ist)
|
||||
|
||||
// Nur die Best\u00e4tigungsnachricht an den Spieler wird verz\u00f6gert,
|
||||
// damit der Client bereit ist.
|
||||
plugin.getProxy().getScheduler().schedule(plugin, () -> {
|
||||
if (player.isConnected()) {
|
||||
player.sendMessage(color("&8[&7Vanish&8] &7Du bist &cUnsichtbar&7."));
|
||||
@@ -105,7 +108,7 @@ public class VanishModule implements Module, Listener {
|
||||
@EventHandler
|
||||
public void onDisconnect(PlayerDisconnectEvent e) {
|
||||
// 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());
|
||||
}
|
||||
|
||||
@@ -132,7 +135,7 @@ public class VanishModule implements Module, Listener {
|
||||
} else {
|
||||
// Anderen Spieler vanishen
|
||||
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;
|
||||
}
|
||||
ProxiedPlayer target = ProxyServer.getInstance().getPlayer(args[0]);
|
||||
@@ -173,7 +176,7 @@ public class VanishModule implements Module, Listener {
|
||||
/**
|
||||
* 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
|
||||
*/
|
||||
private void toggleVanish(CommandSender executor, ProxiedPlayer target) {
|
||||
@@ -184,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 &aSichtbar&7.";
|
||||
|
||||
// Feedback an den Ausführenden
|
||||
// Feedback an den Ausf\u00fchrenden
|
||||
executor.sendMessage(color(statusMsg));
|
||||
|
||||
// Falls jemand anderes gevanisht wurde, auch dem Ziel Bescheid geben
|
||||
@@ -195,7 +198,7 @@ public class VanishModule implements Module, Listener {
|
||||
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()) {
|
||||
if (p.equals(executor) || p.equals(target)) continue;
|
||||
if (p.hasPermission("chat.admin.bypass")) {
|
||||
@@ -219,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) {
|
||||
return VanishProvider.isVanished(player);
|
||||
|
||||
@@ -34,7 +34,7 @@ import java.util.Properties;
|
||||
public class VerifyModule implements Module {
|
||||
|
||||
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<>();
|
||||
|
||||
@Override
|
||||
@@ -84,7 +84,7 @@ public class VerifyModule implements Module {
|
||||
ServerConfig config = serverConfigs.computeIfAbsent(serverName, k -> new ServerConfig());
|
||||
if ("id".equalsIgnoreCase(type)) {
|
||||
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)) {
|
||||
config.sharedSecret = props.getProperty(key);
|
||||
}
|
||||
@@ -103,11 +103,11 @@ public class VerifyModule implements Module {
|
||||
|
||||
@Override
|
||||
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;
|
||||
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();
|
||||
ServerConfig config = serverConfigs.get(serverName);
|
||||
|
||||
|
||||
@@ -82,8 +82,8 @@ public class PlayerStats {
|
||||
* balance|totalEarned|totalSpent|transactionsCount|
|
||||
* bansCount|mutesCount|warnsCount|lastPunishmentAt|lastPunishmentType|punishmentScore
|
||||
*
|
||||
* HINWEIS: kills/deaths wurden in Version 1.17.1 als Felder 7 und 8 eingefügt.
|
||||
* fromLine() ist rückwärtskompatibel (alte Dateien ohne kills/deaths werden 0 gesetzt).
|
||||
* HINWEIS: kills/deaths wurden in Version 1.17.1 als Felder 7 und 8 eingef\u00fcgt.
|
||||
* fromLine() ist r\u00fcckw\u00e4rtskompatibel (alte Dateien ohne kills/deaths werden 0 gesetzt).
|
||||
*/
|
||||
public synchronized String toLine() {
|
||||
String safeType = (lastPunishmentType == null ? "" : lastPunishmentType).replace("|", "_");
|
||||
@@ -123,7 +123,7 @@ public class PlayerStats {
|
||||
// Erkennung ob altes Format (ohne kills/deaths, Economy ab Index 7)
|
||||
// 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).
|
||||
// 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.
|
||||
boolean newFormat = false;
|
||||
if (parts.length >= 19) {
|
||||
|
||||
@@ -13,14 +13,14 @@ import java.util.concurrent.TimeUnit;
|
||||
* StatsModule: Tracking von Spielerdaten (Playtime, Joins, Kills, Deaths).
|
||||
*
|
||||
* 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
|
||||
*/
|
||||
public class StatsModule implements Module, Listener {
|
||||
|
||||
/**
|
||||
* Maximale Sessionlänge nach einem Crash noch gutschreiben (24 Stunden).
|
||||
* Längere Differenzen sind unrealistisch → werden ignoriert, currentSessionStart = 0 gesetzt.
|
||||
* Maximale Sessionl\u00e4nge nach einem Crash noch gutschreiben (24 Stunden).
|
||||
* L\u00e4ngere Differenzen sind unrealistisch → werden ignoriert, currentSessionStart = 0 gesetzt.
|
||||
*/
|
||||
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.
|
||||
//
|
||||
// Bei normalem Shutdown setzt onDisable() currentSessionStart = 0 und speichert.
|
||||
// Bei einem Crash (kill -9, OOM, etc.) passiert das nicht. Beim nächsten Start
|
||||
// sind alle Spieler offline, aber currentSessionStart enthält noch den alten
|
||||
// Timestamp. getPlaytimeWithCurrentSession() würde dann fälschlicherweise
|
||||
// Bei einem Crash (kill -9, OOM, etc.) passiert das nicht. Beim n\u00e4chsten Start
|
||||
// sind alle Spieler offline, aber currentSessionStart enth\u00e4lt noch den alten
|
||||
// Timestamp. getPlaytimeWithCurrentSession() w\u00fcrde dann f\u00e4lschlicherweise
|
||||
// (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
|
||||
// - Unplausibel (> MAX_SESSION_SECONDS) → verwerfen, nur zurücksetzen
|
||||
// - In beiden Fällen: currentSessionStart = 0 setzen
|
||||
// - Unplausibel (> MAX_SESSION_SECONDS) → verwerfen, nur zur\u00fccksetzen
|
||||
// - In beiden F\u00e4llen: currentSessionStart = 0 setzen
|
||||
// -----------------------------------------------------------------------
|
||||
long now = System.currentTimeMillis() / 1000L;
|
||||
int recovered = 0;
|
||||
@@ -68,9 +68,9 @@ public class StatsModule implements Module, Listener {
|
||||
recovered++;
|
||||
} else if (delta > MAX_SESSION_SECONDS) {
|
||||
plugin.getLogger().warning(
|
||||
"[StatsModule] Unplausibler currentSessionStart für " + ps.name
|
||||
"[StatsModule] Unplausibler currentSessionStart f\u00fcr " + ps.name
|
||||
+ " (delta=" + delta + "s > " + MAX_SESSION_SECONDS + "s). "
|
||||
+ "Session wird ohne Gutschrift zurückgesetzt."
|
||||
+ "Session wird ohne Gutschrift zur\u00fcckgesetzt."
|
||||
);
|
||||
}
|
||||
ps.currentSessionStart = 0;
|
||||
|
||||
@@ -1,6 +1,8 @@
|
||||
package net.viper.status.stats;
|
||||
|
||||
import java.io.*;
|
||||
import java.nio.file.Files;
|
||||
import java.nio.file.StandardCopyOption;
|
||||
|
||||
/**
|
||||
* Fix #9: save() und load() sind jetzt synchronized um Race Conditions
|
||||
@@ -17,12 +19,21 @@ public class StatsStorage {
|
||||
|
||||
public void save(StatsManager manager) {
|
||||
synchronized (fileLock) {
|
||||
try (BufferedWriter bw = new BufferedWriter(new FileWriter(file))) {
|
||||
// Atomares Schreiben: erst in .tmp, dann umbenennen.
|
||||
// Verhindert korrupte stats.dat bei Server-Absturz während des Schreibvorgangs.
|
||||
File tmp = new File(file.getParentFile(), "stats.dat.tmp");
|
||||
try (BufferedWriter bw = new BufferedWriter(new FileWriter(tmp))) {
|
||||
for (PlayerStats ps : manager.all()) {
|
||||
bw.write(ps.toLine());
|
||||
bw.newLine();
|
||||
}
|
||||
bw.flush();
|
||||
} catch (IOException e) {
|
||||
e.printStackTrace();
|
||||
return; // Nicht umbenennen bei Fehler
|
||||
}
|
||||
try {
|
||||
Files.move(tmp.toPath(), file.toPath(), StandardCopyOption.REPLACE_EXISTING, StandardCopyOption.ATOMIC_MOVE);
|
||||
} catch (IOException e) {
|
||||
e.printStackTrace();
|
||||
}
|
||||
|
||||
@@ -1,18 +1,52 @@
|
||||
§8[§2Viper-Netzwerk§8] §7Der Server läuft 24/7 – also keine Hektik beim Spielen :)
|
||||
§8[§2Viper-Netzwerk§8] §7Dies ist ein privater Server – hier zählt der Zusammenhalt.
|
||||
§8[§dTipp§8] §7Wenn du denkst, du bist sicher… schau nochmal nach. Creeper machen keine Geräusche beim Tippen.
|
||||
§8[§2Viper-Netzwerk§8] §7Wähle einen Server, leg los – der Rest ergibt sich. Oder explodiert.
|
||||
§8[§2Viper-Netzwerk§8] §7Mehr Server. Mehr Blöcke. Mehr Unfälle. Willkommen!
|
||||
§8[§dTipp§8] §7Halte eine Spitzhacke mit Glück bereit. Man weiß nie, wann das nächste Erz kommt.
|
||||
§8[§dTipp§8] §7Mit §e/home§7 kannst du dich jederzeit nach Hause teleportieren.
|
||||
§8[§2Viper-Netzwerk§8] §7Das wichtigste Plugin? Du selbst. Spiel fair, sei kreativ!
|
||||
§8[§2Viper-Netzwerk§8] §7Redstone ist keine Magie – aber fast.
|
||||
§8[§dTipp§8] §7Schilde sind cool. Besonders wenn Skelette zielen.
|
||||
§8[§2Viper-Netzwerk§8] §7Wenn du in Lava fällst, bist du nicht der Erste. Nur der Nächste.
|
||||
§8[§dTipp§8] §7Villager sind nicht dumm – nur sehr… eigen.
|
||||
§8[§2Viper-Netzwerk§8] §7Bau groß, bau sicher – oder bau eine Treppe zur Nachbarschaftsklage.
|
||||
§8[§2Viper-Netzwerk§8] §7Gras wächst. Spieler auch. Gib jedem eine Chance!
|
||||
§8[§2Viper-Netzwerk§8] §7Ein Creeper ist keine Begrüßung. Es sei denn, du willst es spannend machen.
|
||||
§8[§dTipp§8] §7Ein voller Magen ist halbe Miete. Farmen lohnt sich!
|
||||
§8[§2Viper-Netzwerk§8] §7Wir haben keine Probleme – nur Redstone-Schaltungen mit Charakter.
|
||||
§8[§dTipp§8] §7Markiere dein Grundstück mit §e/p claim§7, bevor es jemand anderes tut!
|
||||
§8[%gradient:&b:&f:&b:&lViper-Network%§8] §7Der Server läuft 24/7 – also keine Hektik beim Spielen :)
|
||||
§8[%gradient:&b:&f:&b:&lViper-Network%§8] §7Dies ist ein privater Server – hier zählt der Zusammenhalt.
|
||||
§8[%gradient:&b:&f:&b:&lTipp%§8] §7Wenn du denkst, du bist sicher… schau nochmal nach. Creeper machen keine Geräusche beim Tippen.
|
||||
§8[%gradient:&b:&f:&b:&lViper-Network%§8] §7Wähle einen Server, leg los – der Rest ergibt sich. Oder explodiert.
|
||||
§8[%gradient:&b:&f:&b:&lViper-Network%§8] §7Mehr Server. Mehr Blöcke. Mehr Unfälle. Willkommen!
|
||||
§8[%gradient:&b:&f:&b:&lTipp%§8] §7Halte eine Spitzhacke mit Glück bereit. Man weiß nie, wann das nächste Erz kommt.
|
||||
§8[%gradient:&b:&f:&b:&lTipp%§8] §7Dein Bett ist dein Ankerpunkt – platziere es weise!
|
||||
§8[%gradient:&b:&f:&b:&lViper-Network%§8] §7Das wichtigste Plugin? Du selbst. Spiel fair, sei kreativ!
|
||||
§8[%gradient:&b:&f:&b:&lViper-Network%§8] §7Redstone ist keine Magie – aber fast.
|
||||
§8[%gradient:&b:&f:&b:&lTipp%§8] §7Schilde sind cool. Besonders wenn Skelette zielen.
|
||||
§8[%gradient:&b:&f:&b:&lViper-Network%§8] §7Wenn du in Lava fällst, bist du nicht der Erste. Nur der Nächste.
|
||||
§8[%gradient:&b:&f:&b:&lTipp%§8] §7Villager sind nicht dumm – nur sehr… eigen.
|
||||
§8[%gradient:&b:&f:&b:&lViper-Network%§8] §7Bau groß, bau sicher – oder bau eine Treppe zur Nachbarschaftsklage.
|
||||
§8[%gradient:&b:&f:&b:&lViper-Network%§8] §7Gras wächst. Spieler auch. Gib jedem eine Chance!
|
||||
§8[%gradient:&b:&f:&b:&lViper-Network%§8] §7Ein Creeper ist keine Begrüßung. Es sei denn, du willst es spannend machen.
|
||||
§8[%gradient:&b:&f:&b:&lTipp%§8] §7Ein voller Magen ist halbe Miete. Farmen lohnt sich!
|
||||
§8[%gradient:&b:&f:&b:&lViper-Network%§8] §7Wir haben keine Probleme – nur Redstone-Schaltungen mit Charakter.
|
||||
§8[%gradient:&b:&f:&b:&lTipp%§8] §7Markiere dein Grundstück frühzeitig, bevor es jemand anderes tut!
|
||||
§8[%gradient:&b:&f:&b:&lLobby%§8] §7Hier beginnt alles – such dir deinen Server aus und leg los!
|
||||
§8[%gradient:&b:&f:&b:&lLobby%§8] §7Die Lobby ist kein Ziel, sondern der Start. Oder ein Treffpunkt. Oder beides.
|
||||
§8[%gradient:&b:&f:&b:&lLobby%§8] §7Noch unentschlossen? Einfach mal reinschauen – auf allen Servern!
|
||||
§8[%gradient:&b:&f:&b:&lCitybuild%§8] §7Auf Citybuild baust du deine eigene Stadt – Block für Block.
|
||||
§8[%gradient:&b:&f:&b:&lCitybuild%§8] §7Grundstück sichern nicht vergessen – sonst baut jemand anderes drauf!
|
||||
§8[%gradient:&b:&f:&b:&lCitybuild%§8] §7Gute Nachbarn bauen gute Städte. Oder zumindest hübschere.
|
||||
§8[%gradient:&b:&f:&b:&lCitybuild%§8] §7Je größer die Stadt, desto mehr Redstone geht schief. Viel Erfolg!
|
||||
§8[%gradient:&b:&f:&b:&lTipp%§8] §7Auf Citybuild kannst du dein Grundstück mit Freunden teilen – baut zusammen mehr!
|
||||
§8[%gradient:&b:&f:&b:&lFreebuild%§8] §7Auf Freebuild gibt es keine Grenzen – außer deiner Fantasie.
|
||||
§8[%gradient:&b:&f:&b:&lFreebuild%§8] §7Bauen ohne Regeln. Na ja, fast. Sei trotzdem nett zu deinen Nachbarn.
|
||||
§8[%gradient:&b:&f:&b:&lFreebuild%§8] §7Riesige Projekte, verrückte Ideen – Freebuild macht's möglich.
|
||||
§8[%gradient:&b:&f:&b:&lTipp%§8] §7Auf Freebuild gilt: Erst messen, dann bauen. Oder erst bauen und dann bereuen.
|
||||
§8[%gradient:&b:&f:&b:&lSurvival%§8] §7Auf Survival zählt jeder Block – den du abbaust und den, der auf dich fällt.
|
||||
§8[%gradient:&b:&f:&b:&lSurvival%§8] §7Tag 1: Holz hacken. Tag 2: Höhle finden. Tag 3: Creeper ärgert dich. Klassiker.
|
||||
§8[%gradient:&b:&f:&b:&lSurvival%§8] §7Die Nether-Festung wartet. Bring Feuerresistenz mit – und Mut.
|
||||
§8[%gradient:&b:&f:&b:&lSurvival%§8] §7Ein Bett in der Nähe spart lange Laufwege nach dem Tod. Spar dir den Fußmarsch!
|
||||
§8[%gradient:&b:&f:&b:&lTipp%§8] §7Auf Survival lohnt sich ein Außenposten – Ressourcen gibt's nie genug.
|
||||
§8[%gradient:&b:&f:&b:&lTipp%§8] §7Verzaubere deine Ausrüstung so früh wie möglich. Schutzverzauberung rettet Leben!
|
||||
§8[%gradient:&b:&f:&b:&lSkyblock%§8] §7Auf Skyblock fängst du mit einer Insel an. Was du draus machst, liegt bei dir.
|
||||
§8[%gradient:&b:&f:&b:&lSkyblock%§8] §7Wasser + Lava = Kobblestone. Das Fundament jeder Skyblock-Karriere.
|
||||
§8[%gradient:&b:&f:&b:&lSkyblock%§8] §7Deine Insel, deine Regeln – aber Creeper kennen keine Regeln. Und keine Ränder.
|
||||
§8[%gradient:&b:&f:&b:&lSkyblock%§8] §7Tipp vom Profi: Nie rückwärts auf einer Skyblock-Insel laufen.
|
||||
§8[%gradient:&b:&f:&b:&lTipp%§8] §7Auf Skyblock ist ein Kompostierer Gold wert – Essen ist alles!
|
||||
§8[%gradient:&b:&f:&b:&lTipp%§8] §7Baue deine Skyblock-Insel nach außen aus, bevor du in die Höhe gehst.
|
||||
§8[%gradient:&b:&f:&b:&lMinigames%§8] §7Auf Minigames zählt Schnelligkeit, Köpfchen – und manchmal einfach Glück.
|
||||
§8[%gradient:&b:&f:&b:&lMinigames%§8] §7Gewonnen oder verloren – beim nächsten Spiel ist alles wieder offen!
|
||||
§8[%gradient:&b:&f:&b:&lMinigames%§8] §7Minigames sind der perfekte Ort, um Freunde zu finden. Oder Rivalen.
|
||||
§8[%gradient:&b:&f:&b:&lMinigames%§8] §7Kurze Runden, viel Spaß – einfach Minigames starten und loslegen!
|
||||
§8[%gradient:&b:&f:&b:&lViper-Network%§8] §7Sechs Server, ein Netzwerk – such dir deinen Lieblingsplatz!
|
||||
§8[%gradient:&b:&f:&b:&lViper-Network%§8] §7Egal ob Bauen, Überleben oder Kämpfen – hier ist für jeden was dabei.
|
||||
§8[%gradient:&b:&f:&b:&lViper-Network%§8] §7Neue Spieler sind immer willkommen. Zeig ihnen, wie es geht!
|
||||
§8[%gradient:&b:&f:&b:&lViper-Network%§8] §7Respekt kostet nichts – macht aber den Server für alle besser.
|
||||
§8[%gradient:&b:&f:&b:&lViper-Network%§8] §7Probleme oder Fragen? Wende dich ans Team – wir helfen gerne!
|
||||
§8[%gradient:&b:&f:&b:&lTipp%§8] §7Du kannst zwischen allen Servern wechseln – einfach den Kompass nutzen!
|
||||
@@ -6,10 +6,10 @@ networkinfo.command.enabled=true
|
||||
# Aus Datenschutzgruenden standardmaessig aus. Wenn true, erscheinen alle Spielernamen im JSON.
|
||||
networkinfo.include_player_names=false
|
||||
|
||||
# Discord Webhook fuer Status-, Warn- und Attack-Meldungen
|
||||
networkinfo.webhook.enabled=true
|
||||
networkinfo.webhook.url=https://discord.com/api/webhooks/1488630083164831844/o7L5Mhy5P_xE_n-2Dq9usIVX40o7fCpPHgaGQOVIQHjfK7SDrVJbdeZM-G6vVRVhvzT9
|
||||
networkinfo.webhook.username=StatusAPI
|
||||
# Discord Webhook für Status-, Warn- und Attack-Meldungen
|
||||
networkinfo.webhook.enabled=false
|
||||
networkinfo.webhook.url=
|
||||
networkinfo.webhook.username=
|
||||
networkinfo.webhook.thumbnail_url=
|
||||
networkinfo.webhook.notify_start_stop=true
|
||||
# compact = kurze Texte | detailed = strukturierte Embeds mit Feldern
|
||||
@@ -27,9 +27,13 @@ networkinfo.alert.tps_threshold=18.0
|
||||
# Attack Meldungen (Detected/Stopped)
|
||||
networkinfo.attack.enabled=true
|
||||
# Nutzt automatisch networkinfo.webhook.url
|
||||
networkinfo.attack.source=Viper-Network
|
||||
networkinfo.attack.source=
|
||||
# API-Key fuer POST /network/attack
|
||||
networkinfo.attack.api_key=2jN8xQ4mL9vK3sT7pR1yW6dH5cF0bZ
|
||||
networkinfo.attack.api_key=
|
||||
|
||||
# Zeitzone (IANA-Format, z.B. Europe/Berlin, UTC, America/New_York)
|
||||
# Vollstaendige Liste: https://en.wikipedia.org/wiki/List_of_tz_database_time_zones
|
||||
login.log.timezone=Europe/Berlin
|
||||
|
||||
# ===========================
|
||||
# ANTIBOT / ATTACK GUARD
|
||||
@@ -89,8 +93,8 @@ antibot.learning.recent_events.limit=30
|
||||
# ===========================
|
||||
# Diese Werte koennen von BackendJoinGuard im StatusAPI-Sync-Modus abgeholt werden.
|
||||
# Standalone bleibt weiterhin moeglich.
|
||||
backendguard.enforcement_enabled=true
|
||||
backendguard.log_blocked_attempts=true
|
||||
backendguard.enforcement_enabled=false
|
||||
backendguard.log_blocked_attempts=false
|
||||
backendguard.kick_message=&cBitte verbinde dich nur über den Proxy-Server.
|
||||
# Wichtig: Hier nur echte Proxy-IP(s) eintragen.
|
||||
backendguard.allowed_proxy_ips=127.0.0.1,::1,10.0.0.10
|
||||
@@ -99,4 +103,38 @@ backendguard.allowed_proxy_cidrs=10.0.0.0/24
|
||||
|
||||
# Optionaler API-Key fuer GET /network/backendguard/config
|
||||
# Leer = kein API-Key erforderlich (nur im internen Netzwerk empfohlen)
|
||||
backendguard.sync.api_key=bgSync_7Rk9pQ2nLm5xV8cH4tW1yZ6
|
||||
backendguard.sync.api_key=
|
||||
|
||||
# ===========================
|
||||
# MULTI ACCOUNT GUARD
|
||||
# ===========================
|
||||
# Verhindert, dass ein Spieler mit zwei Accounts gleichzeitig online ist.
|
||||
multiaccountguard.enabled=false
|
||||
|
||||
# IP-Check: Gleiche IP mit unterschiedlichem Namen -> blockieren
|
||||
multiaccountguard.check_ip=false
|
||||
|
||||
# UUID-Check: Gleiche UUID mit unterschiedlichem Namen -> blockieren (Bedrock-Edge-Cases)
|
||||
multiaccountguard.check_uuid=false
|
||||
|
||||
# true = bestehenden (alten) Account rauswerfen, neuen reinlassen
|
||||
# false = neuen Account blockieren (Standard)
|
||||
multiaccountguard.kick_existing=false
|
||||
|
||||
# Kick-Nachricht (& fuer Farbcodes, \n fuer Zeilenumbruch)
|
||||
multiaccountguard.kick_message=&cDu bist bereits mit einem anderen Account online!\n&7Bitte trenne deinen anderen Account zuerst.
|
||||
|
||||
# Staff-Benachrichtigung bei Konflikt (Permission: statusapi.staff.notify)
|
||||
multiaccountguard.staff_notify.enabled=false
|
||||
multiaccountguard.staff_notify.format=&8[&cMAG&8] &e{blocked} &7wurde blockiert &8(2. Account von &e{existing}&8) &7| IP: &f{ip}
|
||||
|
||||
# Temporaerer IP-Bann nach X Versuchen (Integration mit AntiBotModule)
|
||||
# max_attempts: Anzahl Konflikte bevor die IP gebannt wird
|
||||
# duration_secs: Bann-Dauer in Sekunden
|
||||
multiaccountguard.tempban.enabled=false
|
||||
multiaccountguard.tempban.max_attempts=3
|
||||
multiaccountguard.tempban.duration_secs=300
|
||||
|
||||
# Discord-Meldung bei jedem Konflikt (nutzt networkinfo.webhook.url automatisch)
|
||||
multiaccountguard.webhook.enabled=false
|
||||
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
name: StatusAPI
|
||||
main: net.viper.status.StatusAPI
|
||||
version: 4.1.0
|
||||
version: 4.1.5
|
||||
author: M_Viper
|
||||
description: StatusAPI für BungeeCord inkl. Update-Checker, Modul-System und ChatModule
|
||||
# Mindestanforderung: Minecraft 1.20 / BungeeCord mit PlayerChatEvent-Unterstützung
|
||||
@@ -10,12 +10,31 @@ softdepend:
|
||||
- Geyser-BungeeCord
|
||||
|
||||
commands:
|
||||
# ── AfkModule ──────────────────────────────────────────────
|
||||
afk:
|
||||
description: AFK-Modus ein- oder ausschalten
|
||||
usage: /afk
|
||||
|
||||
# ── HelpModule ────────────────────────────────────────────
|
||||
help:
|
||||
description: Zeigt alle verfügbaren Befehle (Admin-Befehle nur mit Berechtigung)
|
||||
usage: /<command> help
|
||||
# Hinweis: Der Befehlsname ist in verify.properties unter statusapi.help konfigurierbar
|
||||
# Beispiel: statusapi.help=vn → /vn help
|
||||
|
||||
# ── ScoreboardModule ──────────────────────────────────────
|
||||
scoreboard:
|
||||
description: Scoreboard ein-/ausblenden oder zwischen Player/Admin wechseln
|
||||
usage: /scoreboard [hide|show|player|admin]
|
||||
aliases: [sb, togglesb]
|
||||
|
||||
# ── StatusAPI Admin ───────────────────────────────────────
|
||||
statusapi:
|
||||
description: StatusAPI verwalten (Reload, Info)
|
||||
usage: /statusapi reload
|
||||
aliases: [sapi]
|
||||
permission: statusapi.admin
|
||||
|
||||
# /pay und /ecoadmin werden von NexEco (Spigot) verwaltet
|
||||
|
||||
# ── VanishModule ──────────────────────────────────────────
|
||||
@@ -191,7 +210,18 @@ commands:
|
||||
aliases: [wechsel, switch]
|
||||
|
||||
permissions:
|
||||
# ── AfkModule ──────────────────────────────────────────────
|
||||
# KEIN default – Permission muss manuell vergeben werden!
|
||||
# lp user <Name> permission set statusapi.afk.bypass true
|
||||
statusapi.afk.bypass:
|
||||
description: Automatisches AFK nach Inaktivität umgehen (nur manuell vergeben)
|
||||
default: false
|
||||
|
||||
# ── StatusAPI Core ────────────────────────────────────────
|
||||
statusapi.admin:
|
||||
description: Zugang zu StatusAPI-Administrationsbefehlen (reload etc.)
|
||||
default: op
|
||||
|
||||
statusapi.update.notify:
|
||||
description: Erlaubt Update-Benachrichtigungen
|
||||
default: op
|
||||
@@ -208,6 +238,16 @@ permissions:
|
||||
description: Zugriff auf /automessage reload
|
||||
default: op
|
||||
|
||||
# ── MultiAccountGuard ─────────────────────────────────────
|
||||
# KEIN default – Permission muss manuell vergeben werden!
|
||||
# lp user <Name> permission set statusapi.multiaccountguard.bypass true
|
||||
statusapi.multiaccountguard.bypass:
|
||||
description: Erlaubt mehrere gleichzeitige Accounts (nur manuell vergeben)
|
||||
|
||||
statusapi.staff.notify:
|
||||
description: Empfaengt Ingame-Benachrichtigungen vom MultiAccountGuard
|
||||
default: false
|
||||
|
||||
# ── ChatModule – Kanaele ──────────────────────────────────
|
||||
chat.channel.local:
|
||||
description: Zugang zum Local-Kanal
|
||||
@@ -281,4 +321,4 @@ permissions:
|
||||
# ── ServerSwitcherModule ──────────────────────────────────
|
||||
serverswitcher.use:
|
||||
description: Zugriff auf /go (Schneller Serverwechsel)
|
||||
default: false
|
||||
default: false
|
||||
@@ -0,0 +1,322 @@
|
||||
name: StatusAPI
|
||||
main: net.viper.status.StatusAPI
|
||||
version: 4.1.4
|
||||
author: M_Viper
|
||||
description: StatusAPI für BungeeCord inkl. Update-Checker, Modul-System und ChatModule
|
||||
# Mindestanforderung: Minecraft 1.20 / BungeeCord mit PlayerChatEvent-Unterstützung
|
||||
|
||||
softdepend:
|
||||
- LuckPerms
|
||||
- Geyser-BungeeCord
|
||||
|
||||
commands:
|
||||
# ── AfkModule ──────────────────────────────────────────────
|
||||
afk:
|
||||
description: AFK-Modus ein- oder ausschalten
|
||||
usage: /afk
|
||||
|
||||
# ── HelpModule ────────────────────────────────────────────
|
||||
help:
|
||||
description: Zeigt alle verfügbaren Befehle (Admin-Befehle nur mit Berechtigung)
|
||||
usage: /<command> help
|
||||
# Hinweis: Der Befehlsname ist in verify.properties unter statusapi.help konfigurierbar
|
||||
# Beispiel: statusapi.help=vn → /vn help
|
||||
|
||||
# ── ScoreboardModule ──────────────────────────────────────
|
||||
scoreboard:
|
||||
description: Scoreboard ein-/ausblenden oder zwischen Player/Admin wechseln
|
||||
usage: /scoreboard [hide|show|player|admin]
|
||||
aliases: [sb, togglesb]
|
||||
|
||||
# ── StatusAPI Admin ───────────────────────────────────────
|
||||
statusapi:
|
||||
description: StatusAPI verwalten (Reload, Info)
|
||||
usage: /statusapi reload
|
||||
aliases: [sapi]
|
||||
permission: statusapi.admin
|
||||
|
||||
# /pay und /ecoadmin werden von NexEco (Spigot) verwaltet
|
||||
|
||||
# ── VanishModule ──────────────────────────────────────────
|
||||
vanish:
|
||||
description: Vanish ein-/ausschalten
|
||||
usage: /vanish [Spieler]
|
||||
aliases: [v]
|
||||
|
||||
vanishlist:
|
||||
description: Alle unsichtbaren Spieler anzeigen
|
||||
usage: /vanishlist
|
||||
aliases: [vlist]
|
||||
|
||||
# ── Verify Modul ──────────────────────────────────────────
|
||||
verify:
|
||||
description: Verifiziere dich mit einem Token
|
||||
usage: /verify <token>
|
||||
|
||||
# ── ForumBridge Modul ─────────────────────────────────────
|
||||
forumlink:
|
||||
description: Verknüpfe deinen Minecraft-Account mit dem Forum
|
||||
usage: /forumlink <token>
|
||||
aliases: [fl]
|
||||
|
||||
forum:
|
||||
description: Zeigt ausstehende Forum-Benachrichtigungen an
|
||||
usage: /forum
|
||||
|
||||
# ── NetworkInfo Modul ─────────────────────────────────────
|
||||
netinfo:
|
||||
description: Zeigt erweiterte Proxy- und Systeminfos an
|
||||
usage: /netinfo
|
||||
|
||||
antibot:
|
||||
description: Zeigt AntiBot-Status und Verwaltung
|
||||
usage: /antibot <status|clearblocks|unblock|profile|reload>
|
||||
|
||||
# ── AutoMessage Modul ─────────────────────────────────────
|
||||
automessage:
|
||||
description: AutoMessage Verwaltung
|
||||
usage: /automessage reload
|
||||
|
||||
# ── ChatModule – Kanal ────────────────────────────────────
|
||||
channel:
|
||||
description: Kanal wechseln oder Kanalliste anzeigen
|
||||
usage: /channel [kanalname]
|
||||
aliases: [ch, kanal]
|
||||
|
||||
# ── ChatModule – HelpOp ───────────────────────────────────
|
||||
helpop:
|
||||
description: Sende eine Hilfeanfrage an das Team
|
||||
usage: /helpop <Nachricht>
|
||||
|
||||
# ── ChatModule – Privat-Nachrichten ───────────────────────
|
||||
msg:
|
||||
description: Sende eine private Nachricht
|
||||
usage: /msg <Spieler> <Nachricht>
|
||||
aliases: [tell, w, whisper]
|
||||
|
||||
r:
|
||||
description: Antworte auf die letzte private Nachricht
|
||||
usage: /r <Nachricht>
|
||||
aliases: [reply, antwort]
|
||||
|
||||
# ── ChatModule – Blockieren ───────────────────────────────
|
||||
ignore:
|
||||
description: Spieler ignorieren
|
||||
usage: /ignore <Spieler>
|
||||
aliases: [block]
|
||||
|
||||
unignore:
|
||||
description: Spieler nicht mehr ignorieren
|
||||
usage: /unignore <Spieler>
|
||||
aliases: [unblock]
|
||||
|
||||
# ── ChatModule – Mute (Admin) ─────────────────────────────
|
||||
chatmute:
|
||||
description: Spieler im Chat stumm schalten
|
||||
usage: /chatmute <Spieler> [Minuten]
|
||||
aliases: [gmute]
|
||||
|
||||
chatunmute:
|
||||
description: Chat-Stummschaltung aufheben
|
||||
usage: /chatunmute <Spieler>
|
||||
aliases: [gunmute]
|
||||
|
||||
# ── ChatModule – Selbst-Mute ──────────────────────────────
|
||||
chataus:
|
||||
description: Eigenen Chat-Empfang ein-/ausschalten
|
||||
usage: /chataus
|
||||
aliases: [togglechat, chaton, chatoff]
|
||||
|
||||
# ── ChatModule – Broadcast ────────────────────────────────
|
||||
broadcast:
|
||||
description: Nachricht an alle Spieler senden
|
||||
usage: /broadcast <Nachricht>
|
||||
aliases: [bc, alert]
|
||||
|
||||
# ── ChatModule – Emoji ────────────────────────────────────
|
||||
emoji:
|
||||
description: Liste aller verfügbaren Emojis
|
||||
usage: /emoji
|
||||
aliases: [emojis]
|
||||
|
||||
# ── ChatModule – Social Spy ───────────────────────────────
|
||||
socialspy:
|
||||
description: Private Nachrichten mitlesen (Admin)
|
||||
usage: /socialspy
|
||||
aliases: [spy]
|
||||
|
||||
# ── ChatModule – Reload ───────────────────────────────────
|
||||
chatreload:
|
||||
description: Chat-Konfiguration neu laden
|
||||
usage: /chatreload
|
||||
|
||||
# ── ChatModule – Admin-Info ───────────────────────────────
|
||||
chatinfo:
|
||||
description: Chat-Informationen ueber einen Spieler anzeigen (Admin)
|
||||
usage: /chatinfo <Spieler>
|
||||
|
||||
# ── ChatModule – Chat-History ─────────────────────────────
|
||||
chathist:
|
||||
description: Chat-History aus dem Logfile anzeigen (Admin)
|
||||
usage: /chathist [Spieler] [Anzahl]
|
||||
|
||||
# ── ChatModule – Mentions ─────────────────────────────────
|
||||
mentions:
|
||||
description: Mention-Benachrichtigungen ein-/ausschalten
|
||||
usage: /mentions
|
||||
aliases: [mention]
|
||||
|
||||
# ── ChatModule – Plugin-Bypass ────────────────────────────
|
||||
chatbypass:
|
||||
description: ChatModule fuer naechste Eingabe ueberspringen (fuer Plugin-Dialoge wie CMI)
|
||||
usage: /chatbypass
|
||||
aliases: [cbp]
|
||||
|
||||
# ── ChatModule – Account-Verknuepfung ─────────────────────
|
||||
# FIX #4: Command-Namen stimmen jetzt mit der Code-Registrierung überein.
|
||||
# Im ChatModule wird "discordlink" mit Alias "dlink" registriert,
|
||||
# und "telegramlink" mit Alias "tlink".
|
||||
discordlink:
|
||||
description: Minecraft-Account mit Discord verknuepfen
|
||||
usage: /discordlink
|
||||
aliases: [dlink]
|
||||
|
||||
telegramlink:
|
||||
description: Minecraft-Account mit Telegram verknuepfen
|
||||
usage: /telegramlink
|
||||
aliases: [tlink]
|
||||
|
||||
unlink:
|
||||
description: Account-Verknuepfung aufheben
|
||||
usage: /unlink <discord|telegram|all>
|
||||
|
||||
# ── ChatModule – Report ───────────────────────────────────
|
||||
report:
|
||||
description: Spieler melden
|
||||
usage: /report <Spieler> <Grund>
|
||||
|
||||
reports:
|
||||
description: Offene Reports anzeigen (Admin)
|
||||
usage: /reports [all]
|
||||
|
||||
reportclose:
|
||||
description: Report schliessen (Admin)
|
||||
usage: /reportclose <ID>
|
||||
|
||||
# ── ServerSwitcherModule ──────────────────────────────────
|
||||
go:
|
||||
description: Schneller Serverwechsel ueber Chat-Menue oder direkt
|
||||
usage: /go [servername]
|
||||
aliases: [wechsel, switch]
|
||||
|
||||
permissions:
|
||||
# ── AfkModule ──────────────────────────────────────────────
|
||||
statusapi.afk.bypass:
|
||||
description: Automatisches AFK nach Inaktivität umgehen
|
||||
default: op
|
||||
|
||||
# ── StatusAPI Core ────────────────────────────────────────
|
||||
statusapi.admin:
|
||||
description: Zugang zu StatusAPI-Administrationsbefehlen (reload etc.)
|
||||
default: op
|
||||
|
||||
statusapi.update.notify:
|
||||
description: Erlaubt Update-Benachrichtigungen
|
||||
default: op
|
||||
|
||||
statusapi.netinfo:
|
||||
description: Zugriff auf /netinfo
|
||||
default: op
|
||||
|
||||
statusapi.antibot:
|
||||
description: Zugriff auf /antibot
|
||||
default: op
|
||||
|
||||
statusapi.automessage:
|
||||
description: Zugriff auf /automessage reload
|
||||
default: op
|
||||
|
||||
# ── MultiAccountGuard ─────────────────────────────────────
|
||||
# KEIN default – Permission muss manuell vergeben werden!
|
||||
# lp user <Name> permission set statusapi.multiaccountguard.bypass true
|
||||
statusapi.multiaccountguard.bypass:
|
||||
description: Erlaubt mehrere gleichzeitige Accounts (nur manuell vergeben)
|
||||
|
||||
statusapi.staff.notify:
|
||||
description: Empfaengt Ingame-Benachrichtigungen vom MultiAccountGuard
|
||||
default: false
|
||||
|
||||
# ── ChatModule – Kanaele ──────────────────────────────────
|
||||
chat.channel.local:
|
||||
description: Zugang zum Local-Kanal
|
||||
default: true
|
||||
|
||||
chat.channel.trade:
|
||||
description: Zugang zum Trade-Kanal
|
||||
default: true
|
||||
|
||||
chat.channel.staff:
|
||||
description: Zugang zum Staff-Kanal
|
||||
default: false
|
||||
|
||||
# ── ChatModule – HelpOp ───────────────────────────────────
|
||||
chat.helpop.receive:
|
||||
description: HelpOp-Nachrichten empfangen
|
||||
default: false
|
||||
|
||||
# ── ChatModule – Mute ─────────────────────────────────────
|
||||
chat.mute:
|
||||
description: Spieler muten / unmuten
|
||||
default: false
|
||||
|
||||
# ── ChatModule – Broadcast ────────────────────────────────
|
||||
chat.broadcast:
|
||||
description: Broadcast-Nachrichten senden
|
||||
default: false
|
||||
|
||||
# ── ChatModule – Social Spy ───────────────────────────────
|
||||
chat.socialspy:
|
||||
description: Private Nachrichten mitlesen
|
||||
default: false
|
||||
|
||||
# ── ChatModule – Admin ────────────────────────────────────
|
||||
chat.admin.bypass:
|
||||
description: Admin-Bypass - Kann nicht geblockt/gemutet werden
|
||||
default: op
|
||||
|
||||
chat.admin.notify:
|
||||
description: Benachrichtigungen ueber Mutes und Blocks erhalten
|
||||
default: false
|
||||
|
||||
# ── ChatModule – Report ───────────────────────────────────
|
||||
chat.report:
|
||||
description: Spieler reporten (/report)
|
||||
default: true
|
||||
|
||||
# ── ChatModule – Farben ───────────────────────────────────
|
||||
chat.color:
|
||||
description: Farbcodes (&a, &b, ...) im Chat nutzen
|
||||
default: false
|
||||
|
||||
chat.color.format:
|
||||
description: Formatierungen (&l, &o, &n, ...) im Chat nutzen
|
||||
default: false
|
||||
|
||||
# ── ChatModule – Filter ───────────────────────────────────
|
||||
chat.filter.bypass:
|
||||
description: Chat-Filter (Anti-Spam, Caps, Blacklist) umgehen
|
||||
default: false
|
||||
|
||||
# ── CommandBlocker ────────────────────────────────────────
|
||||
commandblocker.bypass:
|
||||
description: Command-Blocker umgehen
|
||||
default: op
|
||||
|
||||
commandblocker.admin:
|
||||
description: CommandBlocker verwalten (/cb)
|
||||
default: op
|
||||
|
||||
# ── ServerSwitcherModule ──────────────────────────────────
|
||||
serverswitcher.use:
|
||||
description: Zugriff auf /go (Schneller Serverwechsel)
|
||||
default: false
|
||||
@@ -138,3 +138,10 @@ scoreboard.admin_lines.13=&7Spieler: %online% &8/ &7%maxplayers%
|
||||
scoreboard.admin_lines.14=%line%
|
||||
scoreboard.admin_lines.15=&7%compass%
|
||||
scoreboard.admin_lines.15.2=&7Pos: X:&f%x% &7Y:&f%y% &7Z:&f%z%
|
||||
|
||||
# ===================================================
|
||||
# NAMETAG - Prefix ueber dem Spieler-Kopf
|
||||
# ===================================================
|
||||
# Zeigt den LuckPerms-Prefix ueber dem Spieler-Avatar an.
|
||||
# Auf false setzen zum Deaktivieren.
|
||||
nametag.enabled=true
|
||||
|
||||
@@ -17,7 +17,23 @@ broadcast.format=%prefixColored% %messageColored%
|
||||
# ===========================
|
||||
statusapi.port=9191
|
||||
|
||||
# ===========================
|
||||
# PLAYER LOGIN LOGGER
|
||||
# ===========================
|
||||
# Schreibt UUID, Name und IP jedes Spielers beim Join in player-logins.log
|
||||
# Standardmaessig deaktiviert - nur auf deinem Server auf true setzen
|
||||
login-logger.enabled=false
|
||||
|
||||
# ===========================
|
||||
# INGAME HILFE
|
||||
# ===========================
|
||||
# Befehlsname für die Ingame-Hilfe (Standard: help)
|
||||
# Beispiel: statusapi.help=vn → Befehl wird /vn
|
||||
statusapi.help=sapi
|
||||
|
||||
# Permission, die Admin-Befehle in der Hilfe sichtbar macht
|
||||
# (OP und Spieler mit dieser Permission sehen die Admin-Sektion)
|
||||
statusapi.help.permission=statusapi.admin
|
||||
|
||||
# ===========================
|
||||
# WORDPRESS / VERIFY EINSTELLUNGEN
|
||||
@@ -93,3 +109,5 @@ economy.mysql.database=survivalplus
|
||||
economy.mysql.username=root
|
||||
economy.mysql.password=
|
||||
economy.start-balance=500.0
|
||||
|
||||
|
||||
|
||||
+79
-5
@@ -6,14 +6,23 @@
|
||||
|
||||
<groupId>net.viper</groupId>
|
||||
<artifactId>StatusAPIBridge</artifactId>
|
||||
<version>1.0.0</version>
|
||||
<version>1.0.3</version>
|
||||
<packaging>jar</packaging>
|
||||
|
||||
<properties>
|
||||
<java.version>17</java.version>
|
||||
<!--
|
||||
Java 21 (LTS) – kompatibel mit 1.21.x (benötigt mind. Java 21) und 26.1.x
|
||||
(benötigt Java 25 zum Starten, akzeptiert aber Java-21-Bytecode problemlos,
|
||||
da Java abwärtskompatibel ist).
|
||||
Für einen reinen 26.1.x-Build kann java.version auf 25 gesetzt werden.
|
||||
-->
|
||||
<java.version>21</java.version>
|
||||
<maven.compiler.source>${java.version}</maven.compiler.source>
|
||||
<maven.compiler.target>${java.version}</maven.compiler.target>
|
||||
<maven.compiler.release>${java.version}</maven.compiler.release>
|
||||
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
|
||||
<!-- Standard-API-Version (wird durch Profile überschrieben) -->
|
||||
<spigot.version>1.21.1-R0.1-SNAPSHOT</spigot.version>
|
||||
</properties>
|
||||
|
||||
<repositories>
|
||||
@@ -25,14 +34,18 @@
|
||||
<id>vault-repo</id>
|
||||
<url>https://nexus.hc.to/content/repositories/pub_releases/</url>
|
||||
</repository>
|
||||
<repository>
|
||||
<id>placeholderapi</id>
|
||||
<url>https://repo.extendedclip.com/content/repositories/placeholderapi/</url>
|
||||
</repository>
|
||||
</repositories>
|
||||
|
||||
<dependencies>
|
||||
<!-- Spigot API -->
|
||||
<!-- Spigot API – Version wird durch Profil gesteuert -->
|
||||
<dependency>
|
||||
<groupId>org.spigotmc</groupId>
|
||||
<artifactId>spigot-api</artifactId>
|
||||
<version>1.21-R0.1-SNAPSHOT</version>
|
||||
<version>${spigot.version}</version>
|
||||
<scope>provided</scope>
|
||||
</dependency>
|
||||
<!-- Vault -->
|
||||
@@ -42,15 +55,76 @@
|
||||
<version>1.7</version>
|
||||
<scope>provided</scope>
|
||||
</dependency>
|
||||
<!-- PlaceholderAPI (optional – per Reflection genutzt) -->
|
||||
<dependency>
|
||||
<groupId>me.clip</groupId>
|
||||
<artifactId>placeholderapi</artifactId>
|
||||
<version>2.11.6</version>
|
||||
<scope>provided</scope>
|
||||
</dependency>
|
||||
</dependencies>
|
||||
|
||||
<!-- ══════════════════════════════════════════════════════════════════════
|
||||
Maven-Profile für Multi-Version-Build
|
||||
mvn package → mc-1.21.1 (Standard, Java 21)
|
||||
mvn package -P mc-26.1.2 → mc-26.1.2 (Java 21 Bytecode, läuft auf Java 25)
|
||||
mvn package -P mc-26.1.2-j25 → mc-26.1.2 mit Java-25-Bytecode (benötigt JDK 25)
|
||||
══════════════════════════════════════════════════════════════════════ -->
|
||||
<profiles>
|
||||
|
||||
<!-- Profil 1: Minecraft 1.21.1 (Standard) -->
|
||||
<profile>
|
||||
<id>mc-1.21.1</id>
|
||||
<activation>
|
||||
<activeByDefault>true</activeByDefault>
|
||||
</activation>
|
||||
<properties>
|
||||
<spigot.version>1.21.1-R0.1-SNAPSHOT</spigot.version>
|
||||
</properties>
|
||||
</profile>
|
||||
|
||||
<!-- Profil 2: Minecraft 26.1.2 – kompiliert gegen 1.21.1-API (kompatibel),
|
||||
erzeugt Java-21-Bytecode der auf Java 25 läuft -->
|
||||
<profile>
|
||||
<id>mc-26.1.2</id>
|
||||
<properties>
|
||||
<spigot.version>26.1.2-R0.1-SNAPSHOT</spigot.version>
|
||||
</properties>
|
||||
</profile>
|
||||
|
||||
<!-- Profil 3: Minecraft 26.1.2 mit nativem Java-25-Bytecode.
|
||||
Benötigt JDK 25 als JAVA_HOME beim Build. -->
|
||||
<profile>
|
||||
<id>mc-26.1.2-j25</id>
|
||||
<properties>
|
||||
<spigot.version>26.1.2-R0.1-SNAPSHOT</spigot.version>
|
||||
<java.version>25</java.version>
|
||||
<maven.compiler.source>25</maven.compiler.source>
|
||||
<maven.compiler.target>25</maven.compiler.target>
|
||||
<maven.compiler.release>25</maven.compiler.release>
|
||||
</properties>
|
||||
</profile>
|
||||
|
||||
</profiles>
|
||||
|
||||
<build>
|
||||
<finalName>StatusAPIBridge</finalName>
|
||||
<plugins>
|
||||
<plugin>
|
||||
<groupId>org.apache.maven.plugins</groupId>
|
||||
<artifactId>maven-compiler-plugin</artifactId>
|
||||
<version>3.14.0</version>
|
||||
<configuration>
|
||||
<source>${java.version}</source>
|
||||
<target>${java.version}</target>
|
||||
<release>${java.version}</release>
|
||||
<encoding>UTF-8</encoding>
|
||||
</configuration>
|
||||
</plugin>
|
||||
<plugin>
|
||||
<groupId>org.apache.maven.plugins</groupId>
|
||||
<artifactId>maven-shade-plugin</artifactId>
|
||||
<version>3.5.1</version>
|
||||
<version>3.6.0</version>
|
||||
<executions>
|
||||
<execution>
|
||||
<phase>package</phase>
|
||||
|
||||
@@ -14,24 +14,30 @@ import org.bukkit.event.player.PlayerMoveEvent;
|
||||
import org.bukkit.event.player.PlayerQuitEvent;
|
||||
import org.bukkit.plugin.RegisteredServiceProvider;
|
||||
import org.bukkit.plugin.java.JavaPlugin;
|
||||
import org.bukkit.plugin.messaging.PluginMessageListener;
|
||||
|
||||
import java.io.OutputStream;
|
||||
import java.net.HttpURLConnection;
|
||||
import java.net.URL;
|
||||
import java.nio.charset.StandardCharsets;
|
||||
import java.util.Map;
|
||||
import java.util.Set;
|
||||
import java.util.UUID;
|
||||
import java.util.concurrent.ConcurrentHashMap;
|
||||
import java.util.concurrent.ExecutorService;
|
||||
import java.util.concurrent.Executors;
|
||||
import java.util.concurrent.LinkedBlockingQueue;
|
||||
import java.util.concurrent.RejectedExecutionException;
|
||||
import java.util.concurrent.ThreadPoolExecutor;
|
||||
import java.util.concurrent.TimeUnit;
|
||||
|
||||
public class StatusAPIBridge extends JavaPlugin implements Listener {
|
||||
public class StatusAPIBridge extends JavaPlugin implements Listener, PluginMessageListener {
|
||||
|
||||
private Economy economy;
|
||||
private String statusApiUrl;
|
||||
private int pushDelayTicks;
|
||||
private int liveSyncIntervalTicks;
|
||||
private int scoreboardSyncIntervalTicks;
|
||||
private int compassMinPushIntervalMs;
|
||||
|
||||
private final Map<UUID, Double> lastPushedBalance = new ConcurrentHashMap<>();
|
||||
private final Map<UUID, Double> lastPushedHealth = new ConcurrentHashMap<>();
|
||||
@@ -39,21 +45,62 @@ public class StatusAPIBridge extends JavaPlugin implements Listener {
|
||||
private final Map<UUID, String> lastPushedWorld = new ConcurrentHashMap<>();
|
||||
private final Map<UUID, String> lastPushedData = new ConcurrentHashMap<>();
|
||||
private final Map<UUID, String> lastPushedStats = new ConcurrentHashMap<>();
|
||||
private final Map<UUID, Long> lastCompassPushAt = new ConcurrentHashMap<>();
|
||||
private String lastPushedTicketGlobal = "";
|
||||
|
||||
private final ExecutorService httpExecutor = Executors.newSingleThreadExecutor(r -> {
|
||||
Thread t = new Thread(r, "StatusAPIBridge-HTTP");
|
||||
t.setDaemon(true);
|
||||
return t;
|
||||
});
|
||||
// ── PlaceholderAPI ────────────────────────────────────────────────────────
|
||||
private final Set<String> papiTokens = new java.util.LinkedHashSet<>();
|
||||
private final Map<UUID, String> lastPapiValues = new ConcurrentHashMap<>();
|
||||
private boolean papiEnabled = false;
|
||||
|
||||
// ── Nametag ───────────────────────────────────────────────────────────────
|
||||
/** Scoreboard für Nametag-Teams (einmalig pro Server erstellt) */
|
||||
private org.bukkit.scoreboard.Scoreboard nametagBoard = null;
|
||||
/** Zuletzt gesetzter Prefix pro Spieler (Change-Detection) */
|
||||
private final Map<UUID, String> lastNametagPrefix = new ConcurrentHashMap<>();
|
||||
/** Feature aktivierbar via config: nametag-enabled */
|
||||
private boolean nametagEnabled = true;
|
||||
/** AFK-Status pro Spieler – wird via Plugin-Message von BungeeCord aktualisiert */
|
||||
private final Map<UUID, Boolean> afkState = new ConcurrentHashMap<>();
|
||||
|
||||
// ── Versions-Detection ────────────────────────────────────────────────────
|
||||
// true = 1.21.x-Modus (Spigot/Paper)
|
||||
// false = 26.1.x-Modus (neuere Server-Version, kein NMS-Fallback)
|
||||
private boolean isLegacyMode = true;
|
||||
|
||||
// BUGFIX: DiscardOldestPolicy kann HTTP-Requests silently verwerfen obwohl lastPushed* schon
|
||||
// aktualisiert wurde → der nächste Sync-Zyklus schickt keinen erneuten Push mehr.
|
||||
// CallerRunsPolicy blockiert stattdessen den aufrufenden Thread, was verhindert dass
|
||||
// Economy/Stats-Updates verloren gehen. Queue-Größe auf 2000 reduziert (war 4000, unnötig groß).
|
||||
private final ExecutorService httpExecutor = new ThreadPoolExecutor(
|
||||
2,
|
||||
2,
|
||||
30L,
|
||||
TimeUnit.SECONDS,
|
||||
new LinkedBlockingQueue<>(2000),
|
||||
r -> {
|
||||
Thread t = new Thread(r, "StatusAPIBridge-HTTP");
|
||||
t.setDaemon(true);
|
||||
return t;
|
||||
},
|
||||
new ThreadPoolExecutor.CallerRunsPolicy()
|
||||
);
|
||||
|
||||
@Override
|
||||
public void onEnable() {
|
||||
saveDefaultConfig();
|
||||
detectMinecraftVersion();
|
||||
nametagEnabled = getConfig().getBoolean("nametag-enabled", true);
|
||||
if (nametagEnabled) {
|
||||
// Eigenes Scoreboard für Nametag-Teams erstellen
|
||||
nametagBoard = Bukkit.getScoreboardManager().getNewScoreboard();
|
||||
getLogger().info("Nametag-Prefix aktiviert (LuckPerms).");
|
||||
}
|
||||
statusApiUrl = getConfig().getString("statusapi-url", "http://127.0.0.1:9191").trim();
|
||||
pushDelayTicks = getConfig().getInt("push-delay-ticks", 40);
|
||||
liveSyncIntervalTicks = Math.max(20, getConfig().getInt("live-sync-interval-ticks", 20));
|
||||
scoreboardSyncIntervalTicks = Math.max(20, getConfig().getInt("scoreboard-sync-interval-ticks", 20));
|
||||
compassMinPushIntervalMs = Math.max(50, getConfig().getInt("compass-min-push-interval-ms", 150));
|
||||
|
||||
if (!setupEconomy()) {
|
||||
getLogger().warning("Vault/Economy nicht gefunden – Economy-Push deaktiviert.");
|
||||
@@ -62,6 +109,8 @@ public class StatusAPIBridge extends JavaPlugin implements Listener {
|
||||
}
|
||||
|
||||
Bukkit.getPluginManager().registerEvents(this, this);
|
||||
// Plugin-Message-Channel für AFK-Status-Updates von BungeeCord registrieren
|
||||
getServer().getMessenger().registerIncomingPluginChannel(this, "statusapi:afk", this);
|
||||
Bukkit.getScheduler().runTaskTimer(this, this::pushChangedBalancesForOnlinePlayers,
|
||||
liveSyncIntervalTicks, liveSyncIntervalTicks);
|
||||
Bukkit.getScheduler().runTaskTimer(this, this::pushScoreboardData,
|
||||
@@ -70,11 +119,34 @@ public class StatusAPIBridge extends JavaPlugin implements Listener {
|
||||
// TicketSystem-Daten alle 5 Sekunden pushen (100 Ticks)
|
||||
Bukkit.getScheduler().runTaskTimerAsynchronously(this, this::pushTicketData, 100L, 100L);
|
||||
|
||||
// PlaceholderAPI-Integration
|
||||
papiEnabled = getServer().getPluginManager().getPlugin("PlaceholderAPI") != null;
|
||||
if (papiEnabled) {
|
||||
// Tokens alle 30s von StatusAPI holen, nur bei Änderung loggen
|
||||
Bukkit.getScheduler().runTaskTimerAsynchronously(this, () -> {
|
||||
Set<String> before = new java.util.LinkedHashSet<>(papiTokens);
|
||||
boolean fetched = fetchPapiTokensFromStatusAPI();
|
||||
if (fetched && !papiTokens.equals(before)) {
|
||||
if (papiTokens.isEmpty()) {
|
||||
getLogger().info("[PAPI] Keine Placeholder in der StatusAPI-Config gefunden.");
|
||||
} else {
|
||||
getLogger().info("[PAPI] " + papiTokens.size() + " Placeholder erkannt: " + papiTokens);
|
||||
}
|
||||
}
|
||||
}, 40L, 600L); // nach 2s starten, alle 30s wiederholen
|
||||
|
||||
// Sync-Task läuft dauerhaft – tut nichts wenn papiTokens leer
|
||||
Bukkit.getScheduler().runTaskTimer(this, this::syncPapiValues, scoreboardSyncIntervalTicks, scoreboardSyncIntervalTicks);
|
||||
} else {
|
||||
getLogger().info("[PAPI] PlaceholderAPI nicht gefunden – Placeholder werden nicht aufgelöst.");
|
||||
}
|
||||
|
||||
getLogger().info("StatusAPIBridge gestartet. Ziel: " + statusApiUrl);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onDisable() {
|
||||
getServer().getMessenger().unregisterIncomingPluginChannel(this, "statusapi:afk");
|
||||
httpExecutor.shutdownNow();
|
||||
}
|
||||
|
||||
@@ -96,6 +168,9 @@ public class StatusAPIBridge extends JavaPlugin implements Listener {
|
||||
if (!player.isOnline()) return;
|
||||
if (economy != null) pushEconomy(player);
|
||||
pushPlayerScoreboardData(player);
|
||||
if (papiEnabled && !papiTokens.isEmpty()) pushPapiValues(player);
|
||||
// Nametag: LuckPerms-Prefix über dem Kopf setzen
|
||||
if (nametagEnabled) applyNametag(player);
|
||||
}, pushDelayTicks);
|
||||
}
|
||||
|
||||
@@ -110,26 +185,31 @@ public class StatusAPIBridge extends JavaPlugin implements Listener {
|
||||
lastPushedWorld.remove(id);
|
||||
lastPushedData.remove(id);
|
||||
lastPushedStats.remove(id);
|
||||
lastCompassPushAt.remove(id);
|
||||
lastPapiValues.remove(id);
|
||||
lastNametagPrefix.remove(id);
|
||||
// Nametag-Team beim Quit aufräumen
|
||||
if (nametagEnabled) removeNametag(player);
|
||||
}
|
||||
|
||||
@EventHandler(priority = EventPriority.MONITOR, ignoreCancelled = true)
|
||||
public void onDamage(EntityDamageEvent e) {
|
||||
if (!(e.getEntity() instanceof Player)) return;
|
||||
Player player = (Player) e.getEntity();
|
||||
if (!(e.getEntity() instanceof Player player)) return;
|
||||
Bukkit.getScheduler().runTaskLater(this,
|
||||
() -> { if (player.isOnline()) pushHealthIfChanged(player); }, 1L);
|
||||
}
|
||||
|
||||
@EventHandler(priority = EventPriority.MONITOR, ignoreCancelled = true)
|
||||
public void onHeal(EntityRegainHealthEvent e) {
|
||||
if (!(e.getEntity() instanceof Player)) return;
|
||||
Player player = (Player) e.getEntity();
|
||||
if (!(e.getEntity() instanceof Player player)) return;
|
||||
Bukkit.getScheduler().runTaskLater(this,
|
||||
() -> { if (player.isOnline()) pushHealthIfChanged(player); }, 1L);
|
||||
}
|
||||
|
||||
@EventHandler(priority = EventPriority.MONITOR, ignoreCancelled = true)
|
||||
public void onMove(PlayerMoveEvent e) {
|
||||
// getTo() kann in 1.20.5+ bei reinen Head-Rotationen null sein
|
||||
if (e.getTo() == null) return;
|
||||
if (e.getFrom().getYaw() == e.getTo().getYaw()) return;
|
||||
pushCompassIfChanged(e.getPlayer());
|
||||
}
|
||||
@@ -160,6 +240,9 @@ public class StatusAPIBridge extends JavaPlugin implements Listener {
|
||||
pushWorldIfChanged(player);
|
||||
pushPlayerDataIfChanged(player);
|
||||
pushStatsIfChanged(player);
|
||||
// BUGFIX: applyNametag() hier NICHT aufrufen – es überschreibt bei jedem Scoreboard-Sync-Tick
|
||||
// das Scoreboard ALLER Online-Spieler und zerstört dabei Scoreboards anderer Plugins.
|
||||
// Nametag-Updates laufen jetzt nur noch event-basiert (Join + expliziter Rang-Wechsel).
|
||||
}
|
||||
|
||||
// ── Push-Methoden ─────────────────────────────────────────────────────────
|
||||
@@ -169,7 +252,7 @@ public class StatusAPIBridge extends JavaPlugin implements Listener {
|
||||
}
|
||||
|
||||
private void pushEconomyAsync(UUID uuid, String name, double balance) {
|
||||
httpExecutor.execute(() -> {
|
||||
submitHttp(() -> {
|
||||
try {
|
||||
sendPost(statusApiUrl + "/economy/update",
|
||||
"{\"uuid\":\"" + uuid + "\",\"name\":\"" + escapeName(name)
|
||||
@@ -178,7 +261,7 @@ public class StatusAPIBridge extends JavaPlugin implements Listener {
|
||||
} catch (Exception e) {
|
||||
getLogger().warning("Economy-Push fehlgeschlagen fuer " + name + ": " + e.getMessage());
|
||||
}
|
||||
});
|
||||
}, "economy");
|
||||
}
|
||||
|
||||
private void pushHealthIfChanged(Player player) {
|
||||
@@ -187,7 +270,7 @@ public class StatusAPIBridge extends JavaPlugin implements Listener {
|
||||
if (last != null && Math.abs(health - last) < 0.01) return;
|
||||
lastPushedHealth.put(player.getUniqueId(), health);
|
||||
UUID uuid = player.getUniqueId(); String name = player.getName();
|
||||
httpExecutor.execute(() -> {
|
||||
submitHttp(() -> {
|
||||
try {
|
||||
sendPost(statusApiUrl + "/scoreboard/health",
|
||||
"{\"uuid\":\"" + uuid + "\",\"name\":\"" + escapeName(name)
|
||||
@@ -195,17 +278,22 @@ public class StatusAPIBridge extends JavaPlugin implements Listener {
|
||||
} catch (Exception e) {
|
||||
getLogger().warning("Health-Push fehlgeschlagen: " + e.getMessage());
|
||||
}
|
||||
});
|
||||
}, "health");
|
||||
}
|
||||
|
||||
private void pushCompassIfChanged(Player player) {
|
||||
UUID uuid = player.getUniqueId();
|
||||
long now = System.currentTimeMillis();
|
||||
Long lastPushAt = lastCompassPushAt.get(uuid);
|
||||
if (lastPushAt != null && (now - lastPushAt) < compassMinPushIntervalMs) return;
|
||||
|
||||
// Rohen Yaw normalisieren auf 0..360 (0 = Süden, wie MC-Konvention)
|
||||
float rawYaw = player.getLocation().getYaw();
|
||||
float normYaw = ((rawYaw % 360) + 360) % 360;
|
||||
String yawStr = String.format(java.util.Locale.US, "%.1f", normYaw);
|
||||
|
||||
// Nur senden wenn Änderung >= 0.5° – fein genug für 1-Grad-Slots
|
||||
String lastStr = lastPushedCompass.get(player.getUniqueId());
|
||||
String lastStr = lastPushedCompass.get(uuid);
|
||||
if (lastStr != null) {
|
||||
try {
|
||||
float lastYaw = Float.parseFloat(lastStr);
|
||||
@@ -215,9 +303,10 @@ public class StatusAPIBridge extends JavaPlugin implements Listener {
|
||||
} catch (NumberFormatException ignored) {}
|
||||
}
|
||||
|
||||
lastPushedCompass.put(player.getUniqueId(), yawStr);
|
||||
UUID uuid = player.getUniqueId(); String name = player.getName();
|
||||
httpExecutor.execute(() -> {
|
||||
lastPushedCompass.put(uuid, yawStr);
|
||||
lastCompassPushAt.put(uuid, now);
|
||||
String name = player.getName();
|
||||
submitHttp(() -> {
|
||||
try {
|
||||
sendPost(statusApiUrl + "/scoreboard/compass",
|
||||
"{\"uuid\":\"" + uuid + "\",\"name\":\"" + escapeName(name)
|
||||
@@ -225,7 +314,7 @@ public class StatusAPIBridge extends JavaPlugin implements Listener {
|
||||
} catch (Exception e) {
|
||||
getLogger().warning("Compass-Push fehlgeschlagen: " + e.getMessage());
|
||||
}
|
||||
});
|
||||
}, "compass");
|
||||
}
|
||||
|
||||
private void pushWorldIfChanged(Player player) {
|
||||
@@ -233,7 +322,7 @@ public class StatusAPIBridge extends JavaPlugin implements Listener {
|
||||
if (world.equals(lastPushedWorld.get(player.getUniqueId()))) return;
|
||||
lastPushedWorld.put(player.getUniqueId(), world);
|
||||
UUID uuid = player.getUniqueId(); String name = player.getName();
|
||||
httpExecutor.execute(() -> {
|
||||
submitHttp(() -> {
|
||||
try {
|
||||
sendPost(statusApiUrl + "/player/world",
|
||||
"{\"uuid\":\"" + uuid + "\",\"name\":\"" + escapeName(name)
|
||||
@@ -241,7 +330,7 @@ public class StatusAPIBridge extends JavaPlugin implements Listener {
|
||||
} catch (Exception e) {
|
||||
getLogger().warning("World-Push fehlgeschlagen: " + e.getMessage());
|
||||
}
|
||||
});
|
||||
}, "world");
|
||||
}
|
||||
|
||||
private void pushPlayerDataIfChanged(Player player) {
|
||||
@@ -258,7 +347,7 @@ public class StatusAPIBridge extends JavaPlugin implements Listener {
|
||||
lastPushedData.put(player.getUniqueId(), key);
|
||||
UUID uuid = player.getUniqueId();
|
||||
String name = player.getName();
|
||||
httpExecutor.execute(() -> {
|
||||
submitHttp(() -> {
|
||||
try {
|
||||
sendPost(statusApiUrl + "/player/data",
|
||||
"{\"uuid\":\"" + uuid + "\",\"name\":\"" + escapeName(name)
|
||||
@@ -273,7 +362,7 @@ public class StatusAPIBridge extends JavaPlugin implements Listener {
|
||||
} catch (Exception e) {
|
||||
getLogger().warning("PlayerData-Push fehlgeschlagen: " + e.getMessage());
|
||||
}
|
||||
});
|
||||
}, "player-data");
|
||||
}
|
||||
|
||||
private void pushStatsIfChanged(Player player) {
|
||||
@@ -289,7 +378,7 @@ public class StatusAPIBridge extends JavaPlugin implements Listener {
|
||||
|
||||
UUID uuid = player.getUniqueId();
|
||||
String name = player.getName();
|
||||
httpExecutor.execute(() -> {
|
||||
submitHttp(() -> {
|
||||
try {
|
||||
sendPost(statusApiUrl + "/stats/update",
|
||||
"{\"uuid\":\"" + uuid + "\",\"name\":\"" + escapeName(name)
|
||||
@@ -299,7 +388,7 @@ public class StatusAPIBridge extends JavaPlugin implements Listener {
|
||||
} catch (Exception e) {
|
||||
getLogger().warning("Stats-Push fehlgeschlagen: " + e.getMessage());
|
||||
}
|
||||
});
|
||||
}, "stats");
|
||||
}
|
||||
|
||||
// ── TicketSystem Push ──────────────────────────────────────────────────
|
||||
@@ -368,35 +457,291 @@ public class StatusAPIBridge extends JavaPlugin implements Listener {
|
||||
}
|
||||
}
|
||||
|
||||
// ── PlaceholderAPI ────────────────────────────────────────────────────────
|
||||
|
||||
private boolean fetchPapiTokensFromStatusAPI() {
|
||||
try {
|
||||
@SuppressWarnings("deprecation")
|
||||
java.net.URL url = new java.net.URI(statusApiUrl + "/papi/tokens").toURL();
|
||||
java.net.HttpURLConnection c = (java.net.HttpURLConnection) url.openConnection();
|
||||
c.setRequestMethod("GET");
|
||||
c.setConnectTimeout(3000);
|
||||
c.setReadTimeout(3000);
|
||||
if (c.getResponseCode() != 200) { c.disconnect(); return false; }
|
||||
java.io.InputStream is = c.getInputStream();
|
||||
StringBuilder sb = new StringBuilder();
|
||||
int ch; while ((ch = is.read()) != -1) sb.append((char) ch);
|
||||
c.disconnect();
|
||||
String body = sb.toString().trim();
|
||||
papiTokens.clear();
|
||||
if (body.startsWith("[") && body.endsWith("]")) {
|
||||
String inner = body.substring(1, body.length() - 1).trim();
|
||||
if (!inner.isEmpty()) {
|
||||
int i = 0;
|
||||
while (i < inner.length()) {
|
||||
while (i < inner.length() && inner.charAt(i) != '"') i++;
|
||||
if (i >= inner.length()) break;
|
||||
i++;
|
||||
StringBuilder token = new StringBuilder();
|
||||
while (i < inner.length() && inner.charAt(i) != '"') {
|
||||
char c2 = inner.charAt(i++);
|
||||
if (c2 == '\\' && i < inner.length()) c2 = inner.charAt(i++);
|
||||
token.append(c2);
|
||||
}
|
||||
i++;
|
||||
if (token.length() > 0) papiTokens.add(token.toString());
|
||||
}
|
||||
}
|
||||
}
|
||||
return true;
|
||||
} catch (Exception e) { return false; }
|
||||
}
|
||||
|
||||
private void syncPapiValues() {
|
||||
if (!papiEnabled || papiTokens.isEmpty()) return;
|
||||
for (Player p : Bukkit.getOnlinePlayers()) pushPapiValues(p);
|
||||
}
|
||||
|
||||
private void pushPapiValues(Player p) {
|
||||
try {
|
||||
Class<?> papiClass = Class.forName("me.clip.placeholderapi.PlaceholderAPI");
|
||||
java.lang.reflect.Method setPlaceholders = papiClass.getMethod("setPlaceholders", Player.class, String.class);
|
||||
StringBuilder jsonValues = new StringBuilder();
|
||||
for (String token : papiTokens) {
|
||||
String resolved = (String) setPlaceholders.invoke(null, p, "%" + token + "%");
|
||||
if (resolved == null) resolved = "";
|
||||
if (jsonValues.length() > 0) jsonValues.append(",");
|
||||
jsonValues.append("\"").append(esc(token)).append("\":\"").append(esc(resolved)).append("\"");
|
||||
}
|
||||
String snapshot = jsonValues.toString();
|
||||
if (snapshot.equals(lastPapiValues.get(p.getUniqueId()))) return;
|
||||
lastPapiValues.put(p.getUniqueId(), snapshot);
|
||||
String json = "{\"uuid\":\"" + p.getUniqueId() + "\",\"placeholders\":{" + snapshot + "}}";
|
||||
submitHttp(() -> {
|
||||
try { sendPost(statusApiUrl + "/player/papi", json); }
|
||||
catch (Exception e) { getLogger().warning("[PAPI] Push fehlgeschlagen: " + e.getMessage()); }
|
||||
}, "papi");
|
||||
} catch (ClassNotFoundException ignored) {
|
||||
} catch (Exception e) { getLogger().warning("[PAPI] Fehler: " + e.getMessage()); }
|
||||
}
|
||||
|
||||
private static String esc(String s) {
|
||||
if (s == null) return "";
|
||||
// BUGFIX: Steuerzeichen escapen (JSON-Injection-Schutz für PAPI-Werte)
|
||||
return s.replace("\\", "\\\\")
|
||||
.replace("\"", "\\\"")
|
||||
.replace("\n", "\\n")
|
||||
.replace("\r", "\\r")
|
||||
.replace("\t", "\\t");
|
||||
}
|
||||
|
||||
private void pushTpsAsync(UUID uuid, double tps) {
|
||||
httpExecutor.execute(() -> {
|
||||
submitHttp(() -> {
|
||||
try {
|
||||
sendPost(statusApiUrl + "/scoreboard/tps",
|
||||
"{\"uuid\":\"" + uuid + "\",\"tps\":" + tps + "}");
|
||||
} catch (Exception ignored) {}
|
||||
});
|
||||
}, "tps");
|
||||
}
|
||||
|
||||
// ── Plugin-Message-Handler ───────────────────────────────────────────────
|
||||
|
||||
/**
|
||||
* Empfängt AFK-Status-Updates von BungeeCord (AfkModule).
|
||||
* Channel: "statusapi:afk", Format: "UUID:true" oder "UUID:false"
|
||||
* Aktualisiert den Nametag-Prefix des Spielers sofort.
|
||||
*/
|
||||
@Override
|
||||
public void onPluginMessageReceived(String channel, Player unused, byte[] message) {
|
||||
if (!"statusapi:afk".equals(channel)) return;
|
||||
try {
|
||||
String msg = new String(message, java.nio.charset.StandardCharsets.UTF_8);
|
||||
int sep = msg.lastIndexOf(':');
|
||||
if (sep < 0) return;
|
||||
UUID uuid = UUID.fromString(msg.substring(0, sep));
|
||||
boolean afk = Boolean.parseBoolean(msg.substring(sep + 1));
|
||||
afkState.put(uuid, afk);
|
||||
// Nametag sofort aktualisieren wenn Spieler auf diesem Server ist
|
||||
Player target = Bukkit.getPlayer(uuid);
|
||||
if (target != null && target.isOnline() && nametagEnabled) {
|
||||
// Muss im Hauptthread laufen (Scoreboard-API ist nicht thread-safe)
|
||||
Bukkit.getScheduler().runTask(this, () -> applyNametag(target));
|
||||
}
|
||||
} catch (Exception e) {
|
||||
getLogger().warning("[StatusAPIBridge] Fehler beim Verarbeiten der AFK-Message: " + e.getMessage());
|
||||
}
|
||||
}
|
||||
|
||||
// ── Nametag-Methoden ──────────────────────────────────────────────────────
|
||||
|
||||
/**
|
||||
* Setzt den LuckPerms-Prefix als Nametag über dem Spieler-Kopf.
|
||||
* Nutzt die Bukkit Scoreboard Team API – zuverlässig auf allen Spigot/Paper-Versionen.
|
||||
* Wird bei Join und periodisch (scoreboard-sync) aufgerufen.
|
||||
*/
|
||||
@SuppressWarnings("deprecation")
|
||||
private void applyNametag(Player player) {
|
||||
if (!nametagEnabled || nametagBoard == null) return;
|
||||
String rawPrefix = getLuckPermsPrefix(player);
|
||||
// AFK-Prefix ERSETZT den Rang-Prefix (nicht voranstellen). Muss identisch zu
|
||||
// ScoreboardModule.updateNametag() im StatusAPI-Proxy sein: beide Systeme
|
||||
// setzen Nametag-Teams, und je nachdem wer zuletzt geschrieben hat gewinnt
|
||||
// eines von beiden. Bei unterschiedlichen Formaten sieht der Spieler sonst
|
||||
// mal "[AFK] Rang Name" und mal "[AFK] Name".
|
||||
// Ergebnis hier nach dem angehaengten Leerzeichen: "§7[AFK]§r " – exakt wie
|
||||
// "§7[AFK] §r" auf der Proxy-Seite.
|
||||
boolean isAfk = Boolean.TRUE.equals(afkState.get(player.getUniqueId()));
|
||||
String prefix = isAfk ? "&7[AFK]&r" : rawPrefix;
|
||||
|
||||
// Change-Detection: nicht neu setzen wenn Prefix gleich geblieben
|
||||
String last = lastNametagPrefix.get(player.getUniqueId());
|
||||
if (prefix.equals(last)) return;
|
||||
lastNametagPrefix.put(player.getUniqueId(), prefix);
|
||||
|
||||
// Team-Name: "vnt_" + erste 12 Zeichen der UUID (ohne Bindestriche)
|
||||
// Minecraft-Limit: 16 Zeichen für Teamnamen
|
||||
String teamName = "vnt" + player.getUniqueId().toString().replace("-", "").substring(0, 13);
|
||||
|
||||
try {
|
||||
// Bestehendes Team holen oder neu erstellen
|
||||
org.bukkit.scoreboard.Team team = nametagBoard.getTeam(teamName);
|
||||
if (team == null) {
|
||||
team = nametagBoard.registerNewTeam(teamName);
|
||||
}
|
||||
|
||||
// Prefix setzen (Bukkit konvertiert §-Codes automatisch)
|
||||
String coloredPrefix = org.bukkit.ChatColor.translateAlternateColorCodes('&', prefix) + " ";
|
||||
team.setPrefix(coloredPrefix);
|
||||
team.setSuffix("");
|
||||
|
||||
// Spieler dem Team zuweisen
|
||||
if (!team.hasEntry(player.getName())) {
|
||||
team.addEntry(player.getName());
|
||||
}
|
||||
|
||||
// BUGFIX: Scoreboard NUR dem betroffenen Spieler selbst zuweisen.
|
||||
// Das vorherige Verhalten hat bei jedem Nametag-Update das Scoreboard ALLER
|
||||
// Online-Spieler überschrieben, was Scoreboards anderer Plugins (Sidebar etc.) zerstört.
|
||||
// Clients sehen Nametag-Teams korrekt sobald BEIDE Spieler dasselbe Scoreboard haben –
|
||||
// das wird beim Join jedes neuen Spielers via onJoin() automatisch gesetzt.
|
||||
player.setScoreboard(nametagBoard);
|
||||
} catch (Exception e) {
|
||||
getLogger().warning("[Nametag] Fehler beim Setzen des Prefixes für " + player.getName() + ": " + e.getMessage());
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Entfernt den Spieler aus seinem Nametag-Team beim Disconnect.
|
||||
*/
|
||||
private void removeNametag(Player player) {
|
||||
if (nametagBoard == null) return;
|
||||
String teamName = "vnt" + player.getUniqueId().toString().replace("-", "").substring(0, 13);
|
||||
try {
|
||||
org.bukkit.scoreboard.Team team = nametagBoard.getTeam(teamName);
|
||||
if (team != null) {
|
||||
if (team.hasEntry(player.getName())) {
|
||||
team.removeEntry(player.getName());
|
||||
}
|
||||
// Team löschen wenn leer
|
||||
if (team.getEntries().isEmpty()) team.unregister();
|
||||
}
|
||||
} catch (Exception ignored) {}
|
||||
}
|
||||
|
||||
/**
|
||||
* Holt den LuckPerms-Prefix eines Spielers via Reflection (keine harte Dependency).
|
||||
* Gibt leeren String zurück wenn LuckPerms nicht vorhanden oder kein Prefix gesetzt.
|
||||
*/
|
||||
private String getLuckPermsPrefix(Player player) {
|
||||
try {
|
||||
Class<?> provClass = Class.forName("net.luckperms.api.LuckPermsProvider");
|
||||
Object api = provClass.getMethod("get").invoke(null);
|
||||
Object um = api.getClass().getMethod("getUserManager").invoke(api);
|
||||
Object usr = um.getClass().getMethod("getUser", UUID.class).invoke(um, player.getUniqueId());
|
||||
if (usr == null) return "";
|
||||
Class<?> qoClass = Class.forName("net.luckperms.api.query.QueryOptions");
|
||||
Object opts = qoClass.getMethod("defaultContextualOptions").invoke(null);
|
||||
Object cache = usr.getClass().getMethod("getCachedData").invoke(usr);
|
||||
Object meta = cache.getClass().getMethod("getMetaData", qoClass).invoke(cache, opts);
|
||||
Object pfx = meta.getClass().getMethod("getPrefix").invoke(meta);
|
||||
if (pfx != null && !pfx.toString().isEmpty()) return pfx.toString();
|
||||
} catch (ClassNotFoundException ignored) {
|
||||
// LuckPerms nicht installiert
|
||||
} catch (Exception e) {
|
||||
getLogger().warning("[Nametag] LuckPerms-Prefix konnte nicht gelesen werden: " + e.getMessage());
|
||||
}
|
||||
return "";
|
||||
}
|
||||
|
||||
// ── Hilfsmethoden ─────────────────────────────────────────────────────────
|
||||
|
||||
/** TPS – Paper-API zuerst, dann Spigot-Reflection-Fallback */
|
||||
/**
|
||||
* Erkennt beim Start die Server-Version und setzt den internen Modus.
|
||||
* Sichtbar im Server-Log als [StatusAPIBridge] Versions-Modus: ...
|
||||
*/
|
||||
private void detectMinecraftVersion() {
|
||||
String bukkitVersion = Bukkit.getBukkitVersion(); // z.B. "1.21.1-R0.1-SNAPSHOT" oder "26.1.2-R0.1-SNAPSHOT"
|
||||
// Alles ab 26.x gilt als "neuer Modus" ohne NMS-Fallback
|
||||
try {
|
||||
String major = bukkitVersion.split("\\.")[0];
|
||||
int majorVersion = Integer.parseInt(major);
|
||||
isLegacyMode = majorVersion < 26;
|
||||
} catch (Exception e) {
|
||||
isLegacyMode = true; // Fallback: sicherer Legacy-Modus
|
||||
}
|
||||
getLogger().info("Versions-Modus: "
|
||||
+ (isLegacyMode ? "1.21.x-Modus (NMS-Fallback aktiv)" : "26.1.x-Modus (kein NMS-Fallback)")
|
||||
+ " | BukkitVersion: " + bukkitVersion);
|
||||
}
|
||||
|
||||
/**
|
||||
* TPS auslesen – kompatibel mit Paper 1.21+, Spigot 1.21+, Java 17/21.
|
||||
* Reihenfolge:
|
||||
* 1. Paper-API: getTPS() direkt auf dem Server (sauberster Weg)
|
||||
* 2. Spigot-Reflection: recentTps-Feld auf dem NMS-MinecraftServer
|
||||
* 3. Fallback: 20.0
|
||||
*/
|
||||
private double getCurrentTps() {
|
||||
// 1. Bevorzugt: Bukkit.getTPS() – funktioniert auf beiden Versionen
|
||||
try {
|
||||
double[] tps = (double[]) Bukkit.getServer().getClass()
|
||||
.getMethod("getTPS").invoke(Bukkit.getServer());
|
||||
return Math.min(20.0, tps[0]);
|
||||
} catch (Exception ignored) {}
|
||||
try {
|
||||
Object ms = Bukkit.getServer().getClass()
|
||||
.getMethod("getServer").invoke(Bukkit.getServer());
|
||||
double[] tps = (double[]) ms.getClass().getField("recentTps").get(ms);
|
||||
return Math.min(20.0, tps[0]);
|
||||
if (tps != null && tps.length > 0) return Math.min(20.0, tps[0]);
|
||||
} catch (Exception ignored) {}
|
||||
|
||||
// 2. NMS-Reflection-Fallback – nur im 1.21.x-Modus
|
||||
// Auf 26.1.x schlägt recentTps fehl → wird bewusst übersprungen
|
||||
if (isLegacyMode) {
|
||||
try {
|
||||
Object nmsServer = Bukkit.getServer().getClass()
|
||||
.getMethod("getServer").invoke(Bukkit.getServer());
|
||||
for (String fieldName : new String[]{"recentTps", "tps"}) {
|
||||
try {
|
||||
java.lang.reflect.Field f = nmsServer.getClass().getField(fieldName);
|
||||
Object val = f.get(nmsServer);
|
||||
if (val instanceof double[]) {
|
||||
double[] tps = (double[]) val;
|
||||
if (tps.length > 0) return Math.min(20.0, tps[0]);
|
||||
}
|
||||
} catch (NoSuchFieldException ignored2) {}
|
||||
}
|
||||
} catch (Exception ignored) {}
|
||||
}
|
||||
|
||||
return 20.0;
|
||||
}
|
||||
|
||||
private void submitHttp(Runnable task, String kind) {
|
||||
try {
|
||||
httpExecutor.execute(task);
|
||||
} catch (RejectedExecutionException ex) {
|
||||
getLogger().warning("HTTP-Queue voll, verwerfe " + kind + " Push.");
|
||||
}
|
||||
}
|
||||
|
||||
private void sendPost(String urlStr, String json) throws Exception {
|
||||
URL url = new URL(urlStr);
|
||||
@SuppressWarnings("deprecation")
|
||||
URL url = new java.net.URI(urlStr).toURL();
|
||||
HttpURLConnection conn = (HttpURLConnection) url.openConnection();
|
||||
conn.setRequestMethod("POST");
|
||||
conn.setDoOutput(true);
|
||||
@@ -414,6 +759,11 @@ public class StatusAPIBridge extends JavaPlugin implements Listener {
|
||||
|
||||
private String escapeName(String name) {
|
||||
if (name == null) return "";
|
||||
return name.replace("\\", "\\\\").replace("\"", "\\\"");
|
||||
// BUGFIX: Neben \ und " auch Steuerzeichen escapen (JSON-Injection-Schutz)
|
||||
return name.replace("\\", "\\\\")
|
||||
.replace("\"", "\\\"")
|
||||
.replace("\n", "\\n")
|
||||
.replace("\r", "\\r")
|
||||
.replace("\t", "\\t");
|
||||
}
|
||||
}
|
||||
@@ -10,3 +10,7 @@ live-sync-interval-ticks: 20
|
||||
# Sync-Intervall fuer Scoreboard-Daten (Health, Compass, TPS, World) in Ticks (mind. 20)
|
||||
# Compass und Health werden zusaetzlich event-basiert aktualisiert.
|
||||
scoreboard-sync-interval-ticks: 20
|
||||
|
||||
# Nametag: LuckPerms-Prefix ueber dem Spieler-Kopf anzeigen
|
||||
# Auf false setzen zum Deaktivieren.
|
||||
nametag-enabled: true
|
||||
|
||||
@@ -1,7 +1,8 @@
|
||||
name: StatusAPIBridge
|
||||
version: 1.0.0
|
||||
version: 1.0.2
|
||||
main: net.viper.statusapibridge.StatusAPIBridge
|
||||
# 1.21 als niedrigste gemeinsame Basis – wird von 1.21.1 und 26.1.2 akzeptiert
|
||||
api-version: 1.21
|
||||
description: Sendet Vault-Economy-Daten an die BungeeCord StatusAPI
|
||||
description: Sendet Spielerdaten an die BungeeCord StatusAPI
|
||||
authors: [Viper]
|
||||
softdepend: [Vault]
|
||||
softdepend: [Vault, PlaceholderAPI]
|
||||
|
||||
Reference in New Issue
Block a user