Files
Aluer/src/main/java/com/aluer/anticheat/world/AntiXrayDetectionService.java
T
Peijun Zhao 055de65e88 安全模块大规模重构:151文件从com.aluer.security分包到8个子包
新包结构:
- com.aluer.anticheat.combat (16) — KillAura/Reach/Criticals/CrystalAura/Totem等战斗反作弊
- com.aluer.anticheat.movement (18) — Fly/Speed/Jesus/NoFall/Timer/Phase等移动反作弊
- com.aluer.anticheat.world (14) — Nuker/Xray/AutoMine/VeinMiner等世界反作弊
- com.aluer.anticheat.player (8) — FakePlayer/AltAccount/NameSpoof等玩家反作弊
- com.aluer.network (32) — DDoS/Protocol/TokenBucket/Firewall等网络安全
- com.aluer.server (10) — ChunkLoad/EntityCount/Redstone/CrashExploit等服务器保护
- com.aluer.chat (5) — ChatFlood/Advertisement/Phishing/CommandAbuse等聊天安全
- com.aluer.defense (50) — WAF/IDS/IPS/Honeypot/EDR/Forensics等纵深防御

所有package声明+跨包import已更新,323测试全绿
2026-05-22 19:29:45 +08:00

263 lines
11 KiB
Java

