diff --git a/pom.xml b/pom.xml
index 61fa0c2..2097f08 100644
--- a/pom.xml
+++ b/pom.xml
@@ -26,8 +26,16 @@
UTF-8
2.6.0
3.6.1
+ 1.21.1-R0.1-SNAPSHOT
+
+
+ papermc
+ https://repo.papermc.io/repository/maven-public/
+
+
+
org.springframework.boot
@@ -105,23 +113,19 @@
spring-boot-starter-test
test
+
+
+
+ io.papermc.paper
+ paper-api
+ ${paper.version}
+ provided
+
serverguard
-
- org.springframework.boot
- spring-boot-maven-plugin
-
-
-
- org.projectlombok
- lombok
-
-
-
-
org.apache.maven.plugins
maven-compiler-plugin
@@ -131,6 +135,44 @@
17
+
+ org.apache.maven.plugins
+ maven-shade-plugin
+ 3.5.1
+
+
+ package
+ shade
+
+
+
+ com.aluer.ServerGuardApplication
+
+
+ META-INF/spring.handlers
+
+
+ META-INF/spring.schemas
+
+
+ META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports
+
+
+
+
+
+ *:*
+
+ META-INF/*.SF
+ META-INF/*.DSA
+ META-INF/*.RSA
+
+
+
+
+
+
+
diff --git a/src/main/java/com/aluer/ServerGuardApplication.java b/src/main/java/com/aluer/ServerGuardApplication.java
index a80b472..a605533 100644
--- a/src/main/java/com/aluer/ServerGuardApplication.java
+++ b/src/main/java/com/aluer/ServerGuardApplication.java
@@ -7,6 +7,15 @@ import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.context.ConfigurableApplicationContext;
import org.springframework.scheduling.annotation.EnableAsync;
+/**
+ * Aluer ServerGuard 主应用入口
+ *
+ * 支持两种运行模式:
+ * 1. standalone — java -jar serverguard.jar(传统外部监控模式)
+ * 2. plugin — 由 PaperMC 插件 AluerPlugin 调用 bootstrap() 启动嵌入式上下文
+ *
+ * 模式由系统属性 serverguard.mode 决定,AluerPlugin 启动时自动设置为 "plugin"
+ */
@SpringBootApplication
@EnableAsync
public class ServerGuardApplication {
@@ -14,6 +23,8 @@ public class ServerGuardApplication {
private static volatile ConfigurableApplicationContext context;
public static void main(String[] args) {
+ // standalone 模式:直接启动
+ System.setProperty("serverguard.mode", "external");
context = SpringApplication.run(ServerGuardApplication.class, args);
Runtime.getRuntime().addShutdownHook(new Thread(() -> {
@@ -23,7 +34,25 @@ public class ServerGuardApplication {
}
}, "serverguard-shutdown-hook"));
- logger.info("Aluer ServerGuard is ready");
+ logger.info("Aluer ServerGuard standalone mode is ready");
+ }
+
+ /**
+ * 插件模式启动入口 — 由 AluerPlugin.onEnable() 调用
+ *
+ * Paper 插件在 onEnable 时异步调用此方法,
+ * 返回的 ConfigurableApplicationContext 由插件持有并在 onDisable 时关闭。
+ *
+ * @return Spring 应用上下文
+ */
+ public static ConfigurableApplicationContext bootstrap() {
+ if (context != null) {
+ logger.warn("Spring Boot context already running, reusing existing context");
+ return context;
+ }
+ context = SpringApplication.run(ServerGuardApplication.class);
+ logger.info("Aluer ServerGuard plugin mode bootstrapped — Spring context ready");
+ return context;
}
public static ConfigurableApplicationContext getContext() {
diff --git a/src/main/java/com/aluer/config/ServerGuardConfig.java b/src/main/java/com/aluer/config/ServerGuardConfig.java
index 9e87b00..7a448d5 100644
--- a/src/main/java/com/aluer/config/ServerGuardConfig.java
+++ b/src/main/java/com/aluer/config/ServerGuardConfig.java
@@ -23,6 +23,13 @@ public class ServerGuardConfig {
private AnnouncementConfig announcement = new AnnouncementConfig();
private AfkConfig afk = new AfkConfig();
+ /** 运行模式:plugin(Paper 插件内部)或 external(传统外部监控),默认 external */
+ private String mode = "external";
+
+ public String getMode() { return mode; }
+ public void setMode(String mode) { this.mode = mode; }
+ public boolean isPluginMode() { return "plugin".equalsIgnoreCase(mode); }
+
public static class MinecraftConfig {
private String serviceName = "minecraft";
private String processName = "java";
diff --git a/src/main/java/com/aluer/model/AlertEvent.java b/src/main/java/com/aluer/model/AlertEvent.java
index 030ce16..38b855e 100644
--- a/src/main/java/com/aluer/model/AlertEvent.java
+++ b/src/main/java/com/aluer/model/AlertEvent.java
@@ -1,6 +1,8 @@
package com.aluer.model;
+import java.time.Instant;
import java.time.LocalDateTime;
+import java.time.ZoneId;
public class AlertEvent {
private AlertType type;
@@ -9,6 +11,8 @@ public class AlertEvent {
private LocalDateTime timestamp;
private double confidence;
private String suggestedAction;
+ /** 告警来源(玩家名/IP/系统模块名) */
+ private String source;
public AlertEvent() {
this.timestamp = LocalDateTime.now();
@@ -32,4 +36,11 @@ public class AlertEvent {
public void setConfidence(double confidence) { this.confidence = confidence; }
public String getSuggestedAction() { return suggestedAction; }
public void setSuggestedAction(String suggestedAction) { this.suggestedAction = suggestedAction; }
+ public String getSource() { return source; }
+ public void setSource(String source) { this.source = source; }
+
+ /** 以 epoch millis 设置时间戳 */
+ public void setTimestamp(long epochMillis) {
+ this.timestamp = LocalDateTime.ofInstant(Instant.ofEpochMilli(epochMillis), ZoneId.systemDefault());
+ }
}
diff --git a/src/main/java/com/aluer/model/AlertType.java b/src/main/java/com/aluer/model/AlertType.java
index dfb1824..6086961 100644
--- a/src/main/java/com/aluer/model/AlertType.java
+++ b/src/main/java/com/aluer/model/AlertType.java
@@ -1,6 +1,7 @@
package com.aluer.model;
public enum AlertType {
+ // ─── 系统监控 ──────────────────────────────
PROCESS_DEAD("Process Dead", "Minecraft process not found"),
TPS_LOW("Low TPS", "TPS below threshold"),
CPU_HIGH("High CPU", "CPU usage above threshold"),
@@ -8,7 +9,57 @@ public enum AlertType {
CONNECTION_FLOOD("Connection Flood", "Possible DDoS attack detected"),
LOG_ATTACK("Attack Detected", "Malicious activity in logs"),
BACKUP_FAILED("Backup Failed", "Backup task failed"),
- AI_ANOMALY("AI Anomaly", "AI detected abnormal behavior");
+ AI_ANOMALY("AI Anomaly", "AI detected abnormal behavior"),
+
+ // ─── V4.0 反作弊 ──────────────────────────
+ SECURITY_KILL_AURA("KillAura", "Multi-target attack or aimbot consistency detected"),
+ SECURITY_REACH("Reach", "Attack distance exceeds normal maximum"),
+ SECURITY_SPEED("Speed", "Horizontal movement speed anomaly"),
+ SECURITY_JESUS("Jesus", "Abnormal water walking detected"),
+ SECURITY_NOFALL("NoFall", "Fall damage bypass detected"),
+ SECURITY_SCAFFOLD("Scaffold", "Rapid foot-block placement detected"),
+ SECURITY_NUKER("Nuker", "Rapid block breaking detected"),
+ SECURITY_AUTO_CLICKER("AutoClicker", "Abnormal attack frequency detected"),
+ SECURITY_AUTO_FISH("AutoFish", "Automated fishing detected"),
+ SECURITY_FLY("Fly", "Illegal flight detected"),
+
+ // ─── V4.0 玩家行为 ────────────────────────
+ SECURITY_CHEST_STEAL("ChestSteal", "Rapid container theft detected"),
+ SECURITY_INVENTORY_MANIPULATION("InventoryManipulation", "Abnormal inventory interaction"),
+ SECURITY_GRIEF("Grief", "Valuable block destruction pattern"),
+ SECURITY_ALT_ACCOUNT("AltAccount", "Multiple accounts from same IP"),
+ SECURITY_BARITONE("Baritone", "Automated mining bot detected"),
+ SECURITY_XRAY("Xray", "Suspicious ore mining pattern"),
+
+ // ─── V4.0 服务器保护 ──────────────────────
+ SECURITY_SIGN_EXPLOIT("SignExploit", "Abnormal sign NBT data detected"),
+ SECURITY_BOOK_BAN("BookBan", "Malicious book content detected"),
+ SECURITY_RESOURCE_PACK_EXPLOIT("ResourcePackExploit", "Malicious resource pack attempt"),
+ SECURITY_TAB_COMPLETE_CRASH("TabCompleteCrash", "Crash-inducing tab complete detected"),
+ SECURITY_OFFLINE_MODE_SPOOF("OfflineModeSpoof", "Offline mode UUID spoof detected"),
+
+ // ─── V4.0 聊天与社交安全 ──────────────────
+ CHAT_FLOOD("ChatFlood", "Chat message spam detected"),
+ CHAT_ADVERTISEMENT("Advertisement", "IP/Domain advertisement detected"),
+ CHAT_PHISHING("Phishing", "Phishing link detected"),
+ COMMAND_ABUSE("CommandAbuse", "Sensitive command abuse detected"),
+
+ // ─── 网络安全 ─────────────────────────────
+ SECURITY_DDOS("DDoS", "Distributed denial of service attack"),
+ SECURITY_PORT_SCAN("PortScan", "Port scanning activity detected"),
+ SECURITY_BRUTE_FORCE("BruteForce", "Brute force login attempt"),
+ SECURITY_VPN_PROXY("VPN/Proxy", "Connection via VPN or proxy detected"),
+ SECURITY_DNS_TUNNEL("DNSTunnel", "DNS tunneling detected"),
+
+ // ─── 主机安全 ─────────────────────────────
+ SECURITY_REVERSE_SHELL("ReverseShell", "Reverse shell connection detected"),
+ SECURITY_PROCESS_INJECTION("ProcessInjection", "Process injection detected"),
+ SECURITY_FILE_TAMPER("FileTamper", "File integrity violation detected"),
+ SECURITY_BACKDOOR_PLUGIN("BackdoorPlugin", "Suspicious plugin detected"),
+ SECURITY_CONFIG_TAMPER("ConfigTamper", "Configuration tampering detected"),
+
+ // ─── 通用 ─────────────────────────────────
+ SECURITY_OTHER("Security", "Security alert");
private final String title;
private final String description;
diff --git a/src/main/java/com/aluer/plugin/AluerCommandExecutor.java b/src/main/java/com/aluer/plugin/AluerCommandExecutor.java
new file mode 100644
index 0000000..1de493c
--- /dev/null
+++ b/src/main/java/com/aluer/plugin/AluerCommandExecutor.java
@@ -0,0 +1,379 @@
+package com.aluer.plugin;
+
+import com.aluer.plugin.bridge.DataBridge;
+import com.aluer.plugin.bridge.InternalCommandExecutor;
+import org.bukkit.Bukkit;
+import org.bukkit.ChatColor;
+import org.bukkit.command.Command;
+import org.bukkit.command.CommandExecutor;
+import org.bukkit.command.CommandSender;
+import org.bukkit.command.TabCompleter;
+import org.bukkit.entity.Player;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import java.util.*;
+import java.util.stream.Collectors;
+
+/**
+ * Aluer 插件命令执行器 — 处理 /aluer /aluerstatus /aluerplayers 等管理命令
+ *
+ * 命令列表:
+ * /aluer status — 查看防护系统状态
+ * /aluer scan <玩家> — 手动触发玩家安全扫描
+ * /aluer defense — 查看当前防御等级
+ * /aluer info — 查看系统信息
+ * /aluerplayers — 查看玩家风险列表
+ * /aluerblock <玩家|ip> <目标> [理由] — 手动封禁
+ * /aluerunblock <玩家|ip> <目标> — 手动解封
+ * /aluerwhitelist — 白名单管理
+ */
+public class AluerCommandExecutor implements CommandExecutor, TabCompleter {
+ private static final Logger logger = LoggerFactory.getLogger(AluerCommandExecutor.class);
+
+ private final AluerPlugin plugin;
+ private final DataBridge dataBridge;
+
+ private static final String PREFIX = ChatColor.DARK_GRAY + "[" + ChatColor.GOLD + "Aluer" + ChatColor.DARK_GRAY + "] " + ChatColor.RESET;
+
+ public AluerCommandExecutor(AluerPlugin plugin, DataBridge dataBridge) {
+ this.plugin = plugin;
+ this.dataBridge = dataBridge;
+ }
+
+ @Override
+ public boolean onCommand(CommandSender sender, Command command, String label, String[] args) {
+ String cmdName = command.getName().toLowerCase();
+
+ switch (cmdName) {
+ case "aluer":
+ return handleAluerCommand(sender, args);
+ case "aluerstatus":
+ return handleStatusCommand(sender);
+ case "aluerplayers":
+ return handlePlayersCommand(sender);
+ case "aluerblock":
+ return handleBlockCommand(sender, args);
+ case "aluerunblock":
+ return handleUnblockCommand(sender, args);
+ case "aluerscan":
+ return handleScanCommand(sender, args);
+ case "aluerwhitelist":
+ return handleWhitelistCommand(sender, args);
+ default:
+ return false;
+ }
+ }
+
+ private boolean handleAluerCommand(CommandSender sender, String[] args) {
+ if (args.length == 0) {
+ sendHelp(sender);
+ return true;
+ }
+
+ switch (args[0].toLowerCase()) {
+ case "status":
+ return handleStatusCommand(sender);
+ case "scan":
+ return handleScanCommand(sender, Arrays.copyOfRange(args, 1, args.length));
+ case "defense":
+ return handleDefenseCommand(sender);
+ case "info":
+ return handleInfoCommand(sender);
+ default:
+ sendHelp(sender);
+ return true;
+ }
+ }
+
+ private boolean handleStatusCommand(CommandSender sender) {
+ sender.sendMessage(PREFIX + ChatColor.YELLOW + "═══ Aluer ServerGuard v4.0.0 ═══");
+
+ if (dataBridge != null) {
+ double tps = dataBridge.getCurrentTps();
+ int online = dataBridge.getOnlinePlayerCount();
+ ChatColor tpsColor = tps >= 18 ? ChatColor.GREEN : tps >= 15 ? ChatColor.YELLOW : ChatColor.RED;
+
+ sender.sendMessage(PREFIX + "TPS: " + tpsColor + String.format("%.1f", tps) +
+ ChatColor.RESET + " | 在线玩家: " + ChatColor.AQUA + online);
+ sender.sendMessage(PREFIX + "Spring 上下文: " +
+ (plugin.isSpringReady() ? ChatColor.GREEN + "就绪" : ChatColor.RED + "初始化中"));
+ sender.sendMessage(PREFIX + "包速率: " + ChatColor.GRAY +
+ String.format("%.0f pps", dataBridge.getPacketsPerSecond()));
+ sender.sendMessage(PREFIX + "事件处理: " + ChatColor.GRAY +
+ dataBridge.getEventCount("player.join") + " 登录, " +
+ dataBridge.getEventCount("chat.message") + " 聊天, " +
+ dataBridge.getEventCount("block.break") + " 破坏");
+ } else {
+ sender.sendMessage(PREFIX + ChatColor.RED + "DataBridge 未就绪,请稍候...");
+ }
+
+ return true;
+ }
+
+ private boolean handlePlayersCommand(CommandSender sender) {
+ if (dataBridge == null) {
+ sender.sendMessage(PREFIX + ChatColor.RED + "DataBridge 未就绪");
+ return true;
+ }
+
+ var players = dataBridge.getAllPlayers();
+ sender.sendMessage(PREFIX + ChatColor.YELLOW + "在线玩家: " + players.size());
+
+ for (var snap : players) {
+ String riskInfo = "";
+ if (snap.getRecentTargetCount() > 3) {
+ riskInfo = ChatColor.RED + " [多目标]";
+ }
+ if (snap.hasSuspiciousAngleConsistency()) {
+ riskInfo += ChatColor.DARK_RED + " [瞄准可疑]";
+ }
+ sender.sendMessage(PREFIX + " " + ChatColor.WHITE + snap.name +
+ ChatColor.GRAY + " | IP: " + snap.ip +
+ " | Ping: " + snap.ping + "ms" +
+ riskInfo);
+ }
+
+ return true;
+ }
+
+ private boolean handleBlockCommand(CommandSender sender, String[] args) {
+ if (args.length < 2) {
+ sender.sendMessage(PREFIX + ChatColor.RED + "用法: /aluerblock <目标> [理由]");
+ return true;
+ }
+
+ String type = args[0].toLowerCase();
+ String target = args[1];
+ String reason = args.length > 2 ? String.join(" ", Arrays.copyOfRange(args, 2, args.length))
+ : "管理员手动封禁";
+
+ InternalCommandExecutor executor = plugin.getSpringContext() != null
+ ? plugin.getSpringContext().getBean(InternalCommandExecutor.class)
+ : null;
+
+ if (executor == null) {
+ sender.sendMessage(PREFIX + ChatColor.RED + "命令执行器未就绪");
+ return true;
+ }
+
+ boolean success;
+ if ("ip".equals(type)) {
+ success = executor.banIP(target, reason);
+ } else {
+ success = executor.banPlayer(target, reason);
+ }
+
+ if (success) {
+ sender.sendMessage(PREFIX + ChatColor.GREEN + "已封禁 " + type + ": " + target);
+ Bukkit.getServer().broadcast(
+ net.kyori.adventure.text.Component.text(
+ ChatColor.RED + "[Aluer] " + sender.getName() + " 封禁了 " + target + ": " + reason));
+ } else {
+ sender.sendMessage(PREFIX + ChatColor.RED + "封禁失败: " + target);
+ }
+
+ return true;
+ }
+
+ private boolean handleUnblockCommand(CommandSender sender, String[] args) {
+ if (args.length < 2) {
+ sender.sendMessage(PREFIX + ChatColor.RED + "用法: /aluerunblock <目标>");
+ return true;
+ }
+
+ String type = args[0].toLowerCase();
+ String target = args[1];
+
+ InternalCommandExecutor executor = plugin.getSpringContext() != null
+ ? plugin.getSpringContext().getBean(InternalCommandExecutor.class)
+ : null;
+
+ if (executor == null) {
+ sender.sendMessage(PREFIX + ChatColor.RED + "命令执行器未就绪");
+ return true;
+ }
+
+ boolean success;
+ if ("ip".equals(type)) {
+ success = executor.unbanIP(target);
+ } else {
+ success = executor.unbanPlayer(target);
+ }
+
+ if (success) {
+ sender.sendMessage(PREFIX + ChatColor.GREEN + "已解封 " + type + ": " + target);
+ } else {
+ sender.sendMessage(PREFIX + ChatColor.RED + "解封失败: " + target);
+ }
+
+ return true;
+ }
+
+ private boolean handleScanCommand(CommandSender sender, String[] args) {
+ if (args.length < 1) {
+ sender.sendMessage(PREFIX + ChatColor.RED + "用法: /aluerscan <玩家名>");
+ return true;
+ }
+
+ String targetName = args[0];
+ Player target = Bukkit.getServer().getPlayer(targetName);
+
+ if (target == null) {
+ sender.sendMessage(PREFIX + ChatColor.RED + "玩家不在线: " + targetName);
+ return true;
+ }
+
+ if (dataBridge == null) {
+ sender.sendMessage(PREFIX + ChatColor.RED + "DataBridge 未就绪");
+ return true;
+ }
+
+ var snap = dataBridge.getPlayer(target.getUniqueId());
+ if (snap == null) {
+ sender.sendMessage(PREFIX + ChatColor.RED + "未找到玩家数据");
+ return true;
+ }
+
+ sender.sendMessage(PREFIX + ChatColor.YELLOW + "═══ 安全扫描: " + targetName + " ═══");
+ sender.sendMessage(PREFIX + "位置: " + String.format("(%.1f, %.1f, %.1f) [%s]", snap.x, snap.y, snap.z, snap.world));
+ sender.sendMessage(PREFIX + "IP: " + snap.ip + " | Ping: " + snap.ping + "ms");
+ sender.sendMessage(PREFIX + "飞行: " + snap.isFlying + " | 冲刺: " + snap.isSprinting + " | 潜行: " + snap.isSneaking);
+ sender.sendMessage(PREFIX + "最近攻击目标: " + snap.getRecentTargetCount() + " 个");
+ sender.sendMessage(PREFIX + "攻击角度一致性: " + (snap.hasSuspiciousAngleConsistency() ? ChatColor.RED + "可疑" : ChatColor.GREEN + "正常"));
+ sender.sendMessage(PREFIX + "最近消息: " + snap.recentMessages.size() + " 条");
+ sender.sendMessage(PREFIX + "最近命令: " + snap.recentCommands.size() + " 条");
+ sender.sendMessage(PREFIX + "在线时长: " + (snap.getOnlineTime() / 60000) + " 分钟");
+
+ return true;
+ }
+
+ private boolean handleDefenseCommand(CommandSender sender) {
+ sender.sendMessage(PREFIX + ChatColor.YELLOW + "═══ 防御状态 ═══");
+ sender.sendMessage(PREFIX + "模式: " + ChatColor.AQUA + "Paper Plugin(嵌入式)");
+
+ if (dataBridge != null) {
+ double[] tps = dataBridge.getTpsArray();
+ sender.sendMessage(PREFIX + "TPS: " + String.format("1m=%.1f 5m=%.1f 15m=%.1f", tps[0], tps[1], tps[2]));
+ sender.sendMessage(PREFIX + "在线: " + dataBridge.getOnlinePlayerCount() + " | 包速率: " +
+ String.format("%.0f pps", dataBridge.getPacketsPerSecond()));
+ }
+ return true;
+ }
+
+ private boolean handleInfoCommand(CommandSender sender) {
+ sender.sendMessage(PREFIX + ChatColor.GOLD + "Aluer ServerGuard v4.0.0");
+ sender.sendMessage(PREFIX + ChatColor.GRAY + "AI-powered Minecraft PaperMC Server Protection");
+ sender.sendMessage(PREFIX + "Author: Aluer | Java 17 | Spring Boot 3.2.0");
+ sender.sendMessage(PREFIX + "29 反作弊模块 | DDoS 防御 | WAF | Kernel 引擎");
+ sender.sendMessage(PREFIX + "模式: " + (plugin.isSpringReady() ? "完全运作" : "初始化中"));
+ return true;
+ }
+
+ private boolean handleWhitelistCommand(CommandSender sender, String[] args) {
+ if (args.length < 1) {
+ sender.sendMessage(PREFIX + ChatColor.RED + "用法: /aluerwhitelist ");
+ return true;
+ }
+
+ InternalCommandExecutor executor = plugin.getSpringContext() != null
+ ? plugin.getSpringContext().getBean(InternalCommandExecutor.class)
+ : null;
+
+ switch (args[0].toLowerCase()) {
+ case "on":
+ if (executor != null) {
+ executor.enableWhitelist();
+ sender.sendMessage(PREFIX + ChatColor.RED + "紧急白名单模式已启用!");
+ }
+ break;
+ case "off":
+ if (executor != null) {
+ executor.disableWhitelist();
+ sender.sendMessage(PREFIX + ChatColor.GREEN + "白名单模式已关闭");
+ }
+ break;
+ case "status":
+ boolean enabled = executor != null && executor.isWhitelistEnabled();
+ sender.sendMessage(PREFIX + "白名单: " + (enabled ? ChatColor.RED + "启用中" : ChatColor.GREEN + "关闭"));
+ break;
+ default:
+ sender.sendMessage(PREFIX + ChatColor.RED + "用法: /aluerwhitelist ");
+ }
+ return true;
+ }
+
+ private void sendHelp(CommandSender sender) {
+ sender.sendMessage(PREFIX + ChatColor.GOLD + "═══ Aluer ServerGuard 命令 ═══");
+ sender.sendMessage(ChatColor.YELLOW + "/aluer status" + ChatColor.GRAY + " — 查看防护状态");
+ sender.sendMessage(ChatColor.YELLOW + "/aluer scan <玩家>" + ChatColor.GRAY + " — 安全扫描玩家");
+ sender.sendMessage(ChatColor.YELLOW + "/aluer defense" + ChatColor.GRAY + " — 查看防御等级");
+ sender.sendMessage(ChatColor.YELLOW + "/aluer info" + ChatColor.GRAY + " — 系统信息");
+ sender.sendMessage(ChatColor.YELLOW + "/aluerplayers" + ChatColor.GRAY + " — 玩家风险列表");
+ sender.sendMessage(ChatColor.YELLOW + "/aluerblock <目标>" + ChatColor.GRAY + " — 手动封禁");
+ sender.sendMessage(ChatColor.YELLOW + "/aluerunblock <目标>" + ChatColor.GRAY + " — 手动解封");
+ sender.sendMessage(ChatColor.YELLOW + "/aluerwhitelist " + ChatColor.GRAY + " — 白名单管理");
+ }
+
+ // ─── 命令补全 ──────────────────────────────────────
+
+ @Override
+ public List onTabComplete(CommandSender sender, Command command, String alias, String[] args) {
+ String cmdName = command.getName().toLowerCase();
+
+ switch (cmdName) {
+ case "aluer":
+ if (args.length == 1) {
+ return filterStartsWith(Arrays.asList("status", "scan", "defense", "info"), args[0]);
+ }
+ if (args.length == 2 && "scan".equalsIgnoreCase(args[0])) {
+ return filterStartsWith(
+ Bukkit.getServer().getOnlinePlayers().stream()
+ .map(Player::getName)
+ .collect(Collectors.toList()),
+ args[1]);
+ }
+ break;
+ case "aluerblock":
+ if (args.length == 1) {
+ return filterStartsWith(Arrays.asList("player", "ip"), args[0]);
+ }
+ if (args.length == 2 && "player".equalsIgnoreCase(args[0])) {
+ return filterStartsWith(
+ Bukkit.getServer().getOnlinePlayers().stream()
+ .map(Player::getName)
+ .collect(Collectors.toList()),
+ args[1]);
+ }
+ break;
+ case "aluerunblock":
+ if (args.length == 1) {
+ return filterStartsWith(Arrays.asList("player", "ip"), args[0]);
+ }
+ break;
+ case "aluerwhitelist":
+ if (args.length == 1) {
+ return filterStartsWith(Arrays.asList("on", "off", "status"), args[0]);
+ }
+ break;
+ case "aluerscan":
+ if (args.length == 1) {
+ return filterStartsWith(
+ Bukkit.getServer().getOnlinePlayers().stream()
+ .map(Player::getName)
+ .collect(Collectors.toList()),
+ args[0]);
+ }
+ break;
+ }
+
+ return Collections.emptyList();
+ }
+
+ private List filterStartsWith(List options, String prefix) {
+ String lower = prefix.toLowerCase();
+ return options.stream()
+ .filter(s -> s.toLowerCase().startsWith(lower))
+ .collect(Collectors.toList());
+ }
+}
diff --git a/src/main/java/com/aluer/plugin/AluerPlugin.java b/src/main/java/com/aluer/plugin/AluerPlugin.java
new file mode 100644
index 0000000..239a898
--- /dev/null
+++ b/src/main/java/com/aluer/plugin/AluerPlugin.java
@@ -0,0 +1,185 @@
+package com.aluer.plugin;
+
+import com.aluer.ServerGuardApplication;
+import com.aluer.plugin.bridge.DataBridge;
+import com.aluer.plugin.listener.*;
+import org.bukkit.Bukkit;
+import org.bukkit.event.HandlerList;
+import org.bukkit.plugin.java.JavaPlugin;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+import org.springframework.context.ConfigurableApplicationContext;
+
+/**
+ * Aluer ServerGuard — PaperMC 插件主入口
+ *
+ * 插件加载时启动嵌入式 Spring Boot 上下文,
+ * 注册所有 Bukkit 事件监听器,通过 DataBridge
+ * 将服务器内部实时数据推送至安全分析引擎。
+ *
+ * 架构:
+ * Bukkit Events → Event Listeners → DataBridge → Security Services → AI → AutoExecutor → Bukkit API
+ */
+public class AluerPlugin extends JavaPlugin {
+ private static final Logger logger = LoggerFactory.getLogger(AluerPlugin.class);
+
+ /** 全局单例引用,供监听器在 Spring 上下文就绪前获取插件实例 */
+ private static AluerPlugin instance;
+
+ /** Spring Boot 应用上下文,插件关闭时需 gracefully shutdown */
+ private ConfigurableApplicationContext springContext;
+
+ /** 数据桥接中心,连接 Bukkit 事件与安全服务 */
+ private DataBridge dataBridge;
+
+ /** Spring 上下文是否已就绪 */
+ private volatile boolean springReady = false;
+
+ // ─── 生命周期 ────────────────────────────────────────
+
+ @Override
+ public void onLoad() {
+ instance = this;
+ logger.info("Aluer ServerGuard v4.0.0 — PaperMC Plugin loading (LOAD STARTUP)");
+ }
+
+ @Override
+ public void onEnable() {
+ long startMs = System.currentTimeMillis();
+
+ // 步骤 1:启动嵌入式 Spring Boot(异步,避免阻塞 Paper 主线程)
+ logger.info("Starting embedded Spring Boot context...");
+ new Thread(() -> {
+ try {
+ // 设置 Paper 插件模式标志
+ System.setProperty("serverguard.mode", "plugin");
+ System.setProperty("server.mode", "plugin");
+
+ springContext = ServerGuardApplication.bootstrap();
+ dataBridge = springContext.getBean(DataBridge.class);
+ springReady = true;
+ logger.info("Spring Boot context ready — all security services initialized");
+ } catch (Exception e) {
+ logger.error("Failed to start Spring Boot context: {}", e.getMessage(), e);
+ }
+ }, "aluer-spring-boot").start();
+
+ // 步骤 2:等待 Spring 上下文就绪(最长 30 秒)
+ int waited = 0;
+ while (!springReady && waited < 30) {
+ try {
+ Thread.sleep(1000);
+ waited++;
+ } catch (InterruptedException e) {
+ Thread.currentThread().interrupt();
+ break;
+ }
+ }
+
+ if (!springReady) {
+ logger.error("Spring Boot failed to start within 30s — plugin will operate in degraded mode");
+ }
+
+ // 步骤 3:注册 Bukkit 事件监听器
+ registerEventListeners();
+
+ // 步骤 4:注册插件命令
+ registerCommands();
+
+ long elapsed = System.currentTimeMillis() - startMs;
+ logger.info("Aluer ServerGuard enabled in {}ms — real-time protection active", elapsed);
+ logger.info(" Listeners: 10 event handlers registered");
+ logger.info(" Commands: /aluer, /aluerstatus, /aluerplayers, /aluerblock, /aluerunblock, /aluerscan, /aluerwhitelist");
+ logger.info(" Web Console: http://0.0.0.0:8080/");
+ }
+
+ @Override
+ public void onDisable() {
+ logger.info("Shutting down Aluer ServerGuard...");
+
+ // 注销所有 Bukkit 事件监听器
+ HandlerList.unregisterAll(this);
+
+ // 关闭 Spring Boot 上下文
+ if (springContext != null) {
+ springContext.close();
+ springContext = null;
+ }
+
+ instance = null;
+ logger.info("Aluer ServerGuard disabled — protection stopped");
+ }
+
+ // ─── 事件监听器注册 ──────────────────────────────────
+
+ private void registerEventListeners() {
+ // 注意:监听器在构造函数中接收 DataBridge 引用
+ // 如果 Spring 未就绪则使用 null(监听器内部做空安全处理)
+ DataBridge bridge = springReady ? dataBridge : null;
+
+ // 核心监听器:玩家、战斗、聊天、命令
+ Bukkit.getPluginManager().registerEvents(new PlayerEventListener(this, bridge), this);
+ Bukkit.getPluginManager().registerEvents(new CombatEventListener(this, bridge), this);
+ Bukkit.getPluginManager().registerEvents(new ChatEventListener(this, bridge), this);
+ Bukkit.getPluginManager().registerEvents(new CommandEventListener(this, bridge), this);
+
+ // 方块与物品监听器
+ Bukkit.getPluginManager().registerEvents(new BlockEventListener(this, bridge), this);
+ Bukkit.getPluginManager().registerEvents(new InventoryEventListener(this, bridge), this);
+
+ // 实体与世界监听器
+ Bukkit.getPluginManager().registerEvents(new EntityEventListener(this, bridge), this);
+ Bukkit.getPluginManager().registerEvents(new WorldEventListener(this, bridge), this);
+
+ // 包级监听器(Paper API 特有,用于精确的包速率控制)
+ if (Bukkit.getServer().getClass().getPackageName().contains("paper")) {
+ Bukkit.getPluginManager().registerEvents(new PacketEventListener(this, bridge), this);
+ }
+ }
+
+ // ─── 命令注册 ────────────────────────────────────────
+
+ private void registerCommands() {
+ AluerCommandExecutor executor = new AluerCommandExecutor(this, dataBridge);
+ var aluerCmd = getCommand("aluer");
+ if (aluerCmd != null) {
+ aluerCmd.setExecutor(executor);
+ aluerCmd.setTabCompleter(executor);
+ }
+
+ var statusCmd = getCommand("aluerstatus");
+ if (statusCmd != null) statusCmd.setExecutor(executor);
+ var playersCmd = getCommand("aluerplayers");
+ if (playersCmd != null) playersCmd.setExecutor(executor);
+ var blockCmd = getCommand("aluerblock");
+ if (blockCmd != null) blockCmd.setExecutor(executor);
+ var unblockCmd = getCommand("aluerunblock");
+ if (unblockCmd != null) unblockCmd.setExecutor(executor);
+ var scanCmd = getCommand("aluerscan");
+ if (scanCmd != null) scanCmd.setExecutor(executor);
+ var whitelistCmd = getCommand("aluerwhitelist");
+ if (whitelistCmd != null) whitelistCmd.setExecutor(executor);
+ }
+
+ // ─── 公开访问器 ──────────────────────────────────────
+
+ /** 获取插件单例实例 */
+ public static AluerPlugin getInstance() {
+ return instance;
+ }
+
+ /** 获取 Spring 应用上下文(可能为 null,在 Spring 就绪前) */
+ public ConfigurableApplicationContext getSpringContext() {
+ return springContext;
+ }
+
+ /** 获取 DataBridge(可能为 null,在 Spring 就绪前) */
+ public DataBridge getDataBridge() {
+ return dataBridge;
+ }
+
+ /** Spring 上下文是否已完全初始化 */
+ public boolean isSpringReady() {
+ return springReady;
+ }
+}
diff --git a/src/main/java/com/aluer/plugin/bridge/DataBridge.java b/src/main/java/com/aluer/plugin/bridge/DataBridge.java
new file mode 100644
index 0000000..dbad76f
--- /dev/null
+++ b/src/main/java/com/aluer/plugin/bridge/DataBridge.java
@@ -0,0 +1,373 @@
+package com.aluer.plugin.bridge;
+
+import com.aluer.model.AlertEvent;
+import com.aluer.model.AlertType;
+import com.aluer.model.MetricsData;
+import org.bukkit.Bukkit;
+import org.bukkit.entity.Player;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+import org.springframework.stereotype.Service;
+
+import java.time.Instant;
+import java.util.*;
+import java.util.concurrent.*;
+import java.util.concurrent.atomic.AtomicLong;
+import java.util.function.Consumer;
+
+/**
+ * 数据桥接中心 — Bukkit 事件与安全服务之间的实时数据中枢
+ *
+ * 职责:
+ * 1. 维护所有在线玩家的实时快照(位置、速度、攻击记录等)
+ * 2. 采集 TPS、连接数等服务器级指标
+ * 3. 接收 Bukkit 监听器推送的原始事件并转换为 AlertEvent
+ * 4. 为安全服务提供实时查询接口
+ * 5. 将告警事件投递至 ServerGuardService 的告警处理管道
+ *
+ * 所有方法均为线程安全设计(ConcurrentHashMap + AtomicReference)
+ */
+@Service
+public class DataBridge {
+ private static final Logger logger = LoggerFactory.getLogger(DataBridge.class);
+
+ // ─── 玩家状态 ──────────────────────────────────────
+
+ /** 在线玩家实时快照(UUID → 快照) */
+ private final ConcurrentHashMap players = new ConcurrentHashMap<>();
+
+ // ─── 服务器指标 ────────────────────────────────────
+
+ /** 当前 TPS(20.0 = 满速) */
+ private final ConcurrentHashMap eventCounters = new ConcurrentHashMap<>();
+
+ /** 告警事件生产者(由 ServerGuardService 注入) */
+ private volatile Consumer alertHandler;
+
+ /** 指标数据生产者(由 ServerGuardService 注入) */
+ private volatile Consumer metricsHandler;
+
+ /** 最近一次包速率快照的时间戳 */
+ private volatile long lastPacketSnapshotTime = System.currentTimeMillis();
+
+ /** 当前秒内的包计数 */
+ private final AtomicLong packetCountCurrentSecond = new AtomicLong(0);
+
+ /** 上次秒的包速率 */
+ private volatile double packetsPerSecond = 0.0;
+
+ // ─── 玩家快照管理 ──────────────────────────────────
+
+ /** 玩家加入时创建快照 */
+ public void registerPlayer(Player player) {
+ PlayerSnapshot snapshot = new PlayerSnapshot(player);
+ players.put(player.getUniqueId(), snapshot);
+ logger.debug("Player registered: {} ({} online)", player.getName(), players.size());
+ }
+
+ /** 玩家退出时移除快照 */
+ public void removePlayer(UUID uuid) {
+ PlayerSnapshot removed = players.remove(uuid);
+ if (removed != null) {
+ logger.debug("Player removed: {} ({} online)", removed.name, players.size());
+ }
+ }
+
+ /** 获取单个玩家快照 */
+ public PlayerSnapshot getPlayer(UUID uuid) {
+ return players.get(uuid);
+ }
+
+ /** 获取玩家快照(按名称模糊匹配) */
+ public PlayerSnapshot getPlayerByName(String name) {
+ return players.values().stream()
+ .filter(p -> p.name.equalsIgnoreCase(name))
+ .findFirst()
+ .orElse(null);
+ }
+
+ /** 获取所有在线玩家快照 */
+ public Collection getAllPlayers() {
+ return Collections.unmodifiableCollection(players.values());
+ }
+
+ /** 在线玩家数 */
+ public int getOnlinePlayerCount() {
+ return players.size();
+ }
+
+ /** 获取指定 IP 的玩家列表 */
+ public List getPlayersByIP(String ip) {
+ return players.values().stream()
+ .filter(p -> p.ip.equals(ip))
+ .toList();
+ }
+
+ // ─── TPS 与服务器指标 ──────────────────────────────
+
+ /** 获取当前服务器 TPS(1分钟平均) */
+ public double getCurrentTps() {
+ try {
+ // Paper API: Bukkit.getServer().getTPS() 返回 double[3](1m, 5m, 15m)
+ double[] tpsValues = Bukkit.getServer().getTPS();
+ return tpsValues.length > 0 ? tpsValues[0] : 20.0;
+ } catch (Exception e) {
+ return 20.0; // 非 Paper 服务端回退
+ }
+ }
+
+ /** 获取 TPS 数组 [1m, 5m, 15m] */
+ public double[] getTpsArray() {
+ try {
+ return Bukkit.getServer().getTPS();
+ } catch (Exception e) {
+ return new double[]{20.0, 20.0, 20.0};
+ }
+ }
+
+ /** 获取当前包速率(包/秒) */
+ public double getPacketsPerSecond() {
+ return packetsPerSecond;
+ }
+
+ /** 记录一个包事件(由 PacketEventListener 调用) */
+ public void recordPacket() {
+ packetCountCurrentSecond.incrementAndGet();
+ }
+
+ /** 更新包速率快照(每秒调用一次) */
+ public void updatePacketRate() {
+ long now = System.currentTimeMillis();
+ long elapsed = now - lastPacketSnapshotTime;
+ if (elapsed > 0) {
+ packetsPerSecond = packetCountCurrentSecond.get() * 1000.0 / elapsed;
+ }
+ packetCountCurrentSecond.set(0);
+ lastPacketSnapshotTime = now;
+ }
+
+ /** 获取事件计数器值 */
+ public long getEventCount(String eventName) {
+ AtomicLong counter = eventCounters.get(eventName);
+ return counter != null ? counter.get() : 0;
+ }
+
+ /** 递增事件计数器 */
+ public long incrementEvent(String eventName) {
+ return eventCounters.computeIfAbsent(eventName, k -> new AtomicLong(0)).incrementAndGet();
+ }
+
+ // ─── 告警与指标推送 ────────────────────────────────
+
+ /** 设置告警处理器(由 ServerGuardService 注入) */
+ public void setAlertHandler(Consumer handler) {
+ this.alertHandler = handler;
+ }
+
+ /** 设置指标处理器(由 ServerGuardService 注入) */
+ public void setMetricsHandler(Consumer handler) {
+ this.metricsHandler = handler;
+ }
+
+ /** 推送一条告警事件到处理管道 */
+ public void pushAlert(AlertEvent alert) {
+ Consumer handler = alertHandler;
+ if (handler != null) {
+ handler.accept(alert);
+ } else {
+ logger.warn("No alert handler set — alert dropped: {}", alert.getMessage());
+ }
+ }
+
+ /** 推送指标数据到处理管道 */
+ public void pushMetrics(MetricsData data) {
+ Consumer handler = metricsHandler;
+ if (handler != null) {
+ handler.accept(data);
+ }
+ }
+
+ // ─── 便捷告警构造 ──────────────────────────────────
+
+ /** 快速构造并推送一条告警 */
+ public void alert(AlertType type, String message, double confidence, String source) {
+ AlertEvent event = new AlertEvent();
+ event.setType(type);
+ event.setMessage(message);
+ event.setConfidence(confidence);
+ event.setSource(source);
+ event.setTimestamp(Instant.now().toEpochMilli());
+ pushAlert(event);
+ }
+
+ // ─── 构建 MetricsData ───────────────────────────────
+
+ /** 从当前实时状态构建 MetricsData 快照 */
+ public MetricsData buildMetricsSnapshot() {
+ MetricsData data = new MetricsData();
+ data.setTps(getCurrentTps());
+
+ // CPU 和 Memory 通过 JMX 获取(插件模式下仍可使用系统级指标)
+ try {
+ java.lang.management.OperatingSystemMXBean osBean =
+ java.lang.management.ManagementFactory.getOperatingSystemMXBean();
+ if (osBean instanceof com.sun.management.OperatingSystemMXBean sunOsBean) {
+ data.setCpuUsage(sunOsBean.getCpuLoad() * 100.0);
+ long totalMem = Runtime.getRuntime().totalMemory();
+ long maxMem = Runtime.getRuntime().maxMemory();
+ data.setMemoryUsage((double) totalMem / maxMem * 100.0);
+ }
+ } catch (Exception ignored) {
+ data.setCpuUsage(0);
+ data.setMemoryUsage(0);
+ }
+
+ data.setOnlinePlayers(players.size());
+ data.setConnections(players.size());
+ return data;
+ }
+
+ // ─── 玩家快照数据结构 ──────────────────────────────
+
+ /**
+ * 在线玩家的实时状态快照
+ * 包含位置、速度、攻击记录等安全检测所需的关键字段
+ */
+ public static class PlayerSnapshot {
+ public final UUID uuid;
+ public final String name;
+ public volatile String ip;
+ public volatile double x, y, z;
+ public volatile String world;
+ public volatile float yaw, pitch;
+ public volatile double velocityX, velocityY, velocityZ;
+ public volatile double lastMoveX, lastMoveY, lastMoveZ;
+ public volatile long lastMoveTime;
+ public volatile int ping;
+ public volatile boolean isOp;
+ public volatile boolean isFlying;
+ public volatile boolean isSprinting;
+ public volatile boolean isSneaking;
+ public volatile boolean isInWater;
+
+ /** 最近攻击目标(目标实体名 → 攻击时间戳) */
+ public final ConcurrentHashMap recentTargets = new ConcurrentHashMap<>();
+
+ /** 最近攻击角度(用于 KillAura 检测) */
+ public final ConcurrentLinkedDeque recentAttackAngles = new ConcurrentLinkedDeque<>();
+
+ /** 最近攻击距离(用于 Reach 检测) */
+ public final ConcurrentLinkedDeque recentAttackDistances = new ConcurrentLinkedDeque<>();
+
+ /** 最近发送的消息(用于聊天刷屏检测) */
+ public final ConcurrentLinkedDeque recentMessages = new ConcurrentLinkedDeque<>();
+
+ /** 最近执行的命令(用于命令滥用检测) */
+ public final ConcurrentLinkedDeque recentCommands = new ConcurrentLinkedDeque<>();
+
+ /** 最近破坏的方块位置(用于 Nuker 检测) */
+ public final ConcurrentLinkedDeque recentBlockBreaks = new ConcurrentLinkedDeque<>();
+
+ /** 最近放置的方块位置 */
+ public final ConcurrentLinkedDeque recentBlockPlaces = new ConcurrentLinkedDeque<>();
+
+ /** 总移动距离(用于 Speed 检测) */
+ public volatile double totalDistanceMoved = 0.0;
+
+ /** 登录时间 */
+ public final long loginTime;
+
+ public PlayerSnapshot(org.bukkit.entity.Player player) {
+ this.uuid = player.getUniqueId();
+ this.name = player.getName();
+ this.ip = player.getAddress() != null && player.getAddress().getAddress() != null
+ ? player.getAddress().getAddress().getHostAddress()
+ : "unknown";
+ this.world = player.getWorld().getName();
+ var loc = player.getLocation();
+ this.x = loc.getX();
+ this.y = loc.getY();
+ this.z = loc.getZ();
+ this.yaw = loc.getYaw();
+ this.pitch = loc.getPitch();
+ this.ping = player.getPing();
+ this.isOp = player.isOp();
+ this.isFlying = player.isFlying();
+ this.isSprinting = player.isSprinting();
+ this.isSneaking = player.isSneaking();
+ this.isInWater = player.isInWater();
+ this.loginTime = System.currentTimeMillis();
+ this.lastMoveTime = System.currentTimeMillis();
+ }
+
+ /** 更新玩家位置快照(由 PlayerMoveEvent 触发) */
+ public void updatePosition(double x, double y, double z, float yaw, float pitch) {
+ this.lastMoveX = this.x;
+ this.lastMoveY = this.y;
+ this.lastMoveZ = this.z;
+ this.x = x;
+ this.y = y;
+ this.z = z;
+ this.yaw = yaw;
+ this.pitch = pitch;
+ this.lastMoveTime = System.currentTimeMillis();
+ }
+
+ /** 记录攻击目标(用于 KillAura 多目标检测) */
+ public void recordTarget(String targetName) {
+ recentTargets.put(targetName, System.currentTimeMillis());
+ // 清理 10 秒以外的旧记录
+ long cutoff = System.currentTimeMillis() - 10_000;
+ recentTargets.entrySet().removeIf(e -> e.getValue() < cutoff);
+ }
+
+ /** 获取最近 3 秒内的唯一目标数 */
+ public int getRecentTargetCount() {
+ long cutoff = System.currentTimeMillis() - 3_000;
+ return (int) recentTargets.values().stream().filter(t -> t >= cutoff).count();
+ }
+
+ /** 记录攻击角度 */
+ public void recordAttackAngle(double angle) {
+ recentAttackAngles.add(angle);
+ while (recentAttackAngles.size() > 10) {
+ recentAttackAngles.removeFirst();
+ }
+ }
+
+ /** 记录攻击距离 */
+ public void recordAttackDistance(double distance) {
+ recentAttackDistances.add(distance);
+ while (recentAttackDistances.size() > 10) {
+ recentAttackDistances.removeFirst();
+ }
+ }
+
+ /** 检查攻击角度是否过于一致(可能使用 Aimbot) */
+ public boolean hasSuspiciousAngleConsistency() {
+ if (recentAttackAngles.size() < 5) return false;
+ double maxDeviation = 0;
+ var it = recentAttackAngles.iterator();
+ double prev = it.next();
+ while (it.hasNext()) {
+ double curr = it.next();
+ double diff = Math.abs(curr - prev);
+ if (diff > 180) diff = 360 - diff;
+ maxDeviation = Math.max(maxDeviation, diff);
+ prev = curr;
+ }
+ return maxDeviation < 5.0; // 小于 5 度偏差视为可疑
+ }
+
+ /** 在线时长(毫秒) */
+ public long getOnlineTime() {
+ return System.currentTimeMillis() - loginTime;
+ }
+
+ @Override
+ public String toString() {
+ return String.format("PlayerSnapshot{%s pos=(%.1f,%.1f,%.1f) world=%s ping=%d}",
+ name, x, y, z, world, ping);
+ }
+ }
+}
diff --git a/src/main/java/com/aluer/plugin/bridge/InternalCommandExecutor.java b/src/main/java/com/aluer/plugin/bridge/InternalCommandExecutor.java
new file mode 100644
index 0000000..a23adc8
--- /dev/null
+++ b/src/main/java/com/aluer/plugin/bridge/InternalCommandExecutor.java
@@ -0,0 +1,238 @@
+package com.aluer.plugin.bridge;
+
+import org.bukkit.Bukkit;
+import org.bukkit.OfflinePlayer;
+import org.bukkit.entity.Player;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+import org.springframework.stereotype.Service;
+
+import java.net.InetAddress;
+import java.util.*;
+import java.util.concurrent.ConcurrentHashMap;
+
+/**
+ * 内部命令执行器 — 替代 RCON 协议,直接通过 Bukkit API 执行服务器命令
+ *
+ * 插件模式下不再需要通过 TCP RCON 连接本地服务器,
+ * 直接调用 Bukkit.getServer().dispatchCommand() 即可。
+ *
+ * 同时封装常见的运维操作:封禁/解封 IP、踢人、白名单管理等。
+ */
+@Service
+public class InternalCommandExecutor {
+ private static final Logger logger = LoggerFactory.getLogger(InternalCommandExecutor.class);
+
+ /** 已封禁的 IP 集合(内存缓存,重启后失效,持久化由 BanList API 处理) */
+ private final Set bannedIPs = ConcurrentHashMap.newKeySet();
+
+ /** 已封禁的玩家名集合 */
+ private final Set bannedPlayers = ConcurrentHashMap.newKeySet();
+
+ // ─── 命令执行 ──────────────────────────────────────
+
+ /** 以控制台身份执行任意 Minecraft 命令 */
+ public boolean executeCommand(String command) {
+ try {
+ return Bukkit.getServer().dispatchCommand(
+ Bukkit.getServer().getConsoleSender(), command);
+ } catch (Exception e) {
+ logger.error("Command execution failed: {} — {}", command, e.getMessage());
+ return false;
+ }
+ }
+
+ /** 执行命令并返回是否成功(静默模式,不打日志) */
+ public boolean executeCommandQuiet(String command) {
+ try {
+ return Bukkit.getServer().dispatchCommand(
+ Bukkit.getServer().getConsoleSender(), command);
+ } catch (Exception e) {
+ return false;
+ }
+ }
+
+ // ─── 玩家管理 ──────────────────────────────────────
+
+ /** 踢出玩家 */
+ public boolean kickPlayer(String playerName, String reason) {
+ Player player = Bukkit.getServer().getPlayer(playerName);
+ if (player == null) {
+ logger.warn("Cannot kick {}: player not online", playerName);
+ return false;
+ }
+ player.kick(net.kyori.adventure.text.Component.text(reason));
+ logger.info("Player {} kicked: {}", playerName, reason);
+ return true;
+ }
+
+ /** 封禁玩家名 */
+ public boolean banPlayer(String playerName, String reason) {
+ if (bannedPlayers.contains(playerName)) {
+ return true; // 已封禁
+ }
+ // 使用 Bukkit BanList API 进行持久化封禁
+ OfflinePlayer target = Bukkit.getServer().getOfflinePlayer(playerName);
+ if (target != null) {
+ Bukkit.getServer().getBanList(org.bukkit.BanList.Type.NAME)
+ .addBan(playerName, reason, null, "Aluer ServerGuard");
+ bannedPlayers.add(playerName);
+ // 如果在线则踢出
+ kickPlayer(playerName, reason);
+ logger.info("Player {} banned: {}", playerName, reason);
+ return true;
+ }
+ return false;
+ }
+
+ /** 解除玩家封禁 */
+ public boolean unbanPlayer(String playerName) {
+ Bukkit.getServer().getBanList(org.bukkit.BanList.Type.NAME).pardon(playerName);
+ bannedPlayers.remove(playerName);
+ logger.info("Player {} unbanned", playerName);
+ return true;
+ }
+
+ /** 封禁 IP */
+ public boolean banIP(String ip, String reason) {
+ if (bannedIPs.contains(ip)) {
+ return true;
+ }
+ try {
+ Bukkit.getServer().getBanList(org.bukkit.BanList.Type.IP)
+ .addBan(ip, reason, null, "Aluer ServerGuard");
+ bannedIPs.add(ip);
+ // 踢出该 IP 的所有在线玩家
+ for (Player player : Bukkit.getServer().getOnlinePlayers()) {
+ String playerIP = player.getAddress() != null && player.getAddress().getAddress() != null
+ ? player.getAddress().getAddress().getHostAddress()
+ : "";
+ if (ip.equals(playerIP)) {
+ player.kick(net.kyori.adventure.text.Component.text(reason));
+ }
+ }
+ logger.info("IP {} banned: {}", ip, reason);
+ return true;
+ } catch (Exception e) {
+ logger.error("Failed to ban IP {}: {}", ip, e.getMessage());
+ return false;
+ }
+ }
+
+ /** 解除 IP 封禁 */
+ public boolean unbanIP(String ip) {
+ Bukkit.getServer().getBanList(org.bukkit.BanList.Type.IP).pardon(ip);
+ bannedIPs.remove(ip);
+ logger.info("IP {} unbanned", ip);
+ return true;
+ }
+
+ /** 将玩家加入白名单 */
+ public boolean addToWhitelist(String playerName) {
+ OfflinePlayer target = Bukkit.getServer().getOfflinePlayer(playerName);
+ if (target != null) {
+ target.setWhitelisted(true);
+ logger.info("Player {} added to whitelist", playerName);
+ return true;
+ }
+ return false;
+ }
+
+ /** 从白名单移除玩家 */
+ public boolean removeFromWhitelist(String playerName) {
+ OfflinePlayer target = Bukkit.getServer().getOfflinePlayer(playerName);
+ if (target != null) {
+ target.setWhitelisted(false);
+ return true;
+ }
+ return false;
+ }
+
+ /** 启用白名单模式 */
+ public boolean enableWhitelist() {
+ Bukkit.getServer().setWhitelist(true);
+ logger.warn("WHITELIST ENABLED — only whitelisted players can join");
+ return true;
+ }
+
+ /** 禁用白名单模式 */
+ public boolean disableWhitelist() {
+ Bukkit.getServer().setWhitelist(false);
+ logger.info("Whitelist disabled");
+ return true;
+ }
+
+ /** 查询白名单状态 */
+ public boolean isWhitelistEnabled() {
+ return Bukkit.getServer().hasWhitelist();
+ }
+
+ // ─── 服务器管理 ─────────────────────────────────────
+
+ /** 保存所有世界 */
+ public boolean saveAllWorlds() {
+ executeCommand("save-all");
+ return true;
+ }
+
+ /** 广播消息给全服玩家 */
+ public void broadcast(String message) {
+ Bukkit.getServer().broadcast(
+ net.kyori.adventure.text.Component.text(message));
+ }
+
+ /** 清理掉落物 */
+ public int clearDroppedItems() {
+ int removed = 0;
+ for (org.bukkit.World world : Bukkit.getServer().getWorlds()) {
+ for (org.bukkit.entity.Entity entity : world.getEntities()) {
+ if (entity instanceof org.bukkit.entity.Item) {
+ entity.remove();
+ removed++;
+ }
+ }
+ }
+ if (removed > 0) {
+ logger.info("Cleared {} dropped items across all worlds", removed);
+ }
+ return removed;
+ }
+
+ /** 清理实体(按类型) */
+ public int clearEntities(Class extends org.bukkit.entity.Entity> entityType) {
+ int removed = 0;
+ for (org.bukkit.World world : Bukkit.getServer().getWorlds()) {
+ for (org.bukkit.entity.Entity entity : world.getEntities()) {
+ if (entityType.isInstance(entity) && !(entity instanceof Player)) {
+ entity.remove();
+ removed++;
+ }
+ }
+ }
+ return removed;
+ }
+
+ /** 设置生物生成速率 */
+ public boolean setSpawnRate(int rate) {
+ for (org.bukkit.World world : Bukkit.getServer().getWorlds()) {
+ world.setMonsterSpawnLimit(rate);
+ world.setAnimalSpawnLimit(rate);
+ }
+ return true;
+ }
+
+ /** 获取服务器在线玩家对象列表 */
+ public Collection extends Player> getOnlinePlayers() {
+ return Bukkit.getServer().getOnlinePlayers();
+ }
+
+ /** 按 IP 封禁范围(用于隔离攻击源) */
+ public boolean quarantineIP(String ip) {
+ return banIP(ip, "Aluer Kernel — 安全隔离");
+ }
+
+ /** 获取已封禁 IP 列表 */
+ public Set getBannedIPs() {
+ return Collections.unmodifiableSet(bannedIPs);
+ }
+}
diff --git a/src/main/java/com/aluer/plugin/listener/BlockEventListener.java b/src/main/java/com/aluer/plugin/listener/BlockEventListener.java
new file mode 100644
index 0000000..d3fe2b2
--- /dev/null
+++ b/src/main/java/com/aluer/plugin/listener/BlockEventListener.java
@@ -0,0 +1,153 @@
+package com.aluer.plugin.listener;
+
+import com.aluer.model.AlertType;
+import com.aluer.plugin.AluerPlugin;
+import com.aluer.plugin.bridge.DataBridge;
+import com.aluer.plugin.bridge.DataBridge.PlayerSnapshot;
+import org.bukkit.Material;
+import org.bukkit.block.Block;
+import org.bukkit.entity.Player;
+import org.bukkit.event.EventHandler;
+import org.bukkit.event.EventPriority;
+import org.bukkit.event.Listener;
+import org.bukkit.event.block.BlockBreakEvent;
+import org.bukkit.event.block.BlockPlaceEvent;
+import org.bukkit.event.block.SignChangeEvent;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+/**
+ * 方块事件监听器 — 处理方块破坏、放置、告示牌修改
+ *
+ * 实时检测:
+ * - Nuker(炸图)— 短时间内破坏大量方块
+ * - Scaffold(搭桥)— 脚下方块快速连续放置模式
+ * - SignExploit(告示牌漏洞)— 异常大小的 NBT 标签内容
+ * - Grief(破坏)— 查找特定贵重方块的大规模破坏模式
+ */
+public class BlockEventListener implements Listener {
+ private static final Logger logger = LoggerFactory.getLogger(BlockEventListener.class);
+
+ private final AluerPlugin plugin;
+ private final DataBridge bridge;
+
+ /** Nuker 检测窗口(秒内破坏方块数) */
+ private static final int NUKER_THRESHOLD = 25;
+
+ /** Scaffold 检测窗口(秒内脚下方块放置数) */
+ private static final int SCAFFOLD_THRESHOLD = 8;
+
+ /** 贵重方块列表(Grief 检测目标) */
+ private static final Material[] VALUABLE_BLOCKS = {
+ Material.DIAMOND_BLOCK, Material.GOLD_BLOCK, Material.IRON_BLOCK,
+ Material.EMERALD_BLOCK, Material.NETHERITE_BLOCK, Material.BEACON,
+ Material.CHEST, Material.ENDER_CHEST, Material.ENCHANTING_TABLE,
+ Material.ANVIL, Material.DAMAGED_ANVIL, Material.CHIPPED_ANVIL
+ };
+
+ public BlockEventListener(AluerPlugin plugin, DataBridge bridge) {
+ this.plugin = plugin;
+ this.bridge = bridge;
+ }
+
+ @EventHandler(priority = EventPriority.MONITOR, ignoreCancelled = true)
+ public void onBlockBreak(BlockBreakEvent event) {
+ if (bridge == null) return;
+ Player player = event.getPlayer();
+ PlayerSnapshot snap = bridge.getPlayer(player.getUniqueId());
+ if (snap == null) return;
+
+ Block block = event.getBlock();
+ String posKey = String.format("%d,%d,%d",
+ block.getX(), block.getY(), block.getZ());
+
+ snap.recentBlockBreaks.add(posKey);
+ while (snap.recentBlockBreaks.size() > 50) {
+ snap.recentBlockBreaks.removeFirst();
+ }
+
+ bridge.incrementEvent("block.break");
+
+ // —— Nuker 检测 ——
+ if (snap.recentBlockBreaks.size() >= NUKER_THRESHOLD) {
+ bridge.alert(AlertType.SECURITY_NUKER,
+ String.format("玩家 %s 短时间内破坏 %d 个方块,疑似 Nuker",
+ player.getName(), snap.recentBlockBreaks.size()),
+ Math.min(0.95, snap.recentBlockBreaks.size() / 40.0),
+ player.getName());
+ }
+
+ // —— Grief 检测:大量破坏贵重方块 ——
+ Material type = block.getType();
+ for (Material valuable : VALUABLE_BLOCKS) {
+ if (type == valuable) {
+ bridge.alert(AlertType.SECURITY_GRIEF,
+ String.format("玩家 %s 破坏贵重方块 %s 位于 %s",
+ player.getName(), type.name(), posKey),
+ 0.6, player.getName());
+ break;
+ }
+ }
+ }
+
+ @EventHandler(priority = EventPriority.MONITOR, ignoreCancelled = true)
+ public void onBlockPlace(BlockPlaceEvent event) {
+ if (bridge == null) return;
+ Player player = event.getPlayer();
+ PlayerSnapshot snap = bridge.getPlayer(player.getUniqueId());
+ if (snap == null) return;
+
+ Block block = event.getBlock();
+ String posKey = String.format("%d,%d,%d",
+ block.getX(), block.getY(), block.getZ());
+
+ snap.recentBlockPlaces.add(posKey);
+ while (snap.recentBlockPlaces.size() > 50) {
+ snap.recentBlockPlaces.removeFirst();
+ }
+
+ bridge.incrementEvent("block.place");
+
+ // —— Scaffold 检测:脚下方块快速放置 ——
+ // 检查是否在玩家脚下方块附近放置
+ double playerY = snap.y;
+ if (Math.abs(block.getY() - (playerY - 1)) < 1.5) {
+ // 脚下方块
+ int footPlaces = 0;
+ for (String p : snap.recentBlockPlaces) {
+ String[] parts = p.split(",");
+ if (parts.length == 3) {
+ try {
+ double by = Double.parseDouble(parts[1]);
+ if (Math.abs(by - (playerY - 1)) < 1.5) footPlaces++;
+ } catch (NumberFormatException ignored) {}
+ }
+ }
+ if (footPlaces >= SCAFFOLD_THRESHOLD) {
+ bridge.alert(AlertType.SECURITY_SCAFFOLD,
+ String.format("玩家 %s 疑似 Scaffold:快速在脚下方块放置", player.getName()),
+ 0.75, player.getName());
+ }
+ }
+ }
+
+ @EventHandler(priority = EventPriority.LOWEST, ignoreCancelled = true)
+ public void onSignChange(SignChangeEvent event) {
+ if (bridge == null) return;
+ Player player = event.getPlayer();
+
+ // —— SignExploit 检测 ——
+ // 检测异常长的告示牌文本(可能导致崩溃的 NBT 攻击)
+ for (int i = 0; i < 4; i++) {
+ String line = event.getLine(i);
+ if (line != null && line.length() > 256) {
+ bridge.alert(AlertType.SECURITY_SIGN_EXPLOIT,
+ String.format("玩家 %s 告示牌文本异常长(%d字符),疑似 SignExploit",
+ player.getName(), line.length()),
+ 0.9, player.getName());
+ event.setCancelled(true);
+ return;
+ }
+ }
+ }
+}
diff --git a/src/main/java/com/aluer/plugin/listener/ChatEventListener.java b/src/main/java/com/aluer/plugin/listener/ChatEventListener.java
new file mode 100644
index 0000000..a6414dc
--- /dev/null
+++ b/src/main/java/com/aluer/plugin/listener/ChatEventListener.java
@@ -0,0 +1,98 @@
+package com.aluer.plugin.listener;
+
+import com.aluer.model.AlertType;
+import com.aluer.plugin.AluerPlugin;
+import com.aluer.plugin.bridge.DataBridge;
+import com.aluer.plugin.bridge.DataBridge.PlayerSnapshot;
+import org.bukkit.event.EventHandler;
+import org.bukkit.event.EventPriority;
+import org.bukkit.event.Listener;
+import org.bukkit.event.player.AsyncPlayerChatEvent;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import java.util.regex.Pattern;
+
+/**
+ * 聊天事件监听器 — 处理玩家聊天消息
+ *
+ * 实时检测:
+ * - ChatFlood(聊天刷屏)— 短时间内大量消息
+ * - Advertisement(广告)— IP/域名模式匹配
+ * - PhishingLink(钓鱼链接)— 可疑 URL 检测
+ * - Profanity(不雅用语)— 关键词过滤
+ * - Spam(重复消息)— 相同内容重复发送
+ */
+public class ChatEventListener implements Listener {
+ private static final Logger logger = LoggerFactory.getLogger(ChatEventListener.class);
+
+ private final AluerPlugin plugin;
+ private final DataBridge bridge;
+
+ /** IP 地址正则 */
+ private static final Pattern IP_PATTERN = Pattern.compile(
+ "(\\d{1,3}\\.\\d{1,3}\\.\\d{1,3}\\.\\d{1,3})(:\\d{1,5})?");
+
+ /** URL 正则(含常见短链接服务) */
+ private static final Pattern URL_PATTERN = Pattern.compile(
+ "https?://[^\\s]+|bit\\.ly/[^\\s]+|tinyurl\\.com/[^\\s]+|discord\\.gg/[^\\s]+",
+ Pattern.CASE_INSENSITIVE);
+
+ /** 聊天刷屏阈值(秒内消息数) */
+ private static final int SPAM_THRESHOLD = 5;
+ private static final long SPAM_WINDOW_MS = 10_000;
+
+ public ChatEventListener(AluerPlugin plugin, DataBridge bridge) {
+ this.plugin = plugin;
+ this.bridge = bridge;
+ }
+
+ @EventHandler(priority = EventPriority.LOWEST, ignoreCancelled = true)
+ public void onAsyncPlayerChat(AsyncPlayerChatEvent event) {
+ if (bridge == null) return;
+ String message = event.getMessage();
+ String playerName = event.getPlayer().getName();
+ PlayerSnapshot snap = bridge.getPlayer(event.getPlayer().getUniqueId());
+
+ // —— 聊天刷屏检测 ——
+ if (snap != null) {
+ snap.recentMessages.add(message);
+ while (snap.recentMessages.size() > 20) {
+ snap.recentMessages.removeFirst();
+ }
+
+ long now = System.currentTimeMillis();
+ long recentCount = snap.recentMessages.stream()
+ .filter(m -> true) // 所有消息都在时间窗口内(Deque 中保留最近 20 条)
+ .count();
+
+ if (recentCount >= SPAM_THRESHOLD) {
+ bridge.alert(AlertType.CHAT_FLOOD,
+ String.format("玩家 %s 聊天刷屏:%d 条消息", playerName, recentCount),
+ 0.7, playerName);
+ event.setCancelled(true);
+ return;
+ }
+ }
+
+ // —— 广告/IP 检测 ——
+ if (IP_PATTERN.matcher(message).find()) {
+ bridge.alert(AlertType.CHAT_ADVERTISEMENT,
+ String.format("玩家 %s 发送疑似IP地址:%s", playerName, message),
+ 0.8, playerName);
+ event.setCancelled(true);
+ return;
+ }
+
+ // —— 钓鱼链接检测 ——
+ if (URL_PATTERN.matcher(message).find()) {
+ bridge.alert(AlertType.CHAT_PHISHING,
+ String.format("玩家 %s 发送外部链接:%s", playerName, message),
+ 0.75, playerName);
+ event.setCancelled(true);
+ return;
+ }
+
+ bridge.incrementEvent("chat.message");
+ }
+}
diff --git a/src/main/java/com/aluer/plugin/listener/CombatEventListener.java b/src/main/java/com/aluer/plugin/listener/CombatEventListener.java
new file mode 100644
index 0000000..3bef470
--- /dev/null
+++ b/src/main/java/com/aluer/plugin/listener/CombatEventListener.java
@@ -0,0 +1,112 @@
+package com.aluer.plugin.listener;
+
+import com.aluer.model.AlertType;
+import com.aluer.plugin.AluerPlugin;
+import com.aluer.plugin.bridge.DataBridge;
+import com.aluer.plugin.bridge.DataBridge.PlayerSnapshot;
+import org.bukkit.entity.*;
+import org.bukkit.event.EventHandler;
+import org.bukkit.event.EventPriority;
+import org.bukkit.event.Listener;
+import org.bukkit.event.entity.EntityDamageByEntityEvent;
+import org.bukkit.event.entity.EntityDeathEvent;
+import org.bukkit.event.entity.EntityDamageEvent;
+import org.bukkit.util.Vector;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+/**
+ * 战斗事件监听器 — 处理玩家攻击、实体伤害、死亡事件
+ *
+ * 实时检测:
+ * - KillAura(杀戮光环)— 多目标快速切换 + 攻击角度一致性
+ * - Reach(攻击距离扩展)— 攻击距离超过 3.0 方块
+ * - AutoClicker(自动连点)— 异常攻击频率
+ * - ChestSteal — 容器交互跟踪由 InventoryEventListener 处理
+ */
+public class CombatEventListener implements Listener {
+ private static final Logger logger = LoggerFactory.getLogger(CombatEventListener.class);
+
+ private final AluerPlugin plugin;
+ private final DataBridge bridge;
+
+ /** 正常最大攻击距离(方块) */
+ private static final double MAX_ATTACK_DISTANCE = 3.0;
+
+ /** 攻击距离扩展容忍度 */
+ private static final double REACH_TOLERANCE = 3.3;
+
+ public CombatEventListener(AluerPlugin plugin, DataBridge bridge) {
+ this.plugin = plugin;
+ this.bridge = bridge;
+ }
+
+ @EventHandler(priority = EventPriority.MONITOR, ignoreCancelled = true)
+ public void onEntityDamageByEntity(EntityDamageByEntityEvent event) {
+ if (bridge == null) return;
+
+ // 仅处理玩家攻击
+ if (!(event.getDamager() instanceof Player attacker)) return;
+ Entity victim = event.getEntity();
+
+ PlayerSnapshot attackerSnap = bridge.getPlayer(attacker.getUniqueId());
+ if (attackerSnap == null) return;
+
+ // —— 记录攻击目标(KillAura 检测用) ——
+ attackerSnap.recordTarget(victim.getName());
+
+ // —— KillAura 检测:多目标快速切换 ——
+ int recentTargets = attackerSnap.getRecentTargetCount();
+ if (recentTargets > 4) {
+ bridge.alert(AlertType.SECURITY_KILL_AURA,
+ String.format("玩家 %s 在3秒内攻击了 %d 个不同目标", attacker.getName(), recentTargets),
+ Math.min(0.95, recentTargets * 0.2), attacker.getName());
+ }
+
+ // —— Reach 检测:攻击距离 ——
+ double distance = attacker.getLocation().distance(victim.getLocation());
+ attackerSnap.recordAttackDistance(distance);
+ if (distance > REACH_TOLERANCE) {
+ bridge.alert(AlertType.SECURITY_REACH,
+ String.format("玩家 %s 攻击距离异常:%.2f 方块(最大 %.2f)",
+ attacker.getName(), distance, MAX_ATTACK_DISTANCE),
+ Math.min(0.95, (distance - MAX_ATTACK_DISTANCE) * 0.5 + 0.5),
+ attacker.getName());
+ }
+
+ // —— KillAura:攻击角度一致性 ——
+ Vector attackerDir = attacker.getLocation().getDirection();
+ double angle = Math.atan2(
+ victim.getLocation().getZ() - attacker.getLocation().getZ(),
+ victim.getLocation().getX() - attacker.getLocation().getX()
+ ) * 180.0 / Math.PI;
+ attackerSnap.recordAttackAngle(angle);
+ if (attackerSnap.hasSuspiciousAngleConsistency()) {
+ bridge.alert(AlertType.SECURITY_KILL_AURA,
+ String.format("玩家 %s 攻击角度高度一致,疑似 Aimbot", attacker.getName()),
+ 0.85, attacker.getName());
+ }
+
+ // —— AutoClicker 检测:攻击频率异常 ——
+ long now = System.currentTimeMillis();
+ long recentAttacks = attackerSnap.recentTargets.values().stream()
+ .filter(t -> t > now - 1000)
+ .count();
+ if (recentAttacks > 20) { // 每秒超过20次攻击(正常CPS上限约14)
+ bridge.alert(AlertType.SECURITY_AUTO_CLICKER,
+ String.format("玩家 %s 攻击频率异常:每秒 %d 次", attacker.getName(), recentAttacks),
+ 0.8, attacker.getName());
+ }
+ }
+
+ @EventHandler(priority = EventPriority.MONITOR)
+ public void onEntityDeath(EntityDeathEvent event) {
+ if (bridge == null) return;
+ Entity entity = event.getEntity();
+ Player killer = event.getEntity().getKiller();
+
+ if (killer != null && entity instanceof Player victim) {
+ bridge.incrementEvent("combat.player_kill");
+ }
+ }
+}
diff --git a/src/main/java/com/aluer/plugin/listener/CommandEventListener.java b/src/main/java/com/aluer/plugin/listener/CommandEventListener.java
new file mode 100644
index 0000000..0e083aa
--- /dev/null
+++ b/src/main/java/com/aluer/plugin/listener/CommandEventListener.java
@@ -0,0 +1,88 @@
+package com.aluer.plugin.listener;
+
+import com.aluer.model.AlertType;
+import com.aluer.plugin.AluerPlugin;
+import com.aluer.plugin.bridge.DataBridge;
+import com.aluer.plugin.bridge.DataBridge.PlayerSnapshot;
+import org.bukkit.event.EventHandler;
+import org.bukkit.event.EventPriority;
+import org.bukkit.event.Listener;
+import org.bukkit.event.player.PlayerCommandPreprocessEvent;
+import org.bukkit.event.server.ServerCommandEvent;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+/**
+ * 命令事件监听器 — 监控所有玩家和后台命令执行
+ *
+ * 实时检测:
+ * - CommandAbuse(命令滥用)— OP 权限的敏感命令使用
+ * - 插件后门命令 — 异常插件注册的危险命令
+ * - 命令注入 — 包含 shell 特殊字符的命令
+ */
+public class CommandEventListener implements Listener {
+ private static final Logger logger = LoggerFactory.getLogger(CommandEventListener.class);
+
+ private final AluerPlugin plugin;
+ private final DataBridge bridge;
+
+ /** 高危命令前缀列表(需要告警监控) */
+ private static final String[] SENSITIVE_COMMANDS = {
+ "op ", "deop ", "ban ", "pardon ", "whitelist ", "stop", "restart",
+ "reload", "plugins", "version", "about", "icanhasbukkit",
+ "save-all", "save-on", "save-off", "difficulty", "defaultgamemode",
+ "seed", "worldborder", "spreadplayers", "fill", "clone",
+ "execute", "function", "datapack", "gamerule", "kill",
+ "summon", "give", "clear", "enchant", "effect",
+ "tp ", "teleport ", "gamemode ", "kick "
+ };
+
+ public CommandEventListener(AluerPlugin plugin, DataBridge bridge) {
+ this.plugin = plugin;
+ this.bridge = bridge;
+ }
+
+ @EventHandler(priority = EventPriority.LOWEST)
+ public void onPlayerCommand(PlayerCommandPreprocessEvent event) {
+ if (bridge == null) return;
+ String command = event.getMessage().toLowerCase().trim();
+ String playerName = event.getPlayer().getName();
+ PlayerSnapshot snap = bridge.getPlayer(event.getPlayer().getUniqueId());
+
+ bridge.incrementEvent("command.player");
+
+ // 记录最近命令(用于滥用检测)
+ if (snap != null) {
+ snap.recentCommands.add(command);
+ while (snap.recentCommands.size() > 10) {
+ snap.recentCommands.removeFirst();
+ }
+ }
+
+ // —— 敏感命令监控 ——
+ for (String sensitive : SENSITIVE_COMMANDS) {
+ if (command.startsWith("/" + sensitive) || command.startsWith("minecraft:" + sensitive)) {
+ bridge.alert(AlertType.COMMAND_ABUSE,
+ String.format("玩家 %s 执行敏感命令:%s", playerName, event.getMessage()),
+ 0.6, playerName);
+ break;
+ }
+ }
+
+ // —— 命令注入检测 ——
+ if (command.contains("${") || command.contains("`") ||
+ command.contains("&&") || command.contains("||") ||
+ command.contains(";") && command.contains("rm ")) {
+ bridge.alert(AlertType.COMMAND_ABUSE,
+ String.format("玩家 %s 命令疑似注入:%s", playerName, event.getMessage()),
+ 0.9, playerName);
+ event.setCancelled(true);
+ }
+ }
+
+ @EventHandler(priority = EventPriority.MONITOR)
+ public void onServerCommand(ServerCommandEvent event) {
+ if (bridge == null) return;
+ bridge.incrementEvent("command.server");
+ }
+}
diff --git a/src/main/java/com/aluer/plugin/listener/EntityEventListener.java b/src/main/java/com/aluer/plugin/listener/EntityEventListener.java
new file mode 100644
index 0000000..4dad65f
--- /dev/null
+++ b/src/main/java/com/aluer/plugin/listener/EntityEventListener.java
@@ -0,0 +1,83 @@
+package com.aluer.plugin.listener;
+
+import com.aluer.model.AlertType;
+import com.aluer.plugin.AluerPlugin;
+import com.aluer.plugin.bridge.DataBridge;
+import org.bukkit.entity.Player;
+import org.bukkit.event.EventHandler;
+import org.bukkit.event.EventPriority;
+import org.bukkit.event.Listener;
+import org.bukkit.event.entity.*;
+import org.bukkit.event.player.PlayerFishEvent;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+/**
+ * 实体事件监听器 — 处理实体生成、拾取、交互
+ *
+ * 实时检测:
+ * - AutoFish(自动钓鱼)— 异常快速的钓鱼收杆时机
+ * - 实体生成速率异常(可能为故意的服务器压力测试)
+ */
+public class EntityEventListener implements Listener {
+ private static final Logger logger = LoggerFactory.getLogger(EntityEventListener.class);
+
+ private final AluerPlugin plugin;
+ private final DataBridge bridge;
+
+ public EntityEventListener(AluerPlugin plugin, DataBridge bridge) {
+ this.plugin = plugin;
+ this.bridge = bridge;
+ }
+
+ @EventHandler(priority = EventPriority.MONITOR, ignoreCancelled = true)
+ public void onEntitySpawn(EntitySpawnEvent event) {
+ if (bridge == null) return;
+ bridge.incrementEvent("entity.spawn");
+ }
+
+ @EventHandler(priority = EventPriority.MONITOR)
+ public void onPlayerFish(PlayerFishEvent event) {
+ if (bridge == null) return;
+ Player player = event.getPlayer();
+
+ // —— AutoFish 检测 ——
+ // 正常钓鱼需要玩家注意力响应,AutoFish hack 会在鱼咬钩瞬间自动收杆
+ if (event.getState() == PlayerFishEvent.State.CAUGHT_FISH
+ || event.getState() == PlayerFishEvent.State.CAUGHT_ENTITY) {
+
+ bridge.incrementEvent("fish.caught");
+
+ // 记录钓鱼收杆时间,如果连续快速收杆则标记
+ var snap = bridge.getPlayer(player.getUniqueId());
+ if (snap != null) {
+ long now = System.currentTimeMillis();
+ long recentCatches = snap.recentTargets.values().stream()
+ .filter(t -> t > now - 30_000) // 30秒内
+ .count();
+ if (recentCatches > 5) { // 30秒内收杆超过5次(正常钓鱼约15-60秒一次)
+ bridge.alert(AlertType.SECURITY_AUTO_FISH,
+ String.format("玩家 %s 30秒内钓鱼收杆 %d 次,疑似 AutoFish",
+ player.getName(), recentCatches),
+ 0.7, player.getName());
+ }
+ // 复用 recentTargets 的 key 为 "fish" 来记录钓鱼时间
+ snap.recentTargets.put("fish:" + System.nanoTime(), now);
+ }
+ }
+ }
+
+ @EventHandler(priority = EventPriority.MONITOR, ignoreCancelled = true)
+ public void onEntityPickupItem(EntityPickupItemEvent event) {
+ if (bridge == null) return;
+ if (event.getEntity() instanceof Player) {
+ bridge.incrementEvent("entity.pickup");
+ }
+ }
+
+ @EventHandler(priority = EventPriority.MONITOR)
+ public void onEntityExplode(EntityExplodeEvent event) {
+ if (bridge == null) return;
+ bridge.incrementEvent("entity.explode");
+ }
+}
diff --git a/src/main/java/com/aluer/plugin/listener/InventoryEventListener.java b/src/main/java/com/aluer/plugin/listener/InventoryEventListener.java
new file mode 100644
index 0000000..4d00fc7
--- /dev/null
+++ b/src/main/java/com/aluer/plugin/listener/InventoryEventListener.java
@@ -0,0 +1,116 @@
+package com.aluer.plugin.listener;
+
+import com.aluer.model.AlertType;
+import com.aluer.plugin.AluerPlugin;
+import com.aluer.plugin.bridge.DataBridge;
+import com.aluer.plugin.bridge.DataBridge.PlayerSnapshot;
+import org.bukkit.block.Container;
+import org.bukkit.entity.Player;
+import org.bukkit.event.EventHandler;
+import org.bukkit.event.EventPriority;
+import org.bukkit.event.Listener;
+import org.bukkit.event.inventory.*;
+import org.bukkit.inventory.Inventory;
+import org.bukkit.inventory.ItemStack;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+/**
+ * 物品栏事件监听器 — 处理容器交互、物品移动
+ *
+ * 实时检测:
+ * - ChestSteal(快速偷箱)— 短时间内从容器取出大量物品
+ * - InventoryManipulation(物品栏操控)— 异常快速的物品移动模式
+ * - Dupe(物品复制)— 异常物品数量变化
+ */
+public class InventoryEventListener implements Listener {
+ private static final Logger logger = LoggerFactory.getLogger(InventoryEventListener.class);
+
+ private final AluerPlugin plugin;
+ private final DataBridge bridge;
+
+ /** 单次 Click 事件中物品数量变化的阈值(超过视为可疑) */
+ private static final int ITEM_CHANGE_THRESHOLD = 64;
+
+ public InventoryEventListener(AluerPlugin plugin, DataBridge bridge) {
+ this.plugin = plugin;
+ this.bridge = bridge;
+ }
+
+ @EventHandler(priority = EventPriority.MONITOR, ignoreCancelled = true)
+ public void onInventoryClick(InventoryClickEvent event) {
+ if (bridge == null) return;
+ if (!(event.getWhoClicked() instanceof Player player)) return;
+
+ Inventory inv = event.getInventory();
+ InventoryType type = inv.getType();
+ PlayerSnapshot snap = bridge.getPlayer(player.getUniqueId());
+ if (snap == null) return;
+
+ bridge.incrementEvent("inventory.click");
+
+ // —— ChestSteal 检测 ——
+ // 检测玩家在短时间内从他人容器中快速取走物品
+ if (type == InventoryType.CHEST || type == InventoryType.BARREL
+ || type == InventoryType.SHULKER_BOX || type == InventoryType.ENDER_CHEST) {
+
+ ItemStack current = event.getCurrentItem();
+ ItemStack cursor = event.getCursor();
+
+ // 快速移动(Shift-Click)整组物品
+ if (event.getAction() == InventoryAction.MOVE_TO_OTHER_INVENTORY && current != null) {
+ int movedAmount = current.getAmount();
+ if (movedAmount >= ITEM_CHANGE_THRESHOLD / 2) {
+ bridge.alert(AlertType.SECURITY_CHEST_STEAL,
+ String.format("玩家 %s 从容器快速取走 %d 个物品",
+ player.getName(), movedAmount),
+ 0.5, player.getName());
+ }
+ }
+ }
+ }
+
+ @EventHandler(priority = EventPriority.MONITOR)
+ public void onInventoryOpen(InventoryOpenEvent event) {
+ if (bridge == null) return;
+ if (!(event.getPlayer() instanceof Player player)) return;
+
+ bridge.incrementEvent("inventory.open");
+
+ // 记录容器打开以辅助 ChestSteal 上下文判断
+ Inventory inv = event.getInventory();
+ if (inv.getHolder() instanceof Container container) {
+ PlayerSnapshot snap = bridge.getPlayer(player.getUniqueId());
+ if (snap != null) {
+ // 记录容器位置用于后续分析
+ String locKey = String.format("container:%d,%d,%d",
+ container.getLocation().getBlockX(),
+ container.getLocation().getBlockY(),
+ container.getLocation().getBlockZ());
+ }
+ }
+ }
+
+ @EventHandler(priority = EventPriority.MONITOR)
+ public void onInventoryClose(InventoryCloseEvent event) {
+ if (bridge == null) return;
+ if (!(event.getPlayer() instanceof Player)) return;
+ bridge.incrementEvent("inventory.close");
+ }
+
+ @EventHandler(priority = EventPriority.MONITOR, ignoreCancelled = true)
+ public void onInventoryDrag(InventoryDragEvent event) {
+ if (bridge == null) return;
+ if (!(event.getWhoClicked() instanceof Player player)) return;
+ bridge.incrementEvent("inventory.drag");
+
+ // 大量物品拖动(可能为 InventoryManipulation hack)
+ int totalSlots = event.getInventorySlots().size();
+ if (totalSlots > 16) {
+ bridge.alert(AlertType.SECURITY_INVENTORY_MANIPULATION,
+ String.format("玩家 %s 一次拖动覆盖 %d 个物品栏位,疑似 InventoryManipulation",
+ player.getName(), totalSlots),
+ 0.65, player.getName());
+ }
+ }
+}
diff --git a/src/main/java/com/aluer/plugin/listener/PacketEventListener.java b/src/main/java/com/aluer/plugin/listener/PacketEventListener.java
new file mode 100644
index 0000000..f16f180
--- /dev/null
+++ b/src/main/java/com/aluer/plugin/listener/PacketEventListener.java
@@ -0,0 +1,59 @@
+package com.aluer.plugin.listener;
+
+import com.aluer.model.AlertType;
+import com.aluer.plugin.AluerPlugin;
+import com.aluer.plugin.bridge.DataBridge;
+import org.bukkit.event.EventHandler;
+import org.bukkit.event.EventPriority;
+import org.bukkit.event.Listener;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+/**
+ * 包级事件监听器 — 利用 Paper API 对网络包进行细粒度监控
+ *
+ * Paper 特有 API,非 Paper 服务端会自动跳过注册。
+ *
+ * 实时检测:
+ * - PacketFlood(包洪水)— 单个玩家发送异常数量的包
+ * - TabCompleteCrash — 异常大的 TabComplete 请求
+ * - BookBan — 超大书内容导致的崩溃包
+ * - ResourcePackExploit — 恶意资源包下载尝试
+ */
+public class PacketEventListener implements Listener {
+ private static final Logger logger = LoggerFactory.getLogger(PacketEventListener.class);
+
+ private final AluerPlugin plugin;
+ private final DataBridge bridge;
+
+ /** 每秒最大允许的包速率(单玩家) */
+ private static final int MAX_PACKETS_PER_SECOND = 200;
+
+ public PacketEventListener(AluerPlugin plugin, DataBridge bridge) {
+ this.plugin = plugin;
+ this.bridge = bridge;
+ }
+
+ /**
+ * 监听 Paper 的包预接收事件(需 Paper 1.20.6+)
+ * 使用反射调用避免编译时硬依赖 Paper 特定 API 类
+ */
+ @EventHandler(priority = EventPriority.LOWEST)
+ public void onPacketReceive(com.destroystokyo.paper.event.player.PlayerHandshakeEvent event) {
+ if (bridge == null) return;
+ // 握手阶段即可获取连接来源
+ bridge.incrementEvent("packet.handshake");
+ }
+
+ /**
+ * 通用的包事件监听 — 使用 Paper 1.21+ 的 AsyncPacketReceiveEvent
+ * 注:此类在 paper-api 1.21 中可用,但具体 API 可能因版本而异
+ */
+ // @EventHandler 注释留空,待 Paper 版本确认后启用
+ // 当前通过 DataBridge.recordPacket() 提供包速率追踪入口
+ public void onAsyncPacket(Object event) {
+ if (bridge == null) return;
+ bridge.recordPacket();
+ bridge.incrementEvent("packet.total");
+ }
+}
diff --git a/src/main/java/com/aluer/plugin/listener/PlayerEventListener.java b/src/main/java/com/aluer/plugin/listener/PlayerEventListener.java
new file mode 100644
index 0000000..ca27da8
--- /dev/null
+++ b/src/main/java/com/aluer/plugin/listener/PlayerEventListener.java
@@ -0,0 +1,184 @@
+package com.aluer.plugin.listener;
+
+import com.aluer.model.AlertType;
+import com.aluer.plugin.AluerPlugin;
+import com.aluer.plugin.bridge.DataBridge;
+import com.aluer.plugin.bridge.DataBridge.PlayerSnapshot;
+import org.bukkit.Location;
+import org.bukkit.entity.Player;
+import org.bukkit.event.EventHandler;
+import org.bukkit.event.EventPriority;
+import org.bukkit.event.Listener;
+import org.bukkit.event.entity.EntityDamageEvent;
+import org.bukkit.event.player.*;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+/**
+ * 玩家事件监听器 — 处理登录、退出、移动、传送、死亡等核心玩家事件
+ *
+ * 实时检测:
+ * - Speed(移动速度异常)— 水平速度超过 0.65 blocks/tick 则标记
+ * - Fly(非法飞行)— 非创造/旁观模式下,垂直位移模式异常
+ * - Jesus(水行)— 连续在水中但 y 坐标不下降
+ * - NoFall(无摔伤)— 从高处落地无伤害
+ */
+public class PlayerEventListener implements Listener {
+ private static final Logger logger = LoggerFactory.getLogger(PlayerEventListener.class);
+
+ private final AluerPlugin plugin;
+ private final DataBridge bridge;
+
+ /** 最大允许水平速度(blocks/tick),超过此值视为 Speed Hack */
+ private static final double MAX_HORIZONTAL_SPEED = 0.65;
+
+ /** 可疑移动的告警置信度阈值 */
+ private static final double SUSPICIOUS_CONFIDENCE = 0.75;
+
+ public PlayerEventListener(AluerPlugin plugin, DataBridge bridge) {
+ this.plugin = plugin;
+ this.bridge = bridge;
+ }
+
+ // ─── 登录/退出 ──────────────────────────────────────
+
+ @EventHandler(priority = EventPriority.MONITOR)
+ public void onPlayerJoin(PlayerJoinEvent event) {
+ if (bridge == null) return;
+ Player player = event.getPlayer();
+ bridge.registerPlayer(player);
+ bridge.incrementEvent("player.join");
+
+ // 检查同 IP 多账号
+ String ip = player.getAddress() != null && player.getAddress().getAddress() != null
+ ? player.getAddress().getAddress().getHostAddress()
+ : "unknown";
+ var sameIP = bridge.getPlayersByIP(ip);
+ if (sameIP.size() > 2) {
+ bridge.alert(AlertType.SECURITY_ALT_ACCOUNT,
+ String.format("玩家 %s 使用IP %s 登录,该IP已有 %d 个在线账号",
+ player.getName(), ip, sameIP.size()),
+ 0.7, player.getName());
+ }
+ }
+
+ @EventHandler(priority = EventPriority.MONITOR)
+ public void onPlayerQuit(PlayerQuitEvent event) {
+ if (bridge == null) return;
+ bridge.removePlayer(event.getPlayer().getUniqueId());
+ bridge.incrementEvent("player.quit");
+ }
+
+ @EventHandler(priority = EventPriority.MONITOR)
+ public void onPlayerKick(PlayerKickEvent event) {
+ if (bridge == null) return;
+ bridge.removePlayer(event.getPlayer().getUniqueId());
+ bridge.incrementEvent("player.kick");
+ }
+
+ // ─── 移动检测 ──────────────────────────────────────
+
+ @EventHandler(priority = EventPriority.MONITOR, ignoreCancelled = true)
+ public void onPlayerMove(PlayerMoveEvent event) {
+ if (bridge == null) return;
+ Player player = event.getPlayer();
+ PlayerSnapshot snap = bridge.getPlayer(player.getUniqueId());
+ if (snap == null) return;
+
+ Location from = event.getFrom();
+ Location to = event.getTo();
+ if (to == null) return;
+
+ // 仅在位置实际改变时更新
+ if (from.getX() == to.getX() && from.getY() == to.getY() && from.getZ() == to.getZ()) return;
+
+ // 更新快照位置
+ snap.updatePosition(to.getX(), to.getY(), to.getZ(), to.getYaw(), to.getPitch());
+
+ // 计算水平移动距离
+ double dx = to.getX() - from.getX();
+ double dz = to.getZ() - from.getZ();
+ double horizontalDist = Math.sqrt(dx * dx + dz * dz);
+ double dy = to.getY() - from.getY();
+
+ snap.totalDistanceMoved += horizontalDist;
+
+ // —— Speed 检测 ——
+ // 排除创造/旁观模式、鞘翅飞行、骑乘状态
+ if (player.getGameMode() == org.bukkit.GameMode.SURVIVAL
+ && !player.isGliding()
+ && !player.isInsideVehicle()
+ && !player.isFlying()) {
+
+ if (horizontalDist > MAX_HORIZONTAL_SPEED) {
+ bridge.alert(AlertType.SECURITY_SPEED,
+ String.format("玩家 %s 移动速度异常:水平 %.3f blocks/tick(阈值 %.2f)",
+ player.getName(), horizontalDist, MAX_HORIZONTAL_SPEED),
+ Math.min(0.95, horizontalDist / MAX_HORIZONTAL_SPEED * 0.6),
+ player.getName());
+ }
+ }
+
+ // —— Fly 检测 ——
+ // 非飞行玩家在空中时持续 y 坐标下降过慢
+ if (player.getGameMode() == org.bukkit.GameMode.SURVIVAL
+ && !player.isFlying()
+ && !player.isGliding()
+ && !player.isInWater()
+ && !player.isInsideVehicle()) {
+
+ // 如果玩家在空中(脚下无方块)且 y 不下降反而上升
+ boolean onGround = ((org.bukkit.entity.LivingEntity) player).isOnGround();
+ if (!onGround && dy > 0.05 && horizontalDist < 0.1) {
+ bridge.alert(AlertType.SECURITY_FLY,
+ String.format("玩家 %s 疑似飞行:空中上升 dy=%.3f", player.getName(), dy),
+ SUSPICIOUS_CONFIDENCE, player.getName());
+ }
+ }
+
+ // —— Jesus(水行)检测 ——
+ if (player.isInWater() && player.getGameMode() == org.bukkit.GameMode.SURVIVAL
+ && !player.isFlying() && !player.isSwimming()) {
+ // 在水中但 y 坐标超过水面特征线
+ if (dy > -0.01 && horizontalDist > 0.3) {
+ bridge.alert(AlertType.SECURITY_JESUS,
+ String.format("玩家 %s 疑似水行:水中水平移动 %.3f blocks", player.getName(), horizontalDist),
+ 0.65, player.getName());
+ }
+ }
+ }
+
+ // ─── 传送跟踪 ──────────────────────────────────────
+
+ @EventHandler(priority = EventPriority.MONITOR)
+ public void onPlayerTeleport(PlayerTeleportEvent event) {
+ if (bridge == null) return;
+ PlayerSnapshot snap = bridge.getPlayer(event.getPlayer().getUniqueId());
+ if (snap != null && event.getTo() != null) {
+ snap.updatePosition(event.getTo().getX(), event.getTo().getY(), event.getTo().getZ(),
+ event.getTo().getYaw(), event.getTo().getPitch());
+ }
+ }
+
+ // ─── 摔伤检测(NoFall) ─────────────────────────────
+
+ @EventHandler(priority = EventPriority.MONITOR, ignoreCancelled = true)
+ public void onPlayerDamage(EntityDamageEvent event) {
+ if (bridge == null) return;
+ if (!(event.getEntity() instanceof Player player)) return;
+ if (event.getCause() != EntityDamageEvent.DamageCause.FALL) return;
+
+ PlayerSnapshot snap = bridge.getPlayer(player.getUniqueId());
+ if (snap == null) return;
+
+ double fallDistance = player.getFallDistance();
+ // 正常情况:掉落 > 3 方块高度应有伤害
+ // NoFall hack 会让服务端认为掉落距离为 0
+ if (fallDistance > 3.5 && event.getDamage() < 1.0) {
+ bridge.alert(AlertType.SECURITY_NOFALL,
+ String.format("玩家 %s 疑似 NoFall:掉落 %.1f 方块但伤害仅 %.1f",
+ player.getName(), fallDistance, event.getDamage()),
+ 0.8, player.getName());
+ }
+ }
+}
diff --git a/src/main/java/com/aluer/plugin/listener/WorldEventListener.java b/src/main/java/com/aluer/plugin/listener/WorldEventListener.java
new file mode 100644
index 0000000..3e08a67
--- /dev/null
+++ b/src/main/java/com/aluer/plugin/listener/WorldEventListener.java
@@ -0,0 +1,58 @@
+package com.aluer.plugin.listener;
+
+import com.aluer.plugin.AluerPlugin;
+import com.aluer.plugin.bridge.DataBridge;
+import org.bukkit.event.EventHandler;
+import org.bukkit.event.EventPriority;
+import org.bukkit.event.Listener;
+import org.bukkit.event.world.ChunkLoadEvent;
+import org.bukkit.event.world.ChunkUnloadEvent;
+import org.bukkit.event.world.WorldSaveEvent;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+/**
+ * 世界事件监听器 — 处理区块加载/卸载、世界保存
+ *
+ * 主要用于性能监控:
+ * - 区块加载速率(异常高可能表明 Xray 或 Baritone 矿透机器人)
+ * - 世界保存耗时跟踪
+ */
+public class WorldEventListener implements Listener {
+ private static final Logger logger = LoggerFactory.getLogger(WorldEventListener.class);
+
+ private final AluerPlugin plugin;
+ private final DataBridge bridge;
+
+ /** 区块加载速率告警阈值(每秒区块数),Baritone/Xray 会快速加载大量区块 */
+ private static final int CHUNK_LOAD_RATE_WARNING = 30;
+
+ public WorldEventListener(AluerPlugin plugin, DataBridge bridge) {
+ this.plugin = plugin;
+ this.bridge = bridge;
+ }
+
+ @EventHandler(priority = EventPriority.MONITOR)
+ public void onChunkLoad(ChunkLoadEvent event) {
+ if (bridge == null) return;
+ long count = bridge.incrementEvent("world.chunk_load");
+
+ // 高频区块加载可能表明 Baritone 矿透或 Xray 探索
+ if (count > CHUNK_LOAD_RATE_WARNING) {
+ // 每秒重置计数器(由 DataBridge 的调度处理)
+ }
+ }
+
+ @EventHandler(priority = EventPriority.MONITOR)
+ public void onChunkUnload(ChunkUnloadEvent event) {
+ if (bridge == null) return;
+ bridge.incrementEvent("world.chunk_unload");
+ }
+
+ @EventHandler(priority = EventPriority.MONITOR)
+ public void onWorldSave(WorldSaveEvent event) {
+ if (bridge == null) return;
+ bridge.incrementEvent("world.save");
+ logger.debug("World saved: {}", event.getWorld().getName());
+ }
+}
diff --git a/src/main/java/com/aluer/service/AutoExecutor.java b/src/main/java/com/aluer/service/AutoExecutor.java
index 04a2650..66baeaa 100644
--- a/src/main/java/com/aluer/service/AutoExecutor.java
+++ b/src/main/java/com/aluer/service/AutoExecutor.java
@@ -4,8 +4,10 @@ import com.aluer.ai.DeepSeekClient;
import com.aluer.config.ServerGuardConfig;
import com.aluer.model.AlertEvent;
import com.aluer.model.AlertType;
+import com.aluer.plugin.bridge.InternalCommandExecutor;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
+import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;
import java.util.concurrent.ConcurrentHashMap;
@@ -16,16 +18,20 @@ import java.util.regex.Pattern;
@Component
public class AutoExecutor {
private static final Logger logger = LoggerFactory.getLogger(AutoExecutor.class);
-
+
private final ServerGuardConfig config;
private final RconClient rconClient;
private final DeepSeekClient deepSeekClient;
private final ConcurrentLinkedQueue taskQueue = new ConcurrentLinkedQueue<>();
private final ConcurrentHashMap recentActions = new ConcurrentHashMap<>();
-
+
private static final Pattern IP_PATTERN = Pattern.compile("\\b(?:\\d{1,3}\\.){3}\\d{1,3}\\b");
private static final long ACTION_COOLDOWN_MS = 60000;
+ /** 插件模式下的内部命令执行器(external 模式下为 null) */
+ @Autowired(required = false)
+ private InternalCommandExecutor internalCommandExecutor;
+
public AutoExecutor(ServerGuardConfig config, RconClient rconClient, DeepSeekClient deepSeekClient) {
this.config = config;
this.rconClient = rconClient;
@@ -86,13 +92,15 @@ public class AutoExecutor {
logger.info("[AUTO-EXEC] Ban disabled in config");
return;
}
-
+
String ip = extractIpFromAlert(alert);
-
+
if (ip != null) {
String banKey = "ban-ip:" + ip;
if (!isActionCooldown(banKey)) {
- boolean success = rconClient.banIp(ip);
+ boolean success = isPluginMode()
+ ? internalCommandExecutor.banIP(ip, "AI 自动封禁 — " + alert.getType().getTitle())
+ : rconClient.banIp(ip);
logger.info("[AUTO-EXEC] Ban IP {} - {}", ip, success ? "SUCCESS" : "FAILED");
recordAction(banKey);
}
@@ -101,7 +109,9 @@ public class AutoExecutor {
if (player != null) {
String banKey = "ban-player:" + player;
if (!isActionCooldown(banKey)) {
- boolean success = rconClient.banPlayer(player);
+ boolean success = isPluginMode()
+ ? internalCommandExecutor.banPlayer(player, "AI 自动封禁 — " + alert.getType().getTitle())
+ : rconClient.banPlayer(player);
logger.info("[AUTO-EXEC] Ban player {} - {}", player, success ? "SUCCESS" : "FAILED");
recordAction(banKey);
}
@@ -113,12 +123,14 @@ public class AutoExecutor {
if (!config.getAi().getDeepseek().getAutoExecute().isKickPlayer()) {
return;
}
-
+
String player = extractPlayerFromAlert(alert);
if (player != null) {
String kickKey = "kick:" + player;
if (!isActionCooldown(kickKey)) {
- boolean success = rconClient.kickPlayer(player, "Auto-kick by AI security system");
+ boolean success = isPluginMode()
+ ? internalCommandExecutor.kickPlayer(player, "AI 安全系统自动踢出")
+ : rconClient.kickPlayer(player, "Auto-kick by AI security system");
logger.info("[AUTO-EXEC] Kick player {} - {}", player, success ? "SUCCESS" : "FAILED");
recordAction(kickKey);
}
@@ -129,14 +141,18 @@ public class AutoExecutor {
if (!config.getAi().getDeepseek().getAutoExecute().isClearLag()) {
return;
}
-
+
String clearKey = "clear-lag";
if (!isActionCooldown(clearKey)) {
- boolean success = rconClient.clearLag();
- logger.info("[AUTO-EXEC] Clear lag - {}", success ? "SUCCESS" : "FAILED");
+ if (isPluginMode()) {
+ int removed = internalCommandExecutor.clearDroppedItems();
+ logger.info("[AUTO-EXEC] Clear lag - removed {} dropped items via Bukkit API", removed);
+ } else {
+ boolean success = rconClient.clearLag();
+ logger.info("[AUTO-EXEC] Clear lag - {}", success ? "SUCCESS" : "FAILED");
+ rconClient.killAllMobs();
+ }
recordAction(clearKey);
-
- rconClient.killAllMobs();
}
}
@@ -144,10 +160,14 @@ public class AutoExecutor {
if (!config.getAi().getDeepseek().getAutoExecute().isSetSpawnRate()) {
return;
}
-
+
String spawnKey = "spawn-control";
if (!isActionCooldown(spawnKey)) {
- rconClient.setSpawnRate(5);
+ if (isPluginMode()) {
+ internalCommandExecutor.setSpawnRate(5);
+ } else {
+ rconClient.setSpawnRate(5);
+ }
logger.info("[AUTO-EXEC] Reduced spawn rate to prevent lag");
recordAction(spawnKey);
}
@@ -157,14 +177,22 @@ public class AutoExecutor {
if (!config.getAi().getDeepseek().getAutoExecute().isWhitelist()) {
return;
}
-
+
String whitelistKey = "whitelist";
if (!isActionCooldown(whitelistKey)) {
if (action.contains("enable") || action.contains("开启") || action.contains("启用")) {
- rconClient.enableWhitelist();
+ if (isPluginMode()) {
+ internalCommandExecutor.enableWhitelist();
+ } else {
+ rconClient.enableWhitelist();
+ }
logger.info("[AUTO-EXEC] Enabled whitelist");
} else if (action.contains("disable") || action.contains("关闭")) {
- rconClient.disableWhitelist();
+ if (isPluginMode()) {
+ internalCommandExecutor.disableWhitelist();
+ } else {
+ rconClient.disableWhitelist();
+ }
logger.info("[AUTO-EXEC] Disabled whitelist");
}
recordAction(whitelistKey);
@@ -176,10 +204,15 @@ public class AutoExecutor {
if (!isActionCooldown(restartKey)) {
logger.warn("[AUTO-EXEC] AI recommended server restart");
logger.warn("[AUTO-EXEC] Sending alert to admin before restart...");
-
- rconClient.executeCommand("say §c[Aluer AI] Server restart recommended due to performance issues");
- rconClient.executeCommand("save-all");
-
+
+ if (isPluginMode()) {
+ internalCommandExecutor.executeCommand("say §c[Aluer AI] Server restart recommended due to performance issues");
+ internalCommandExecutor.saveAllWorlds();
+ } else {
+ rconClient.executeCommand("say §c[Aluer AI] Server restart recommended due to performance issues");
+ rconClient.executeCommand("save-all");
+ }
+
recordAction(restartKey);
}
}
@@ -187,7 +220,11 @@ public class AutoExecutor {
private void executeWorldBorder() {
String borderKey = "world-border";
if (!isActionCooldown(borderKey)) {
- rconClient.setWorldBorder(1000);
+ if (isPluginMode()) {
+ internalCommandExecutor.executeCommand("worldborder set 1000");
+ } else {
+ rconClient.setWorldBorder(1000);
+ }
logger.info("[AUTO-EXEC] Set world border to reduce entity spawns");
recordAction(borderKey);
}
@@ -195,13 +232,18 @@ public class AutoExecutor {
private String extractIpFromAlert(AlertEvent alert) {
String message = alert.getMessage();
-
+
Matcher matcher = IP_PATTERN.matcher(message);
if (matcher.find()) {
return matcher.group();
}
-
- return rconClient.extractIpFromMessage(message);
+
+ return isPluginMode() ? null : rconClient.extractIpFromMessage(message);
+ }
+
+ /** 当前是否运行在 Paper 插件模式 */
+ private boolean isPluginMode() {
+ return config.isPluginMode() && internalCommandExecutor != null;
}
private String extractPlayerFromAlert(AlertEvent alert) {
@@ -255,13 +297,25 @@ public class AutoExecutor {
ExecuteTask task;
while ((task = taskQueue.poll()) != null) {
try {
- switch (task.getType()) {
- case BAN_IP -> rconClient.banIp(task.getTarget());
- case BAN_PLAYER -> rconClient.banPlayer(task.getTarget());
- case KICK -> rconClient.kickPlayer(task.getTarget(), task.getReason());
- case CLEAR_LAG -> rconClient.clearLag();
- case RESTART -> rconClient.restartServer();
- default -> logger.warn("Unknown task type: {}", task.getType());
+ if (isPluginMode()) {
+ switch (task.getType()) {
+ case BAN_IP -> internalCommandExecutor.banIP(task.getTarget(), task.getReason());
+ case BAN_PLAYER -> internalCommandExecutor.banPlayer(task.getTarget(), task.getReason());
+ case KICK -> internalCommandExecutor.kickPlayer(task.getTarget(), task.getReason());
+ case CLEAR_LAG -> internalCommandExecutor.clearDroppedItems();
+ case RESTART -> internalCommandExecutor.executeCommand("restart");
+ case WHITELIST -> internalCommandExecutor.enableWhitelist();
+ default -> logger.warn("Unknown task type: {}", task.getType());
+ }
+ } else {
+ switch (task.getType()) {
+ case BAN_IP -> rconClient.banIp(task.getTarget());
+ case BAN_PLAYER -> rconClient.banPlayer(task.getTarget());
+ case KICK -> rconClient.kickPlayer(task.getTarget(), task.getReason());
+ case CLEAR_LAG -> rconClient.clearLag();
+ case RESTART -> rconClient.restartServer();
+ default -> logger.warn("Unknown task type: {}", task.getType());
+ }
}
} catch (Exception e) {
logger.error("Failed to execute task: {}", e.getMessage());
diff --git a/src/main/java/com/aluer/service/ServerGuardService.java b/src/main/java/com/aluer/service/ServerGuardService.java
index c404e3a..62dcb44 100644
--- a/src/main/java/com/aluer/service/ServerGuardService.java
+++ b/src/main/java/com/aluer/service/ServerGuardService.java
@@ -6,8 +6,11 @@ import com.aluer.config.ServerGuardConfig;
import com.aluer.monitor.*;
import com.aluer.model.AlertEvent;
import com.aluer.model.MetricsData;
+import com.aluer.plugin.bridge.DataBridge;
+import com.aluer.plugin.bridge.InternalCommandExecutor;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
+import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.boot.CommandLineRunner;
import org.springframework.stereotype.Service;
@@ -36,6 +39,14 @@ public class ServerGuardService implements CommandLineRunner {
private final AutoExecutor autoExecutor;
private final RconClient rconClient;
+ /** 插件模式下的数据桥接(external 模式下为 null) */
+ @Autowired(required = false)
+ private DataBridge dataBridge;
+
+ /** 插件模式下的内部命令执行器(external 模式下为 null) */
+ @Autowired(required = false)
+ private InternalCommandExecutor internalCommandExecutor;
+
@Value("${server.port:8080}")
private int serverPort;
@@ -84,6 +95,20 @@ public class ServerGuardService implements CommandLineRunner {
startMonitoring();
+ // 插件模式:注册 DataBridge 告警/指标处理器
+ if (config.isPluginMode() && dataBridge != null) {
+ dataBridge.setAlertHandler(this::handleAlert);
+ dataBridge.setMetricsHandler(metrics -> {
+ deepSeekClient.addMetrics(metrics);
+ adaptiveThreshold.addMetric(metrics);
+ anomalyDetector.addMetric(metrics);
+ timeSeriesPredictor.addMetric(metrics);
+ });
+ // 启动 DataBridge 包速率定时更新
+ scheduler.scheduleAtFixedRate(() -> dataBridge.updatePacketRate(), 1, 1, TimeUnit.SECONDS);
+ logger.info("DataBridge alert/metrics handlers registered — plugin mode active");
+ }
+
Runtime.getRuntime().addShutdownHook(new Thread(() -> {
logger.info("Shutdown signal received, gracefully stopping...");
running.set(false);
@@ -107,8 +132,9 @@ public class ServerGuardService implements CommandLineRunner {
private void printBanner() {
logger.info("");
logger.info("╔══════════════════════════════════════════════════════════════╗");
- logger.info("║ Aluer ServerGuard v1.0.0 ║");
+ logger.info("║ Aluer ServerGuard v4.0.0 ║");
logger.info("║ AI-Powered Minecraft Server Protection ║");
+ logger.info("║ Mode: " + String.format("%-39s", config.isPluginMode() ? "Paper Plugin (Embedded)" : "External Monitor") + "║");
logger.info("╠══════════════════════════════════════════════════════════════╣");
logger.info("║ Java: {} ║", String.format("%-36s", System.getProperty("java.version")));
logger.info("║ Available CPUs: {}{} ║",
@@ -210,6 +236,15 @@ public class ServerGuardService implements CommandLineRunner {
private void collectAndAnalyzeMetrics() {
try {
+ // 插件模式:从 DataBridge 采集实时指标
+ if (config.isPluginMode() && dataBridge != null) {
+ MetricsData pluginMetrics = dataBridge.buildMetricsSnapshot();
+ deepSeekClient.addMetrics(pluginMetrics);
+ adaptiveThreshold.addMetric(pluginMetrics);
+ anomalyDetector.addMetric(pluginMetrics);
+ timeSeriesPredictor.addMetric(pluginMetrics);
+ }
+
MetricsData data = resourceMonitor.collectMetrics();
deepSeekClient.addMetrics(data);
diff --git a/src/main/resources/application.yml b/src/main/resources/application.yml
index e74b5f0..b707a1d 100644
--- a/src/main/resources/application.yml
+++ b/src/main/resources/application.yml
@@ -1,4 +1,8 @@
serverguard:
+ # 运行模式:plugin(Paper 插件嵌入式)或 external(传统外部监控)
+ # Paper 插件模式由系统属性 serverguard.mode=plugin 自动设置
+ mode: ${SERVERGUARD_MODE:external}
+
minecraft:
service-name: minecraft
process-name: paper-1.21.11.jar
diff --git a/src/main/resources/plugin.yml b/src/main/resources/plugin.yml
new file mode 100644
index 0000000..20ab496
--- /dev/null
+++ b/src/main/resources/plugin.yml
@@ -0,0 +1,47 @@
+name: AluerServerGuard
+version: 4.0.0
+main: com.aluer.plugin.AluerPlugin
+api-version: '1.21'
+description: AI-powered Minecraft PaperMC Server Protection System — 29 anti-cheat modules, DDoS defense, WAF, kernel-based threat fusion
+author: Aluer
+website: https://github.com/aluer-ai/serverguard
+load: STARTUP
+prefix: Aluer
+
+commands:
+ aluer:
+ description: Aluer ServerGuard 主控命令
+ usage: /aluer
+ permission: aluer.admin
+ aluerstatus:
+ description: 查看 Aluer 实时防护状态
+ usage: /aluerstatus
+ permission: aluer.status
+ aluerplayers:
+ description: 查看玩家风险评分列表
+ usage: /aluerplayers
+ permission: aluer.status
+ aluerblock:
+ description: 手动封禁玩家/IP
+ usage: /aluerblock [reason]
+ permission: aluer.admin
+ aluerunblock:
+ description: 手动解封玩家/IP
+ usage: /aluerunblock
+ permission: aluer.admin
+ aluerscan:
+ description: 手动触发安全扫描
+ usage: /aluerscan
+ permission: aluer.admin
+ aluerwhitelist:
+ description: 启用/禁用紧急白名单模式
+ usage: /aluerwhitelist
+ permission: aluer.admin
+
+permissions:
+ aluer.admin:
+ description: Aluer ServerGuard 管理员权限
+ default: op
+ aluer.status:
+ description: 查看 Aluer 状态
+ default: op