Dateien nach "src/main/java/net/viper/status/modules/network" hochladen

This commit is contained in:
2026-04-02 06:27:33 +00:00
parent 4ada8fcde6
commit 8e1aa67bbe

View File

@@ -0,0 +1,687 @@
package net.viper.status.modules.network;
import com.sun.management.OperatingSystemMXBean;
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.config.ServerInfo;
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.module.Module;
import java.io.File;
import java.io.FileInputStream;
import java.io.FileOutputStream;
import java.io.InputStreamReader;
import java.io.InputStream;
import java.io.OutputStream;
import java.net.HttpURLConnection;
import java.net.URL;
import java.nio.charset.StandardCharsets;
import java.lang.management.ManagementFactory;
import java.time.Instant;
import java.util.ArrayList;
import java.util.Collection;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Locale;
import java.util.Map;
import java.util.Properties;
import java.util.UUID;
import java.util.concurrent.TimeUnit;
/**
* Liefert erweiterte Proxy- und Systeminformationen für API und Ingame-Debug.
*/
public class NetworkInfoModule implements Module {
private static final String CONFIG_FILE_NAME = "network-guard.properties";
private Plugin plugin;
private long startedAtMillis;
private boolean enabled = true;
private boolean commandEnabled = true;
private boolean includePlayerNames = false;
private boolean webhookEnabled = false;
private String webhookUrl = "";
private String webhookUsername = "StatusAPI";
private String webhookThumbnailUrl = "";
private boolean webhookNotifyStartStop = true;
private String webhookEmbedMode = "detailed";
private int webhookCheckSeconds = 30;
private int alertMemoryPercent = 90;
private int alertPlayerPercent = 95;
private int alertCooldownSeconds = 300;
private boolean attackNotificationsEnabled = true;
private String attackApiKey = "";
private String attackDefaultSource = "BetterBungee";
private long lastMemoryAlertAt = 0L;
private long lastPlayerAlertAt = 0L;
@Override
public String getName() {
return "NetworkInfoModule";
}
@Override
public void onEnable(Plugin plugin) {
this.plugin = plugin;
this.startedAtMillis = System.currentTimeMillis();
ensureModuleConfigExists();
loadConfig();
if (!enabled) {
plugin.getLogger().info("[NetworkInfoModule] deaktiviert via " + CONFIG_FILE_NAME + " (networkinfo.enabled=false)");
return;
}
if (commandEnabled) {
ProxyServer.getInstance().getPluginManager().registerCommand(plugin, new NetInfoCommand());
}
if (webhookEnabled && !webhookUrl.isEmpty()) {
if (webhookNotifyStartStop) {
if (isCompactEmbedMode()) {
sendWebhookEmbed(
webhookUrl,
"✅ NetworkInfo gestartet",
"Proxy: **" + ProxyServer.getInstance().getName() + "**\nÜberwachung und Webhook-Alerts sind jetzt aktiv.",
0x2ECC71
);
} else {
StringBuilder fields = new StringBuilder();
appendEmbedField(fields, "Proxy", ProxyServer.getInstance().getName(), true);
appendEmbedField(fields, "Modus", "Detailed", true);
appendEmbedField(fields, "Check-Intervall", Math.max(10, webhookCheckSeconds) + "s", true);
sendWebhookEmbed(
webhookUrl,
"✅ NetworkInfo gestartet",
"Überwachung und Webhook-Alerts sind jetzt aktiv.",
0x2ECC71,
fields.toString()
);
}
}
int interval = Math.max(10, webhookCheckSeconds);
ProxyServer.getInstance().getScheduler().schedule(plugin, this::evaluateAndSendAlerts, interval, interval, TimeUnit.SECONDS);
}
plugin.getLogger().info("[NetworkInfoModule] aktiviert. commandEnabled=" + commandEnabled + ", includePlayerNames=" + includePlayerNames);
}
@Override
public void onDisable(Plugin plugin) {
if (enabled && webhookEnabled && webhookNotifyStartStop && webhookUrl != null && !webhookUrl.isEmpty()) {
if (isCompactEmbedMode()) {
sendWebhookEmbed(
webhookUrl,
"🛑 NetworkInfo gestoppt",
"Die NetworkInfo-Überwachung wurde gestoppt.\nKeine weiteren Auto-Alerts bis zum nächsten Start.",
0xE74C3C
);
} else {
StringBuilder fields = new StringBuilder();
appendEmbedField(fields, "Proxy", ProxyServer.getInstance().getName(), true);
appendEmbedField(fields, "Modus", "Detailed", true);
appendEmbedField(fields, "Status", "Monitoring pausiert", false);
sendWebhookEmbed(
webhookUrl,
"🛑 NetworkInfo gestoppt",
"Die NetworkInfo-Überwachung wurde gestoppt.",
0xE74C3C,
fields.toString()
);
}
}
}
public boolean isEnabled() {
return enabled;
}
public boolean isAttackNotificationsEnabled() {
return enabled && attackNotificationsEnabled;
}
public boolean isAttackApiKeyValid(String providedKey) {
if (attackApiKey == null || attackApiKey.isEmpty()) {
return true;
}
return providedKey != null && attackApiKey.equals(providedKey.trim());
}
public boolean sendAttackNotification(String eventType,
Integer connectionsPerSecond,
Integer blockedIps,
Long blockedConnections,
String source) {
if (!isAttackNotificationsEnabled()) {
return false;
}
String usedSource = (source == null || source.trim().isEmpty()) ? attackDefaultSource : source.trim();
String type = eventType == null ? "detected" : eventType.trim().toLowerCase(Locale.ROOT);
String title;
String shortText;
int color;
if ("stopped".equals(type) || "mitigated".equals(type)) {
title = "✅ Attack Stopped";
shortText = "Traffic hat sich normalisiert.";
color = 0x2ECC71;
} else {
title = "🚨 Attack Detected";
shortText = "Ungewöhnlich hoher Verbindungs-Traffic erkannt.";
color = 0xE74C3C;
}
StringBuilder fields = new StringBuilder();
appendEmbedField(fields, "Source", usedSource, true);
appendEmbedField(fields, "Event", type.toUpperCase(Locale.ROOT), true);
if (connectionsPerSecond != null && connectionsPerSecond >= 0) {
appendEmbedField(fields, "Connections / Second", String.valueOf(connectionsPerSecond), true);
}
if (blockedIps != null && blockedIps >= 0) {
appendEmbedField(fields, "Blocked IPs", String.valueOf(blockedIps), true);
}
if (blockedConnections != null && blockedConnections >= 0L) {
appendEmbedField(fields, "Blocked Connections", String.valueOf(blockedConnections), true);
}
sendWebhookAttackEmbed(webhookUrl, title, shortText, color, fields.toString());
return true;
}
public Map<String, Object> buildSnapshot() {
Map<String, Object> out = new LinkedHashMap<String, Object>();
long now = System.currentTimeMillis();
long uptimeMs = Math.max(0L, now - startedAtMillis);
Runtime rt = Runtime.getRuntime();
long maxMemory = rt.maxMemory();
long totalMemory = rt.totalMemory();
long freeMemory = rt.freeMemory();
long usedMemory = totalMemory - freeMemory;
Map<String, Object> memory = new LinkedHashMap<String, Object>();
memory.put("used_mb", bytesToMb(usedMemory));
memory.put("free_mb", bytesToMb(freeMemory));
memory.put("total_mb", bytesToMb(totalMemory));
memory.put("max_mb", bytesToMb(maxMemory));
memory.put("usage_percent", percent(usedMemory, Math.max(1L, maxMemory)));
int onlinePlayers = ProxyServer.getInstance().getPlayers().size();
int maxPlayers = ProxyServer.getInstance().getConfig().getPlayerLimit();
Map<String, Object> ping = buildPingSummary(ProxyServer.getInstance().getPlayers());
Map<String, Object> players = new LinkedHashMap<String, Object>();
players.put("online", onlinePlayers);
players.put("max", maxPlayers);
players.put("occupancy_percent", percent(onlinePlayers, Math.max(1, maxPlayers)));
players.put("bedrock_online", countBedrockPlayers());
players.put("ping", ping);
List<Map<String, Object>> backend = buildBackendDistribution();
Map<String, Object> system = new LinkedHashMap<String, Object>();
system.put("java_version", System.getProperty("java.version"));
system.put("java_vendor", System.getProperty("java.vendor"));
system.put("os_name", System.getProperty("os.name"));
system.put("os_arch", System.getProperty("os.arch"));
system.put("available_processors", Runtime.getRuntime().availableProcessors());
system.put("system_load_percent", getSystemLoadPercent());
out.put("enabled", true);
out.put("timestamp_unix", now / 1000L);
out.put("uptime_seconds", uptimeMs / 1000L);
out.put("uptime_human", formatDuration(uptimeMs));
out.put("players", players);
out.put("backend_servers", backend);
out.put("memory", memory);
out.put("system", system);
out.put("features", buildFeatureSummary());
if (includePlayerNames) {
List<String> names = new ArrayList<String>();
for (ProxiedPlayer p : ProxyServer.getInstance().getPlayers()) {
names.add(p.getName());
}
out.put("player_names", names);
}
return out;
}
private Map<String, Object> buildPingSummary(Collection<ProxiedPlayer> players) {
Map<String, Object> ping = new LinkedHashMap<String, Object>();
if (players.isEmpty()) {
ping.put("avg_ms", 0);
ping.put("min_ms", 0);
ping.put("max_ms", 0);
return ping;
}
long sum = 0L;
int min = Integer.MAX_VALUE;
int max = Integer.MIN_VALUE;
for (ProxiedPlayer p : players) {
int ms = Math.max(0, p.getPing());
sum += ms;
if (ms < min) min = ms;
if (ms > max) max = ms;
}
ping.put("avg_ms", Math.round((double) sum / (double) players.size()));
ping.put("min_ms", min == Integer.MAX_VALUE ? 0 : min);
ping.put("max_ms", max == Integer.MIN_VALUE ? 0 : max);
return ping;
}
private List<Map<String, Object>> buildBackendDistribution() {
List<Map<String, Object>> list = new ArrayList<Map<String, Object>>();
for (Map.Entry<String, ServerInfo> entry : ProxyServer.getInstance().getServers().entrySet()) {
ServerInfo info = entry.getValue();
Map<String, Object> row = new LinkedHashMap<String, Object>();
row.put("name", entry.getKey());
row.put("online_players", info.getPlayers().size());
row.put("address", String.valueOf(info.getAddress()));
list.add(row);
}
return list;
}
private Map<String, Object> buildFeatureSummary() {
Map<String, Object> features = new LinkedHashMap<String, Object>();
features.put("luckperms", ProxyServer.getInstance().getPluginManager().getPlugin("LuckPerms") != null);
features.put("floodgate", isFloodgateAvailable());
return features;
}
private boolean isFloodgateAvailable() {
try {
Class.forName("org.geysermc.floodgate.api.FloodgateApi");
return true;
} catch (Throwable ignored) {
return false;
}
}
private int countBedrockPlayers() {
try {
Class<?> apiClass = Class.forName("org.geysermc.floodgate.api.FloodgateApi");
Object api = apiClass.getMethod("getInstance").invoke(null);
int count = 0;
for (ProxiedPlayer p : ProxyServer.getInstance().getPlayers()) {
Boolean isBedrock = (Boolean) api.getClass().getMethod("isBedrockPlayer", UUID.class).invoke(api, p.getUniqueId());
if (Boolean.TRUE.equals(isBedrock)) {
count++;
}
}
return count;
} catch (Throwable ignored) {
return 0;
}
}
private Integer getSystemLoadPercent() {
try {
java.lang.management.OperatingSystemMXBean bean = ManagementFactory.getOperatingSystemMXBean();
if (bean instanceof OperatingSystemMXBean) {
double load = ((OperatingSystemMXBean) bean).getSystemCpuLoad();
if (load >= 0D) {
return (int) Math.round(load * 100D);
}
}
} catch (Throwable ignored) {
}
return null;
}
private int bytesToMb(long bytes) {
return (int) (bytes / (1024L * 1024L));
}
private int percent(long value, long max) {
if (max <= 0L) return 0;
return (int) Math.min(100L, Math.round((value * 100.0D) / max));
}
private String formatDuration(long ms) {
long totalSeconds = ms / 1000L;
long days = totalSeconds / 86400L;
long hours = (totalSeconds % 86400L) / 3600L;
long minutes = (totalSeconds % 3600L) / 60L;
long seconds = totalSeconds % 60L;
return String.format(Locale.ROOT, "%dd %02dh %02dm %02ds", days, hours, minutes, seconds);
}
private void loadConfig() {
File file = new File(plugin.getDataFolder(), CONFIG_FILE_NAME);
if (!file.exists()) {
enabled = true;
commandEnabled = true;
includePlayerNames = false;
webhookEnabled = false;
webhookUrl = "";
webhookUsername = "StatusAPI";
webhookThumbnailUrl = "";
webhookNotifyStartStop = true;
webhookEmbedMode = "detailed";
webhookCheckSeconds = 30;
alertMemoryPercent = 90;
alertPlayerPercent = 95;
alertCooldownSeconds = 300;
attackNotificationsEnabled = true;
attackApiKey = "";
attackDefaultSource = "BetterBungee";
return;
}
Properties props = new Properties();
try (FileInputStream in = new FileInputStream(file)) {
props.load(new InputStreamReader(in, StandardCharsets.UTF_8));
enabled = Boolean.parseBoolean(props.getProperty("networkinfo.enabled", "true"));
commandEnabled = Boolean.parseBoolean(props.getProperty("networkinfo.command.enabled", "true"));
includePlayerNames = Boolean.parseBoolean(props.getProperty("networkinfo.include_player_names", "false"));
webhookEnabled = Boolean.parseBoolean(props.getProperty("networkinfo.webhook.enabled", "false"));
webhookUrl = props.getProperty("networkinfo.webhook.url", "").trim();
webhookUsername = props.getProperty("networkinfo.webhook.username", "StatusAPI").trim();
webhookThumbnailUrl = props.getProperty("networkinfo.webhook.thumbnail_url", "").trim();
webhookNotifyStartStop = Boolean.parseBoolean(props.getProperty("networkinfo.webhook.notify_start_stop", "true"));
webhookEmbedMode = props.getProperty("networkinfo.webhook.embed_mode", "detailed").trim();
webhookCheckSeconds = parseInt(props.getProperty("networkinfo.webhook.check_seconds", "30"), 30);
alertMemoryPercent = parseInt(props.getProperty("networkinfo.alert.memory_percent", "90"), 90);
alertPlayerPercent = parseInt(props.getProperty("networkinfo.alert.player_percent", "95"), 95);
alertCooldownSeconds = parseInt(props.getProperty("networkinfo.alert.cooldown_seconds", "300"), 300);
attackNotificationsEnabled = Boolean.parseBoolean(props.getProperty("networkinfo.attack.enabled", "true"));
attackApiKey = props.getProperty("networkinfo.attack.api_key", "").trim();
attackDefaultSource = props.getProperty("networkinfo.attack.source", "BetterBungee").trim();
} catch (Exception e) {
plugin.getLogger().warning("[NetworkInfoModule] Fehler beim Laden von " + CONFIG_FILE_NAME + ": " + e.getMessage());
enabled = true;
commandEnabled = true;
includePlayerNames = false;
webhookEnabled = false;
webhookUrl = "";
webhookUsername = "StatusAPI";
webhookThumbnailUrl = "";
webhookNotifyStartStop = true;
webhookEmbedMode = "detailed";
webhookCheckSeconds = 30;
alertMemoryPercent = 90;
alertPlayerPercent = 95;
alertCooldownSeconds = 300;
attackNotificationsEnabled = true;
attackApiKey = "";
attackDefaultSource = "BetterBungee";
}
}
private void ensureModuleConfigExists() {
File target = new File(plugin.getDataFolder(), CONFIG_FILE_NAME);
if (target.exists()) {
return;
}
if (!plugin.getDataFolder().exists()) {
plugin.getDataFolder().mkdirs();
}
try (InputStream in = plugin.getResourceAsStream(CONFIG_FILE_NAME);
OutputStream out = new FileOutputStream(target)) {
if (in == null) {
plugin.getLogger().warning("[NetworkInfoModule] Standarddatei " + CONFIG_FILE_NAME + " nicht im JAR gefunden.");
return;
}
byte[] buffer = new byte[4096];
int read;
while ((read = in.read(buffer)) != -1) {
out.write(buffer, 0, read);
}
plugin.getLogger().info("[NetworkInfoModule] " + CONFIG_FILE_NAME + " wurde erstellt.");
} catch (Exception e) {
plugin.getLogger().warning("[NetworkInfoModule] Konnte " + CONFIG_FILE_NAME + " nicht erstellen: " + e.getMessage());
}
}
private void evaluateAndSendAlerts() {
if (!enabled || !webhookEnabled || webhookUrl == null || webhookUrl.isEmpty()) {
return;
}
long now = System.currentTimeMillis();
Map<String, Object> snapshot = buildSnapshot();
@SuppressWarnings("unchecked")
Map<String, Object> memory = (Map<String, Object>) snapshot.get("memory");
@SuppressWarnings("unchecked")
Map<String, Object> players = (Map<String, Object>) snapshot.get("players");
int memoryPercent = toInt(memory.get("usage_percent"));
int playerPercent = toInt(players.get("occupancy_percent"));
if (memoryPercent >= Math.max(1, alertMemoryPercent) && canSend(lastMemoryAlertAt, now)) {
lastMemoryAlertAt = now;
if (isCompactEmbedMode()) {
sendWebhookEmbed(
webhookUrl,
"⚠️ Hohe RAM-Auslastung",
"Aktuell: **" + memoryPercent + "%**\nVerbrauch: **" + memory.get("used_mb") + " MB / " + memory.get("max_mb") + " MB**\nSchwelle: **" + alertMemoryPercent + "%**",
0xF39C12
);
} else {
StringBuilder fields = new StringBuilder();
appendEmbedField(fields, "RAM-Nutzung", memoryPercent + "%", true);
appendEmbedField(fields, "Schwelle", alertMemoryPercent + "%", true);
appendEmbedField(fields, "Verbrauch", memory.get("used_mb") + " MB / " + memory.get("max_mb") + " MB", false);
sendWebhookEmbed(
webhookUrl,
"⚠️ Hohe RAM-Auslastung",
"Ein Schwellwert wurde überschritten.",
0xF39C12,
fields.toString()
);
}
}
if (playerPercent >= Math.max(1, alertPlayerPercent) && canSend(lastPlayerAlertAt, now)) {
lastPlayerAlertAt = now;
if (isCompactEmbedMode()) {
sendWebhookEmbed(
webhookUrl,
"📈 Hohe Spieler-Auslastung",
"Auslastung: **" + playerPercent + "%**\nSpieler: **" + players.get("online") + "/" + players.get("max") + "**\nSchwelle: **" + alertPlayerPercent + "%**",
0x3498DB
);
} else {
StringBuilder fields = new StringBuilder();
appendEmbedField(fields, "Auslastung", playerPercent + "%", true);
appendEmbedField(fields, "Schwelle", alertPlayerPercent + "%", true);
appendEmbedField(fields, "Spieler", players.get("online") + "/" + players.get("max"), true);
sendWebhookEmbed(
webhookUrl,
"📈 Hohe Spieler-Auslastung",
"Die Spielerlast ist aktuell sehr hoch.",
0x3498DB,
fields.toString()
);
}
}
}
private boolean isCompactEmbedMode() {
return "compact".equalsIgnoreCase(webhookEmbedMode == null ? "" : webhookEmbedMode.trim());
}
private boolean canSend(long lastSentAt, long now) {
long cooldownMs = Math.max(10, alertCooldownSeconds) * 1000L;
return (now - lastSentAt) >= cooldownMs;
}
private int parseInt(String s, int fallback) {
try {
return Integer.parseInt(s == null ? "" : s.trim());
} catch (Exception ignored) {
return fallback;
}
}
private int toInt(Object o) {
if (o instanceof Number) {
return ((Number) o).intValue();
}
try {
return Integer.parseInt(String.valueOf(o));
} catch (Exception ignored) {
return 0;
}
}
private void sendWebhookEmbed(String targetWebhookUrl, String title, String description, int color) {
sendWebhookEmbed(targetWebhookUrl, title, description, color, null);
}
private void sendWebhookEmbed(String targetWebhookUrl, String title, String description, int color, String fieldsJson) {
if (targetWebhookUrl == null || targetWebhookUrl.isEmpty()) {
return;
}
StringBuilder embed = new StringBuilder();
embed.append("{\"username\":\"").append(escapeJson(webhookUsername)).append("\",")
.append("\"embeds\":[{\"title\":\"").append(escapeJson(title)).append("\",")
.append("\"description\":\"").append(escapeJson(description)).append("\",")
.append("\"color\":").append(color).append(",");
if (fieldsJson != null && !fieldsJson.trim().isEmpty()) {
embed.append("\"fields\":[").append(fieldsJson).append("],");
}
embed.append("\"footer\":{\"text\":\"StatusPulse • NetworkInfo\"},")
.append("\"timestamp\":\"").append(Instant.now().toString()).append("\"");
if (webhookThumbnailUrl != null && !webhookThumbnailUrl.isEmpty()) {
embed.append(",\"thumbnail\":{\"url\":\"").append(escapeJson(webhookThumbnailUrl)).append("\"}");
}
embed.append("}]}");
postWebhookPayload(targetWebhookUrl, embed.toString());
}
private void sendWebhookAttackEmbed(String targetWebhookUrl,
String title,
String description,
int color,
String fieldsJson) {
if (targetWebhookUrl == null || targetWebhookUrl.isEmpty()) {
return;
}
StringBuilder embed = new StringBuilder();
embed.append("{\"username\":\"").append(escapeJson(webhookUsername)).append("\",")
.append("\"embeds\":[{\"title\":\"").append(escapeJson(title)).append("\",")
.append("\"description\":\"").append(escapeJson(description)).append("\",")
.append("\"color\":").append(color).append(",")
.append("\"fields\":[").append(fieldsJson).append("],")
.append("\"footer\":{\"text\":\"StatusPulse • Network Guard\"},")
.append("\"timestamp\":\"").append(Instant.now().toString()).append("\"");
if (webhookThumbnailUrl != null && !webhookThumbnailUrl.isEmpty()) {
embed.append(",\"thumbnail\":{\"url\":\"").append(escapeJson(webhookThumbnailUrl)).append("\"}");
}
embed.append("}]}");
postWebhookPayload(targetWebhookUrl, embed.toString());
}
private void appendEmbedField(StringBuilder out, String name, String value, boolean inline) {
if (value == null || value.trim().isEmpty()) {
return;
}
if (out.length() > 0) {
out.append(",");
}
out.append("{\"name\":\"").append(escapeJson(name)).append("\",")
.append("\"value\":\"").append(escapeJson(value.trim())).append("\",")
.append("\"inline\":").append(inline ? "true" : "false")
.append("}");
}
private void postWebhookPayload(String targetWebhookUrl, String payload) {
ProxyServer.getInstance().getScheduler().runAsync(plugin, () -> {
HttpURLConnection conn = null;
try {
byte[] bytes = payload.getBytes(StandardCharsets.UTF_8);
conn = (HttpURLConnection) new URL(targetWebhookUrl).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 >= 400) {
plugin.getLogger().warning("[NetworkInfoModule] Discord Webhook HTTP " + code);
}
} catch (Exception e) {
plugin.getLogger().warning("[NetworkInfoModule] Discord Webhook Fehler: " + e.getMessage());
} finally {
if (conn != null) {
conn.disconnect();
}
}
});
}
private String escapeJson(String s) {
if (s == null) return "";
return s.replace("\\", "\\\\")
.replace("\"", "\\\"")
.replace("\n", "\\n")
.replace("\r", "\\r");
}
private class NetInfoCommand extends Command {
NetInfoCommand() {
super("netinfo", "statusapi.netinfo");
}
@Override
public void execute(CommandSender sender, String[] args) {
if (!enabled) {
sender.sendMessage(ChatColor.RED + "NetworkInfoModule ist deaktiviert.");
return;
}
Map<String, Object> snapshot = buildSnapshot();
@SuppressWarnings("unchecked")
Map<String, Object> players = (Map<String, Object>) snapshot.get("players");
@SuppressWarnings("unchecked")
Map<String, Object> memory = (Map<String, Object>) snapshot.get("memory");
@SuppressWarnings("unchecked")
Map<String, Object> ping = (Map<String, Object>) players.get("ping");
sender.sendMessage(ChatColor.GOLD + "----- StatusAPI NetworkInfo -----");
sender.sendMessage(ChatColor.YELLOW + "Uptime: " + ChatColor.WHITE + snapshot.get("uptime_human"));
sender.sendMessage(ChatColor.YELLOW + "Spieler: " + ChatColor.WHITE + players.get("online") + "/" + players.get("max") + ChatColor.GRAY + " (Bedrock: " + players.get("bedrock_online") + ")");
sender.sendMessage(ChatColor.YELLOW + "Ping: " + ChatColor.WHITE + "avg " + ping.get("avg_ms") + "ms, min " + ping.get("min_ms") + "ms, max " + ping.get("max_ms") + "ms");
sender.sendMessage(ChatColor.YELLOW + "RAM: " + ChatColor.WHITE + memory.get("used_mb") + "MB / " + memory.get("max_mb") + "MB" + ChatColor.GRAY + " (" + memory.get("usage_percent") + "%)");
sender.sendMessage(ChatColor.YELLOW + "Backends: " + ChatColor.WHITE + ((List<?>) snapshot.get("backend_servers")).size());
}
}
}