package com.aluer.anticheat.world;
import com.aluer.config.ServerGuardConfig;
import org.springframework.stereotype.Service;
import java.time.Instant;
import java.util.*;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.Executors;
import java.util.concurrent.ScheduledExecutorService;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.atomic.AtomicLong;
@Service
public class AntiXrayDetectionService {
private final ServerGuardConfig config;
private final Map<String, PlayerMiningData> miningData = new ConcurrentHashMap<>();
private final Map<String, Instant> flaggedPlayers = new ConcurrentHashMap<>();
private final ScheduledExecutorService scheduler = Executors.newSingleThreadScheduledExecutor();
private final AtomicLong totalDetections = new AtomicLong(0);
private static final double DIAMOND_STONE_RATIO_THRESHOLD = 0.5;
private static final double EMERALD_ORE_RATIO_THRESHOLD = 0.3;
private static final int MIN_BLOCKS_FOR_ANALYSIS = 30;
private static final int MAX_VALUABLE_STRAIGHT_LINE = 8;
private static final long MINING_SESSION_WINDOW_MS = 300000;
private static final long FLAG_DURATION_SECONDS = 3600;
private static final Set<String> VALUABLE_ORES = Set.of(
"DIAMOND_ORE", "DEEPSLATE_DIAMOND_ORE", "EMERALD_ORE", "DEEPSLATE_EMERALD_ORE",
"ANCIENT_DEBRIS", "NETHER_GOLD_ORE", "GILDED_BLACKSTONE",
"GOLD_ORE", "DEEPSLATE_GOLD_ORE"
);
private static final Set<String> COMMON_BLOCKS = Set.of(
"STONE", "DEEPSLATE", "ANDESITE", "DIORITE", "GRANITE",
"NETHERRACK", "BLACKSTONE", "BASALT", "TUFF", "CALCITE",
"DIRT", "GRAVEL", "SAND", "SANDSTONE"
);
private static final Set<String> CONTAINER_BLOCKS = Set.of(
"CHEST", "TRAPPED_CHEST", "ENDER_CHEST", "BARREL", "SHULKER_BOX",
"WHITE_SHULKER_BOX", "ORANGE_SHULKER_BOX", "MAGENTA_SHULKER_BOX",
"LIGHT_BLUE_SHULKER_BOX", "YELLOW_SHULKER_BOX", "LIME_SHULKER_BOX",
"PINK_SHULKER_BOX", "GRAY_SHULKER_BOX", "LIGHT_GRAY_SHULKER_BOX",
"CYAN_SHULKER_BOX", "PURPLE_SHULKER_BOX", "BLUE_SHULKER_BOX",
"BROWN_SHULKER_BOX", "GREEN_SHULKER_BOX", "RED_SHULKER_BOX", "BLACK_SHULKER_BOX"
);
public AntiXrayDetectionService() {
this(new ServerGuardConfig());
}
public AntiXrayDetectionService(ServerGuardConfig config) {
this.config = config;
scheduler.scheduleAtFixedRate(this::cleanupOldData, 120, 300, TimeUnit.SECONDS);
}
public XrayCheckResult checkBlockBreak(String playerName, String blockType, int x, int y, int z, String world) {
if (!config.getSecurity().getSuperEvolution().isAntiXray()) {
return XrayCheckResult.clean();
}
if (flaggedPlayers.containsKey(playerName)) {
if (Instant.now().isAfter(flaggedPlayers.get(playerName))) {
flaggedPlayers.remove(playerName);
} else {
return XrayCheckResult.flagged("Player is flagged for X-ray behavior");
}
}
PlayerMiningData data = miningData.computeIfAbsent(playerName, k -> new PlayerMiningData());
data.recordBreak(blockType, x, y, z);
int score = 0;
List<String> reasons = new ArrayList<>();
// Rule 1: Diamond-to-stone ratio anomaly
int valuableCount = data.valuableOresBroken;
int commonCount = data.commonBlocksBroken;
if (commonCount >= MIN_BLOCKS_FOR_ANALYSIS) {
double ratio = (double) valuableCount / commonCount;
if (ratio > DIAMOND_STONE_RATIO_THRESHOLD && valuableCount >= 5) {
score += 30;
reasons.add("HIGH_VALUABLE_RATIO: " + String.format("%.2f", ratio) + " (v:" + valuableCount + "/c:" + commonCount + ")");
}
}
// Rule 2: Straight-line valuable mining (classic X-ray pattern)
if (VALUABLE_ORES.contains(blockType.toUpperCase())) {
data.lastValuableX = x;
data.lastValuableY = y;
data.lastValuableZ = z;
data.consecutiveValuable++;
if (data.consecutiveValuable > MAX_VALUABLE_STRAIGHT_LINE) {
score += 30;
reasons.add("STRAIGHT_LINE_MINING: " + data.consecutiveValuable + " consecutive valuable blocks");
}
} else {
data.consecutiveValuable = 0;
}
// Rule 3: Mining directly to ores without exploring
if (VALUABLE_ORES.contains(blockType.toUpperCase())) {
// Check if there's a suspicious pattern of going directly to ores
if (data.totalBlocksBroken > 20 && data.unusualPathCount > 5) {
score += 20;
reasons.add("DIRECT_ORE_PATHING: " + data.unusualPathCount + " unusual paths");
}
}
// Rule 4: Mining containers behind walls
if (CONTAINER_BLOCKS.contains(blockType.toUpperCase())) {
if (data.isMiningThroughWalls()) {
score += 15;
reasons.add("WALL_CONTAINER_ACCESS: " + blockType);
}
}
// Rule 5: Low light level mining precision
if (VALUABLE_ORES.contains(blockType.toUpperCase()) && isMiningInDarkness(world, y)) {
score += 10;
reasons.add("DARK_MINING_PRECISION: y=" + y + " in " + world);
}
if (score >= 60) {
flaggedPlayers.put(playerName, Instant.now().plusSeconds(FLAG_DURATION_SECONDS));
totalDetections.incrementAndGet();
return XrayCheckResult.flagged("X-ray detected: " + String.join("; ", reasons));
} else if (score >= 30) {
return XrayCheckResult.suspicious(score, reasons);
}
return XrayCheckResult.clean();
}
public void clearPlayer(String playerName) {
miningData.remove(playerName);
flaggedPlayers.remove(playerName);
}
public Map<String, Object> getStatus() {
Map<String, Object> status = new LinkedHashMap<>();
status.put("trackedPlayers", miningData.size());
status.put("flaggedPlayers", flaggedPlayers.size());
status.put("totalDetections", totalDetections.get());
List<Map<String, Object>> topMiners = new ArrayList<>();
for (Map.Entry<String, PlayerMiningData> e : miningData.entrySet()) {
PlayerMiningData d = e.getValue();
Map<String, Object> m = new LinkedHashMap<>();
m.put("player", e.getKey());
m.put("valuableOres", d.valuableOresBroken);
m.put("commonBlocks", d.commonBlocksBroken);
double ratio = d.commonBlocksBroken > 0 ? (double) d.valuableOresBroken / d.commonBlocksBroken : 0;
m.put("ratio", String.format("%.3f", ratio));
m.put("consecutiveValuable", d.consecutiveValuable);
topMiners.add(m);
}
topMiners.sort((a, b) -> Double.compare(
Double.parseDouble(b.get("ratio").toString()),
Double.parseDouble(a.get("ratio").toString())));
status.put("topMiners", topMiners.subList(0, Math.min(topMiners.size(), 20)));
return status;
}
public long getTotalDetections() { return totalDetections.get(); }
private boolean isMiningInDarkness(String world, int y) {
return (world.contains("_nether") || y < 10);
}
private void cleanupOldData() {
Instant cutoff = Instant.now().minusSeconds(MINING_SESSION_WINDOW_MS * 2);
miningData.entrySet().removeIf(e -> e.getValue().lastActivity.isBefore(cutoff));
flaggedPlayers.entrySet().removeIf(e -> e.getValue().isBefore(Instant.now()));
}
private static class PlayerMiningData {
int totalBlocksBroken;
int valuableOresBroken;
int commonBlocksBroken;
int consecutiveValuable;
int unusualPathCount;
int lastValuableX, lastValuableY, lastValuableZ;
Instant lastActivity = Instant.now();
final List<BlockRecord> blockHistory = new ArrayList<>();
void recordBreak(String blockType, int x, int y, int z) {
totalBlocksBroken++;
lastActivity = Instant.now();
if (VALUABLE_ORES.contains(blockType.toUpperCase())) {
valuableOresBroken++;
if (lastValuableX != 0 || lastValuableY != 0 || lastValuableZ != 0) {
double dist = Math.sqrt(
Math.pow(x - lastValuableX, 2) +
Math.pow(y - lastValuableY, 2) +
Math.pow(z - lastValuableZ, 2));
if (dist > 20 && dist < 100) unusualPathCount++;
}
}
if (COMMON_BLOCKS.contains(blockType.toUpperCase())) {
commonBlocksBroken++;
}
blockHistory.add(new BlockRecord(blockType, x, y, z, Instant.now()));
if (blockHistory.size() > 200) {
blockHistory.remove(0);
}
consecutiveValuable = VALUABLE_ORES.contains(blockType.toUpperCase()) ? consecutiveValuable + 1 : 0;
}
boolean isMiningThroughWalls() {
if (blockHistory.size() < 10) return false;
int throughWallCount = 0;
for (int i = 3; i < blockHistory.size(); i++) {
if (CONTAINER_BLOCKS.contains(blockHistory.get(i).type.toUpperCase())) {
throughWallCount++;
}
}
return throughWallCount >= 3;
}
}
private static class BlockRecord {
final String type;
final int x, y, z;
final Instant time;
BlockRecord(String type, int x, int y, int z, Instant time) {
this.type = type;
this.x = x; this.y = y; this.z = z;
this.time = time;
}
}
public static class XrayCheckResult {
private final boolean flagged;
private final boolean suspicious;
private final int score;
private final List<String> reasons;
private XrayCheckResult(boolean flagged, boolean suspicious, int score, List<String> reasons) {
this.flagged = flagged;
this.suspicious = suspicious;
this.score = score;
this.reasons = reasons;
}
public static XrayCheckResult clean() { return new XrayCheckResult(false, false, 0, List.of()); }
public static XrayCheckResult suspicious(int score, List<String> reasons) { return new XrayCheckResult(false, true, score, reasons); }
public static XrayCheckResult flagged(String msg) { return new XrayCheckResult(true, true, 100, List.of(msg)); }
public boolean isFlagged() { return flagged; }
public boolean isSuspicious() { return suspicious; }
public int getScore() { return score; }
public List<String> getReasons() { return reasons; }
}
}