diff --git a/StatusAPI/lib/BungeeCord.jar b/StatusAPI/lib/BungeeCord.jar
new file mode 100644
index 0000000..c146a5a
Binary files /dev/null and b/StatusAPI/lib/BungeeCord.jar differ
diff --git a/StatusAPI/pom.xml b/StatusAPI/pom.xml
new file mode 100644
index 0000000..785f063
--- /dev/null
+++ b/StatusAPI/pom.xml
@@ -0,0 +1,107 @@
+
+
+ 4.0.0
+
+ net.viper.bungee
+ StatusAPI
+ 4.1.0
+ jar
+
+ StatusAPI
+ BungeeCord Status API Plugin
+
+
+ 8
+ 8
+ UTF-8
+
+
+
+
+
+ net.md-5
+ bungeecord-api
+ 1.20
+ system
+ ${project.basedir}/lib/BungeeCord.jar
+
+
+
+
+ net.luckperms
+ api
+ 5.4
+ provided
+ true
+
+
+
+
+ com.zaxxer
+ HikariCP
+ 5.1.0
+ compile
+
+
+
+
+ com.mysql
+ mysql-connector-j
+ 9.1.0
+ compile
+
+
+
+
+
+ central
+ https://repo.maven.apache.org/maven2
+
+
+ luckperms
+ https://repo.luckperms.net/releases/
+
+
+
+
+ StatusAPI
+
+
+
+ src/main/resources
+ false
+
+
+
+
+
+ org.apache.maven.plugins
+ maven-shade-plugin
+ 3.5.0
+
+
+ package
+ shade
+
+ false
+
+
+ com.zaxxer.hikari
+ net.viper.status.hikari
+
+
+ com.mysql
+ net.viper.status.mysql
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/StatusAPI/src/main/java/net/viper/status/StatusAPI.java b/StatusAPI/src/main/java/net/viper/status/StatusAPI.java
new file mode 100644
index 0000000..f1ed9a9
--- /dev/null
+++ b/StatusAPI/src/main/java/net/viper/status/StatusAPI.java
@@ -0,0 +1,1050 @@
+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.plugin.Plugin;
+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.antibot.AntiBotModule;
+import net.viper.status.modules.network.NetworkInfoModule;
+import net.viper.status.modules.AutoMessage.AutoMessageModule;
+import net.viper.status.modules.customcommands.CustomCommandModule;
+import net.viper.status.modules.serverswitcher.ServerSwitcherModule;
+import net.viper.status.stats.PlayerStats;
+import net.viper.status.stats.StatsModule;
+import net.viper.status.modules.verify.VerifyModule;
+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 java.io.BufferedReader;
+import java.io.File;
+import java.io.FileInputStream;
+import java.io.IOException;
+import java.io.InputStreamReader;
+import java.io.OutputStream;
+import java.io.StringReader;
+import java.net.InetSocketAddress;
+import java.net.ServerSocket;
+import java.net.Socket;
+import java.net.SocketTimeoutException;
+import java.nio.charset.StandardCharsets;
+import java.nio.file.Files;
+import java.util.*;
+import java.util.concurrent.ConcurrentHashMap;
+import java.util.concurrent.ExecutorService;
+import java.util.concurrent.Executors;
+import java.util.concurrent.RejectedExecutionException;
+import java.util.concurrent.TimeUnit;
+import java.util.concurrent.atomic.AtomicLong;
+import net.md_5.bungee.api.scheduler.ScheduledTask;
+
+/**
+ * StatusAPI - zentraler BungeeCord HTTP-Status- und Broadcast-Endpunkt
+ */
+public class StatusAPI extends Plugin implements Runnable {
+
+ // Welt pro Spieler (UUID -> Weltname), wird von StatusAPIBridge gepusht
+ public static final ConcurrentHashMap playerWorlds = new ConcurrentHashMap<>();
+
+ private volatile Thread thread;
+ private volatile ServerSocket serverSocket;
+ private volatile boolean shuttingDown = false;
+ private int port = 9191;
+ private ScheduledTask httpWatchdogTask;
+ private ExecutorService requestExecutor;
+ private final AtomicLong lastHttpRequestAt = new AtomicLong(0L);
+
+ private ModuleManager moduleManager;
+ private UpdateChecker updateChecker;
+ private Properties verifyProperties;
+
+ @Override
+ public void onEnable() {
+
+ if (!getDataFolder().exists()) {
+ getDataFolder().mkdirs();
+ }
+
+ mergeVerifyConfig();
+
+ // Port aus verify.properties lesen
+ String portStr = verifyProperties != null ? verifyProperties.getProperty("statusapi.port", "9191") : "9191";
+ try {
+ port = Integer.parseInt(portStr);
+ } catch (NumberFormatException e) {
+ getLogger().warning("Ungültiger Port in verify.properties, nutze Standard-Port 9191.");
+ port = 9191;
+ }
+
+ moduleManager = new ModuleManager();
+
+ // Module in korrekter Reihenfolge registrieren
+ // VanishModule MUSS vor ChatModule registriert werden (VanishProvider-Abhängigkeit)
+ moduleManager.registerModule(new StatsModule());
+ moduleManager.registerModule(new VerifyModule());
+ moduleManager.registerModule(new BroadcastModule());
+ moduleManager.registerModule(new CommandBlockerModule());
+ moduleManager.registerModule(new VanishModule());
+ moduleManager.registerModule(new ChatModule());
+ moduleManager.registerModule(new AntiBotModule());
+ moduleManager.registerModule(new NetworkInfoModule());
+ moduleManager.registerModule(new AutoMessageModule());
+ moduleManager.registerModule(new CustomCommandModule());
+ moduleManager.registerModule(new ServerSwitcherModule());
+ moduleManager.registerModule(new EconomyModule());
+ moduleManager.registerModule(new TablistModule());
+
+ try {
+ Class> forumBridge = Class.forName("net.viper.status.modules.forum.ForumBridgeModule");
+ Object forumBridgeInstance = forumBridge.getDeclaredConstructor().newInstance();
+ moduleManager.registerModule((net.viper.status.module.Module) forumBridgeInstance);
+ } catch (Exception e) {
+ getLogger().warning("ForumBridgeModule konnte nicht geladen werden: " + e.getMessage());
+ }
+
+ moduleManager.enableAll(this);
+
+ // WebServer starten
+ shuttingDown = false;
+ requestExecutor = Executors.newFixedThreadPool(4, r -> {
+ Thread t = new Thread(r, "StatusAPI-HTTP-Worker");
+ t.setDaemon(true);
+ return t;
+ });
+ startHttpServerThread();
+ httpWatchdogTask = ProxyServer.getInstance().getScheduler().schedule(
+ this, this::ensureHttpServerAlive, 15, 15, TimeUnit.SECONDS);
+
+ // Update-Checker
+ String currentVersion = getDescription() != null ? getDescription().getVersion() : "0.0.0";
+ updateChecker = new UpdateChecker(this, currentVersion, 6);
+ checkAndMaybeUpdate();
+ ProxyServer.getInstance().getScheduler().schedule(this, this::checkAndMaybeUpdate, 6, 6, TimeUnit.HOURS);
+ }
+
+ @Override
+ public void onDisable() {
+ shuttingDown = true;
+ if (moduleManager != null) {
+ moduleManager.disableAll(this);
+ }
+ if (httpWatchdogTask != null) {
+ httpWatchdogTask.cancel();
+ httpWatchdogTask = null;
+ }
+ stopHttpServerThread();
+ if (requestExecutor != null) {
+ requestExecutor.shutdownNow();
+ requestExecutor = null;
+ }
+ }
+
+ private synchronized void startHttpServerThread() {
+ if (thread != null && thread.isAlive()) {
+ return;
+ }
+ thread = new Thread(this, "StatusAPI-HTTP-Server");
+ thread.setDaemon(true);
+ thread.start();
+ }
+
+ private synchronized void stopHttpServerThread() {
+ Thread localThread = thread;
+ if (localThread != null) {
+ localThread.interrupt();
+ }
+ ServerSocket localServerSocket = serverSocket;
+ if (localServerSocket != null) {
+ try {
+ localServerSocket.close();
+ } catch (IOException ignored) {
+ }
+ }
+ if (localThread != null) {
+ try {
+ localThread.join(1500);
+ } catch (InterruptedException ignored) {
+ Thread.currentThread().interrupt();
+ }
+ }
+ thread = null;
+ }
+
+ private void ensureHttpServerAlive() {
+ if (shuttingDown) return;
+ Thread t = thread;
+ if (t == null || !t.isAlive()) {
+ getLogger().warning("HTTP-Server-Thread war gestoppt und wird neu gestartet.");
+ startHttpServerThread();
+ }
+ }
+
+ // --- MERGE LOGIK ---
+ private void mergeVerifyConfig() {
+ try {
+ File file = new File(getDataFolder(), "verify.properties");
+ verifyProperties = new Properties();
+ if (file.exists()) {
+ try (FileInputStream fis = new FileInputStream(file)) {
+ verifyProperties.load(fis);
+ }
+ } else {
+ getLogger().warning("verify.properties nicht gefunden.");
+ }
+ } catch (IOException e) {
+ getLogger().severe("Fehler beim Laden der verify.properties: " + e.getMessage());
+ }
+ }
+
+ public Properties getVerifyProperties() {
+ synchronized (this) {
+ return verifyProperties;
+ }
+ }
+
+ public ModuleManager getModuleManager() {
+ return moduleManager;
+ }
+
+ // --- Update-Logik ---
+ private void checkAndMaybeUpdate() {
+ try {
+ 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("----------------------------------------");
+ }
+ } catch (Exception e) {
+ getLogger().severe("Fehler beim Update-Check: " + e.getMessage());
+ }
+ }
+
+ // --- WebServer ---
+ @Override
+ public void run() {
+ while (!shuttingDown && !Thread.currentThread().isInterrupted()) {
+ // FIX #1: reuseAddress muss VOR dem bind() gesetzt werden.
+ // new ServerSocket(port) bindet sofort → stattdessen unboundenen Socket anlegen.
+ ServerSocket localServerSocket = null;
+ try {
+ localServerSocket = new ServerSocket();
+ localServerSocket.setReuseAddress(true);
+ localServerSocket.setSoTimeout(1000);
+ localServerSocket.bind(new InetSocketAddress(port));
+ this.serverSocket = localServerSocket;
+
+ while (!shuttingDown && !Thread.currentThread().isInterrupted()) {
+ try {
+ Socket clientSocket = localServerSocket.accept();
+ submitConnection(clientSocket);
+ } catch (SocketTimeoutException ignored) {
+ // Poll-Schleife für Interrupt/Shutdown
+ } catch (IOException e) {
+ if (!shuttingDown) {
+ getLogger().warning("Fehler beim Akzeptieren einer Verbindung: " + e.getMessage());
+ }
+ } catch (Throwable t) {
+ if (!shuttingDown) {
+ getLogger().severe("Unbehandelter Fehler im HTTP-Accept-Loop: " + t.getMessage());
+ }
+ }
+ }
+ } catch (IOException e) {
+ if (!shuttingDown) {
+ getLogger().severe("Konnte ServerSocket nicht starten: " + e.getMessage());
+ try {
+ Thread.sleep(2000L);
+ } catch (InterruptedException ignored) {
+ Thread.currentThread().interrupt();
+ }
+ }
+ } finally {
+ if (localServerSocket != null) {
+ try { localServerSocket.close(); } catch (IOException ignored) {}
+ }
+ serverSocket = null;
+ }
+ }
+ }
+
+ private void submitConnection(Socket clientSocket) {
+ if (clientSocket == null) return;
+ try {
+ clientSocket.setSoTimeout(5000);
+ clientSocket.setTcpNoDelay(true);
+ } catch (Exception ignored) {}
+
+ ExecutorService executor = requestExecutor;
+ if (executor == null || executor.isShutdown()) {
+ try { clientSocket.close(); } catch (IOException ignored) {}
+ return;
+ }
+
+ try {
+ executor.execute(() -> {
+ try {
+ handleConnection(clientSocket);
+ } finally {
+ try { clientSocket.close(); } catch (IOException ignored) {}
+ }
+ });
+ } catch (RejectedExecutionException ex) {
+ try { clientSocket.close(); } catch (IOException ignored) {}
+ }
+ }
+
+ private void handleConnection(Socket clientSocket) {
+ try (BufferedReader in = new BufferedReader(
+ new InputStreamReader(clientSocket.getInputStream(), StandardCharsets.UTF_8));
+ OutputStream out = clientSocket.getOutputStream()) {
+
+ String inputLine = in.readLine();
+ if (inputLine == null) return;
+ lastHttpRequestAt.set(System.currentTimeMillis());
+
+ String[] reqParts = inputLine.split(" ");
+ if (reqParts.length < 2) return;
+ String method = reqParts[0].trim();
+ String path = reqParts[1].trim();
+ String pathOnly = path;
+ int queryIndex = path.indexOf('?');
+ if (queryIndex >= 0) pathOnly = path.substring(0, queryIndex);
+
+ // GET /health
+ if ("GET".equalsIgnoreCase(method) && "/health".equalsIgnoreCase(path)) {
+ long lastMs = lastHttpRequestAt.get();
+ long age = lastMs <= 0L ? -1L : (System.currentTimeMillis() - lastMs);
+ sendHttpResponse(out, "{\"success\":true,\"online\":true,\"last_request_age_ms\":" + age + "}", 200);
+ return;
+ }
+
+ // GET /antibot/security-log
+ if ("GET".equalsIgnoreCase(method) && "/antibot/security-log".equalsIgnoreCase(pathOnly)) {
+ Map payload = new LinkedHashMap<>();
+ payload.put("success", true);
+ payload.put("events", loadAntiBotSecurityEvents(250));
+ sendHttpResponse(out, buildJsonString(payload), 200);
+ return;
+ }
+
+ // Headers lesen
+ Map headers = new HashMap<>();
+ String line;
+ while ((line = in.readLine()) != null && !line.isEmpty()) {
+ int idx = line.indexOf(':');
+ if (idx > 0) {
+ headers.put(line.substring(0, idx).trim().toLowerCase(Locale.ROOT),
+ line.substring(idx + 1).trim());
+ }
+ }
+
+ // GET /network/backendguard/config
+ if ("GET".equalsIgnoreCase(method) && path.equalsIgnoreCase("/network/backendguard/config")) {
+ Properties guardProps = loadNetworkGuardProperties();
+ String requiredApiKey = guardProps.getProperty("backendguard.sync.api_key", "").trim();
+ String providedApiKey = headers.getOrDefault("x-api-key", headers.getOrDefault("x-apikey", ""));
+ if (!requiredApiKey.isEmpty() && !requiredApiKey.equals(providedApiKey == null ? "" : providedApiKey.trim())) {
+ sendHttpResponse(out, "{\"success\":false,\"error\":\"bad_api_key\"}", 403);
+ return;
+ }
+ Map payload = new LinkedHashMap<>();
+ payload.put("success", true);
+ Map guard = new LinkedHashMap<>();
+ guard.put("enforcement_enabled", Boolean.parseBoolean(guardProps.getProperty("backendguard.enforcement_enabled", "true")));
+ guard.put("log_blocked_attempts", Boolean.parseBoolean(guardProps.getProperty("backendguard.log_blocked_attempts", "true")));
+ guard.put("kick_message", guardProps.getProperty("backendguard.kick_message", "&cBitte verbinde dich nur ueber den Proxy."));
+ guard.put("allowed_proxy_ips", parseCommaListProperty(guardProps.getProperty("backendguard.allowed_proxy_ips", "127.0.0.1,::1")));
+ guard.put("allowed_proxy_cidrs", parseCommaListProperty(guardProps.getProperty("backendguard.allowed_proxy_cidrs", "")));
+ payload.put("backend_guard", guard);
+ sendHttpResponse(out, buildJsonString(payload), 200);
+ return;
+ }
+
+ // POST /forum/notify
+ if ("POST".equalsIgnoreCase(method) && path.equalsIgnoreCase("/forum/notify")) {
+ String body = readBody(in, headers);
+ String apiKeyHeader = headers.getOrDefault("x-api-key", headers.getOrDefault("x-apikey", ""));
+ Object mod = moduleManager.getModule("ForumBridgeModule");
+ if (mod != null) {
+ try {
+ java.lang.reflect.Method m = mod.getClass().getMethod("handleNotify", String.class, String.class);
+ String resp = (String) m.invoke(mod, body, apiKeyHeader);
+ sendHttpResponse(out, resp, 200);
+ } catch (Exception e) {
+ e.printStackTrace();
+ sendHttpResponse(out, "{\"success\":false,\"error\":\"internal\"}", 500);
+ }
+ } else {
+ sendHttpResponse(out, "{\"success\":false,\"error\":\"no_forum_module\"}", 500);
+ }
+ return;
+ }
+
+ // POST /network/attack
+ if ("POST".equalsIgnoreCase(method) && path.equalsIgnoreCase("/network/attack")) {
+ String body = readBody(in, headers);
+ String apiKeyHeader = headers.getOrDefault("x-api-key", headers.getOrDefault("x-apikey", ""));
+ NetworkInfoModule mod = (NetworkInfoModule) moduleManager.getModule("NetworkInfoModule");
+ if (mod == null || !mod.isEnabled() || !mod.isAttackNotificationsEnabled()) {
+ sendHttpResponse(out, "{\"success\":false,\"error\":\"network_module_disabled\"}", 403);
+ return;
+ }
+ if (!mod.isAttackApiKeyValid(apiKeyHeader)) {
+ sendHttpResponse(out, "{\"success\":false,\"error\":\"bad_api_key\"}", 403);
+ return;
+ }
+ String eventType = extractJsonString(body, "event");
+ if (eventType == null || eventType.trim().isEmpty()) eventType = "detected";
+ String source = extractJsonString(body, "source");
+
+ Integer cps = null, blockedIps = null;
+ Long blockedConnections = null;
+ String cpsStr = extractJsonString(body, "connectionsPerSecond");
+ if (cpsStr == null || cpsStr.isEmpty()) cpsStr = extractJsonString(body, "cps");
+ try { if (cpsStr != null && !cpsStr.isEmpty()) cps = Integer.valueOf(cpsStr.trim()); } catch (Exception ignored) {}
+ String blockedIpsStr = extractJsonString(body, "ipAddressesBlocked");
+ if (blockedIpsStr == null || blockedIpsStr.isEmpty()) blockedIpsStr = extractJsonString(body, "blockedIps");
+ try { if (blockedIpsStr != null && !blockedIpsStr.isEmpty()) blockedIps = Integer.valueOf(blockedIpsStr.trim()); } catch (Exception ignored) {}
+ String blockedConnectionsStr = extractJsonString(body, "connectionsBlocked");
+ if (blockedConnectionsStr == null || blockedConnectionsStr.isEmpty()) blockedConnectionsStr = extractJsonString(body, "blockedConnections");
+ try { if (blockedConnectionsStr != null && !blockedConnectionsStr.isEmpty()) blockedConnections = Long.valueOf(blockedConnectionsStr.trim()); } catch (Exception ignored) {}
+
+ boolean sent = mod.sendAttackNotification(eventType, cps, blockedIps, blockedConnections, source);
+ sendHttpResponse(out, sent ? "{\"success\":true}" : "{\"success\":false,\"error\":\"webhook_disabled_or_missing\"}", sent ? 200 : 400);
+ return;
+ }
+
+ // POST /broadcast/cancel
+ if ("POST".equalsIgnoreCase(method) && (path.equalsIgnoreCase("/broadcast/cancel") || path.equalsIgnoreCase("/cancel"))) {
+ String body = readBody(in, headers);
+ String clientScheduleId = extractJsonString(body, "clientScheduleId");
+ if (clientScheduleId == null || clientScheduleId.isEmpty()) {
+ sendHttpResponse(out, "{\"success\":false,\"error\":\"missing_clientScheduleId\"}", 400);
+ return;
+ }
+ Object mod = moduleManager.getModule("BroadcastModule");
+ if (mod instanceof BroadcastModule) {
+ boolean ok = ((BroadcastModule) mod).cancelScheduled(clientScheduleId);
+ sendHttpResponse(out, ok ? "{\"success\":true}" : "{\"success\":false,\"error\":\"not_found\"}", ok ? 200 : 404);
+ } else {
+ sendHttpResponse(out, "{\"success\":false,\"error\":\"no_broadcast_module\"}", 500);
+ }
+ return;
+ }
+
+ // POST /broadcast
+ if ("POST".equalsIgnoreCase(method) && ("/broadcast".equalsIgnoreCase(path) || "/".equals(path) || path.isEmpty())) {
+ String body = readBody(in, headers);
+ String apiKeyHeader = headers.getOrDefault("x-api-key", headers.getOrDefault("x-apikey", ""));
+ String message = extractJsonString(body, "message");
+ String type = extractJsonString(body, "type");
+ String prefix = extractJsonString(body, "prefix");
+ String prefixColor = extractJsonString(body, "prefixColor");
+ String bracketColor = extractJsonString(body, "bracketColor");
+ String messageColor = extractJsonString(body, "messageColor");
+ String sourceName = extractJsonString(body, "source");
+ String scheduleTimeStr= extractJsonString(body, "scheduleTime");
+ String recur = extractJsonString(body, "recur");
+ String clientScheduleId = extractJsonString(body, "clientScheduleId");
+
+ if (sourceName == null || sourceName.isEmpty()) sourceName = "PulseCast";
+ if (type == null || type.isEmpty()) type = "global";
+
+ Object mod = moduleManager.getModule("BroadcastModule");
+ if (!(mod instanceof BroadcastModule)) {
+ sendHttpResponse(out, "{\"success\":false,\"error\":\"no_broadcast_module\"}", 500);
+ return;
+ }
+ BroadcastModule bm = (BroadcastModule) mod;
+
+ if (scheduleTimeStr != null && !scheduleTimeStr.trim().isEmpty()) {
+ long scheduleMillis;
+ try {
+ scheduleMillis = Long.parseLong(scheduleTimeStr.trim());
+ if (scheduleMillis < 1_000_000_000_000L) scheduleMillis *= 1000L;
+ } catch (NumberFormatException ignored) {
+ try {
+ long v = (long) Double.parseDouble(scheduleTimeStr.trim());
+ scheduleMillis = v < 1_000_000_000_000L ? v * 1000L : v;
+ } catch (Exception ex) {
+ sendHttpResponse(out, "{\"success\":false,\"error\":\"bad_scheduleTime\"}", 400);
+ return;
+ }
+ }
+ boolean ok = bm.scheduleBroadcast(scheduleMillis, sourceName, message, type, apiKeyHeader,
+ prefix, prefixColor, bracketColor, messageColor,
+ recur == null ? "none" : recur, clientScheduleId);
+ sendHttpResponse(out, ok ? "{\"success\":true}" : "{\"success\":false,\"error\":\"rejected\"}", ok ? 200 : 403);
+ return;
+ }
+
+ if (message == null || message.isEmpty()) {
+ sendHttpResponse(out, "{\"success\":false,\"error\":\"missing_message\"}", 400);
+ return;
+ }
+ boolean ok = bm.handleBroadcast(sourceName, message, type, apiKeyHeader, prefix, prefixColor, bracketColor, messageColor);
+ sendHttpResponse(out, ok ? "{\"success\":true}" : "{\"success\":false,\"error\":\"rejected\"}", ok ? 200 : 403);
+ return;
+ }
+
+ // GET /stats/player?uuid=... oder ?name=...
+ if ("GET".equalsIgnoreCase(method) && "/stats/player".equalsIgnoreCase(pathOnly)) {
+ Map qp = parseQueryParams(path);
+ StatsModule statsMod = (StatsModule) moduleManager.getModule("StatsModule");
+ PlayerStats ps = resolvePlayer(qp.get("uuid"), qp.get("name"), statsMod);
+ if (ps == null) {
+ sendHttpResponse(out, "{\"success\":false,\"error\":\"player_not_found\"}", 404);
+ return;
+ }
+ Map payload = new LinkedHashMap<>();
+ payload.put("success", true);
+ Map playerMap = new LinkedHashMap<>();
+ playerMap.put("uuid", ps.uuid.toString());
+ playerMap.put("name", ps.name);
+ playerMap.put("first_seen", ps.firstSeen);
+ playerMap.put("last_seen", ps.lastSeen);
+ playerMap.put("playtime", ps.getPlaytimeWithCurrentSession());
+ playerMap.put("joins", ps.joins);
+ playerMap.put("online", ProxyServer.getInstance().getPlayer(ps.uuid) != null);
+ // Balance direkt aus MySQL (serverübergreifend)
+ EconomyModule ecoModPlayer = (EconomyModule) moduleManager.getModule("EconomyModule");
+ double playerBalance = (ecoModPlayer != null && ecoModPlayer.getManager() != null)
+ ? ecoModPlayer.getManager().getBalance(ps.uuid)
+ : ps.balance;
+ Map economy = new LinkedHashMap<>();
+ economy.put("balance", playerBalance);
+ economy.put("total_earned", ps.totalEarned);
+ economy.put("total_spent", ps.totalSpent);
+ economy.put("transactions_count", ps.transactionsCount);
+ playerMap.put("economy", economy);
+ Map punishments = new LinkedHashMap<>();
+ punishments.put("bans", ps.bansCount);
+ punishments.put("mutes", ps.mutesCount);
+ punishments.put("warns", ps.warnsCount);
+ punishments.put("last_punishment_at", ps.lastPunishmentAt);
+ punishments.put("last_punishment_type", ps.lastPunishmentType != null ? ps.lastPunishmentType : "");
+ punishments.put("punishment_score", ps.punishmentScore);
+ playerMap.put("punishments", punishments);
+ payload.put("player", playerMap);
+ sendHttpResponse(out, buildJsonString(payload), 200);
+ return;
+ }
+
+ // GET /economy/player?uuid=... oder ?name=...
+ // Kein Cache – UUID und Balance kommen direkt aus der DB
+ if ("GET".equalsIgnoreCase(method) && "/economy/player".equalsIgnoreCase(pathOnly)) {
+ Map qp = parseQueryParams(path);
+ EconomyModule ecoModGet = (EconomyModule) moduleManager.getModule("EconomyModule");
+ if (ecoModGet == null || ecoModGet.getManager() == null) {
+ sendHttpResponse(out, "{\"success\":false,\"error\":\"economy_module_unavailable\"}", 503);
+ return;
+ }
+ // UUID auflösen: erst Query-Param, dann Name über EconomyManager (DB-Lookup)
+ UUID ecoUuid = null;
+ String ecoName = null;
+ String uuidParam = qp.get("uuid");
+ String nameParam = qp.get("name");
+ if (uuidParam != null && !uuidParam.isEmpty()) {
+ try { ecoUuid = UUID.fromString(uuidParam.trim()); } catch (IllegalArgumentException ignored) {}
+ }
+ if (ecoUuid == null && nameParam != null && !nameParam.isEmpty()) {
+ ecoUuid = ecoModGet.getManager().resolveUUID(nameParam.trim());
+ ecoName = nameParam.trim();
+ }
+ if (ecoUuid == null) {
+ sendHttpResponse(out, "{\"success\":false,\"error\":\"player_not_found\"}", 404);
+ return;
+ }
+ if (ecoName == null) ecoName = uuidParam;
+ double directBalance = ecoModGet.getManager().getBalance(ecoUuid);
+ Map payload = new LinkedHashMap<>();
+ payload.put("success", true);
+ payload.put("uuid", ecoUuid.toString());
+ payload.put("name", ecoName);
+ Map economy = new LinkedHashMap<>();
+ economy.put("balance", directBalance);
+ payload.put("economy", economy);
+ sendHttpResponse(out, buildJsonString(payload), 200);
+ return;
+ }
+
+ // POST /economy/update
+ // Kein Cache – Balance wird direkt in die DB geschrieben
+ if ("POST".equalsIgnoreCase(method) && "/economy/update".equalsIgnoreCase(pathOnly)) {
+ String body = readBody(in, headers);
+ EconomyModule ecoModUpd = (EconomyModule) moduleManager.getModule("EconomyModule");
+ if (ecoModUpd == null || ecoModUpd.getManager() == null) {
+ sendHttpResponse(out, "{\"success\":false,\"error\":\"economy_module_unavailable\"}", 503);
+ return;
+ }
+ // UUID auflösen
+ UUID ecoUpdUuid = null;
+ String uuidBody = extractJsonString(body, "uuid");
+ String nameBody = extractJsonString(body, "name");
+ if (uuidBody != null && !uuidBody.isEmpty()) {
+ try { ecoUpdUuid = UUID.fromString(uuidBody.trim()); } catch (IllegalArgumentException ignored) {}
+ }
+ if (ecoUpdUuid == null && nameBody != null && !nameBody.isEmpty()) {
+ ecoUpdUuid = ecoModUpd.getManager().resolveUUID(nameBody.trim());
+ }
+ if (ecoUpdUuid == null) {
+ sendHttpResponse(out, "{\"success\":false,\"error\":\"player_not_found\"}", 404);
+ return;
+ }
+ // Balance direkt in DB schreiben
+ String balStr = extractJsonString(body, "balance");
+ if (balStr != null && !balStr.isEmpty()) {
+ try {
+ double newBal = Double.parseDouble(balStr);
+ ecoModUpd.getManager().setBalance(ecoUpdUuid, newBal);
+ } catch (NumberFormatException ignored) {}
+ }
+ // Stats-Felder (total_earned etc.) im Cache aktualisieren, falls vorhanden
+ StatsModule statsModEco = (StatsModule) moduleManager.getModule("StatsModule");
+ if (statsModEco != null) {
+ PlayerStats psEco = statsModEco.getManager().getIfPresent(ecoUpdUuid);
+ if (psEco != null) {
+ String earnStr = extractJsonString(body, "total_earned");
+ if (earnStr == null) earnStr = extractJsonString(body, "totalEarned");
+ String spentStr = extractJsonString(body, "total_spent");
+ if (spentStr == null) spentStr = extractJsonString(body, "totalSpent");
+ String txStr = extractJsonString(body, "transactions_count");
+ if (txStr == null) txStr = extractJsonString(body, "transactionsCount");
+ synchronized (psEco) {
+ try { if (balStr != null && !balStr.isEmpty()) psEco.balance = Double.parseDouble(balStr); } catch (Exception ignored) {}
+ try { if (earnStr != null && !earnStr.isEmpty()) psEco.totalEarned = Double.parseDouble(earnStr); } catch (Exception ignored) {}
+ try { if (spentStr != null && !spentStr.isEmpty()) psEco.totalSpent = Double.parseDouble(spentStr); } catch (Exception ignored) {}
+ try { if (txStr != null && !txStr.isEmpty()) psEco.transactionsCount = Integer.parseInt(txStr); } catch (Exception ignored) {}
+ }
+ }
+ }
+ sendHttpResponse(out, "{\"success\":true}", 200);
+ return;
+ }
+
+ // POST /punishment/update
+ if ("POST".equalsIgnoreCase(method) && "/punishment/update".equalsIgnoreCase(pathOnly)) {
+ String body = readBody(in, headers);
+ StatsModule statsModPun = (StatsModule) moduleManager.getModule("StatsModule");
+ PlayerStats psPun = resolvePlayer(extractJsonString(body, "uuid"), extractJsonString(body, "name"), statsModPun);
+ if (psPun == null) {
+ sendHttpResponse(out, "{\"success\":false,\"error\":\"player_not_found\"}", 404);
+ return;
+ }
+ String bansStr = extractJsonString(body, "bans");
+ String mutesStr = extractJsonString(body, "mutes");
+ String warnsStr = extractJsonString(body, "warns");
+ String lastAtStr = extractJsonString(body, "last_punishment_at");
+ if (lastAtStr == null) lastAtStr = extractJsonString(body, "lastPunishmentAt");
+ String typeStr = extractJsonString(body, "last_punishment_type");
+ if (typeStr == null) typeStr = extractJsonString(body, "lastPunishmentType");
+ String scoreStr = extractJsonString(body, "punishment_score");
+ if (scoreStr == null) scoreStr = extractJsonString(body, "punishmentScore");
+ // Typ auf erlaubte Werte begrenzen
+ String safeType = null;
+ if (typeStr != null) {
+ String t = typeStr.trim().toLowerCase(Locale.ROOT);
+ safeType = (t.equals("ban") || t.equals("mute") || t.equals("warn") || t.equals("kick")) ? t : "";
+ }
+ synchronized (psPun) {
+ try { if (bansStr != null && !bansStr.isEmpty()) psPun.bansCount = Integer.parseInt(bansStr); } catch (Exception ignored) {}
+ try { if (mutesStr != null && !mutesStr.isEmpty()) psPun.mutesCount = Integer.parseInt(mutesStr); } catch (Exception ignored) {}
+ try { if (warnsStr != null && !warnsStr.isEmpty()) psPun.warnsCount = Integer.parseInt(warnsStr); } catch (Exception ignored) {}
+ try { if (lastAtStr != null && !lastAtStr.isEmpty()) psPun.lastPunishmentAt = Long.parseLong(lastAtStr); } catch (Exception ignored) {}
+ if (safeType != null) psPun.lastPunishmentType = safeType;
+ try { if (scoreStr != null && !scoreStr.isEmpty()) psPun.punishmentScore = Integer.parseInt(scoreStr); } catch (Exception ignored) {}
+ }
+ sendHttpResponse(out, "{\"success\":true}", 200);
+ return;
+ }
+
+ // POST /player/world – Welt eines Spielers aktualisieren (von StatusAPIBridge)
+ if ("POST".equalsIgnoreCase(method) && "/player/world".equalsIgnoreCase(pathOnly)) {
+ String body = readBody(in, headers);
+ String uuidStr = extractJsonString(body, "uuid");
+ String worldStr = extractJsonString(body, "world");
+ if (uuidStr != null && !uuidStr.isEmpty() && worldStr != null && !worldStr.isEmpty()) {
+ try {
+ playerWorlds.put(UUID.fromString(uuidStr.trim()), worldStr.trim());
+ } catch (IllegalArgumentException ignored) {}
+ }
+ sendHttpResponse(out, "{\"success\":true}", 200);
+ return;
+ }
+
+ // GET – Status-Endpunkt
+ if (inputLine.startsWith("GET")) {
+ Map data = new LinkedHashMap<>();
+ data.put("online", true);
+
+ String versionRaw = ProxyServer.getInstance().getVersion();
+ String versionClean = (versionRaw != null && versionRaw.contains(":")) ? versionRaw.split(":")[2].trim() : versionRaw;
+ data.put("version", versionClean);
+
+ int globalLimit = ProxyServer.getInstance().getConfig().getPlayerLimit();
+ if (globalLimit <= 0) {
+ try {
+ Iterator limIt = ProxyServer.getInstance().getConfig().getListeners().iterator();
+ if (limIt.hasNext()) {
+ int listenerMax = limIt.next().getMaxPlayers();
+ if (listenerMax > 0) globalLimit = listenerMax;
+ }
+ } catch (Exception ignored) {}
+ }
+ data.put("max_players", String.valueOf(globalLimit));
+
+ String motd = "BungeeCord";
+ try {
+ Iterator it = ProxyServer.getInstance().getConfig().getListeners().iterator();
+ if (it.hasNext()) motd = it.next().getMotd();
+ } catch (Exception ignored) {}
+ data.put("motd", motd);
+
+ StatsModule statsModule = (StatsModule) moduleManager.getModule("StatsModule");
+ boolean luckPermsEnabled = ProxyServer.getInstance().getPluginManager().getPlugin("LuckPerms") != null;
+ List