Files
Aluer/src/main/java/com/aluer/ml/BehavioralProfilingEngine.java
T
Peijun Zhao 1b66197804 V5.0 安全模块大扩展:Meteor Client 全功能反作弊 + AI行为分析 + 服务器保护
新增 45 个安全模块,覆盖 Meteor Client 全部 hack 类别:

【反作弊基础扩展 6 模块】
- AntiTimerService: 游戏加速检测(移动tick间隔分析)
- AntiVelocityService: 击退修改检测(伤害后速度分析)
- AntiPhaseService: 穿墙检测(固体方块射线追踪)
- AntiBlinkService: 闪烁检测(伤害-断连-重连时序)
- AntiFastBreakService: 快速破坏检测(方块标准破坏时间对比)
- AntiElytraFlyService: 鞘翅飞行检测(速度/高度操控分析)

【Meteor Combat 10 模块】
- AntiCriticalsService: 自动暴击检测(跳跃/地面状态验证)
- AntiAutoCrystalService: 水晶Aura检测(放置/爆炸时序分析)
- AntiAutoTotemService: 自动图腾检测(副手装备速度分析)
- AntiSurroundService: 自动包围检测(脚下方块精确放置模式)
- AntiAutoTrapService: 自动陷阱检测(目标周围方块网格分析)
- AntiAutoArmorService: 自动装备检测(多槽位同tick交换)
- AntiChestSwapService: 自动胸甲交换检测(鞘翅-胸甲乒乓切换)
- AntiAutoLogService: 自动断线检测(伤害-断连500ms时序)
- AntiHitboxesService: 扩展碰撞箱检测(射线-边界边缘命中率)
- AntiBowAimBotService: 弓箭自瞄检测(移动目标精度分析)

【Meteor Movement 10 模块】
- AntiNoSlowService: 无减速检测(物品使用+移动速度组合)
- AntiSpiderService: 爬墙检测(无攀爬方块垂直上升)
- AntiStepService: 自动跨步检测(非跳跃1格上升)
- AntiPacketFlyService: 数据包飞行检测(连续悬浮/弹跳模式)
- AntiAirJumpService: 空中跳跃检测(离地跳跃包)
- AntiLongJumpService: 远跳检测(跳跃后水平速度衰减分析)
- AntiAntiHungerService: 免饥饿检测(活动量vs饥饿消耗率)
- AntiFastFallService: 快速下落检测(超终端速度持续下落)
- AntiVClipService: 垂直穿墙检测(固体方块间瞬移)
- AntiNoFallService 增强: 新增GroundSpoof/数据包地面/坠落累积/瞬移坠落检测

【Meteor World+Player 10 模块】
- AntiSpeedMineService: 加速挖掘检测(方块标准破坏时间对比)
- AntiFastUseService: 快速使用检测(物品冷却时间分析)
- AntiNoInteractService: 交互绕过检测(持剑开箱等异常操作)
- AntiAutoMineService: 自动挖掘检测(挖掘间隔方差/视角追踪)
- AntiVeinMinerService: 连锁挖矿检测(相邻矿石快速序列)
- AntiAutoToolService: 自动工具检测(同tick切工具+开挖)
- AntiFakePlayerService: 假人检测(心跳响应/交互缺失/移动模式)
- AntiPistonAuraService: 活塞光环检测(战斗上下文活塞激活率)
- AntiAnchorService: 锚定检测(受击零位移+封闭空间)
- AntiStashFinderService: 仓库扫描检测(网格区块加载+零交互)

【网络协议安全 5 模块】
- ProtocolStateValidator: 协议状态机验证(四态转换违规检测)
- TokenBucketRateLimiter: 令牌桶速率限制器(可复用速率控制基元)
- BotFingerprintDetector: 机器人指纹检测(五维行为指纹分析)
- NBTExploitPrevention: NBT漏洞防护(深度/大小/书页/告示牌验证)
- ConnectionHandshakeValidator: 握手完整性验证(协议版本/状态/主机名)

【服务器保护 4 模块】
- ChunkLoadRateLimiter: 区块加载限制器(WARN/LIMIT/BLOCK三级响应)
- EntityCountEnforcer: 实体数量执行器(分块限制+优先级清理)
- RedstoneUpdateLimiter: 红石更新限制器(三级渐进+观察者钟检测)
- CrashExploitSignatureDB: 崩溃漏洞签名库(12种已知漏洞+4级自动响应)

【AI/ML 行为分析 4 模块】
- BehavioralProfilingEngine: 行为分析引擎(5维特征+香农熵+Z分数)
- ThreatScoreAggregator: 威胁分数聚合器(指数衰减+4级上报)
- MovementPatternAnalyzer: 移动模式分析器(FFT频谱+旋转分析)
- CombatPatternRecognizer: 战斗模式识别器(CPS方差+多目标熵+扫视分析)

配置更新:AlertType新增39个枚举值,SuperEvolutionConfig新增39个开关,
application.yml新增39个配置项。323项全量测试通过,0失败。
2026-05-16 18:02:49 +08:00

553 lines
21 KiB
Java
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
package com.aluer.ml;
import com.aluer.config.ServerGuardConfig;
import org.apache.commons.math3.stat.descriptive.DescriptiveStatistics;
import org.apache.commons.math3.stat.inference.TTest;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import java.time.Instant;
import java.util.*;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.ConcurrentLinkedDeque;
import java.util.concurrent.atomic.AtomicLong;
/**
* 玩家行为画像引擎 — 基于统计分析的玩家行为模式识别
*
* 核心算法:
* 1. 对每个在线玩家维护5维行为特征向量(移动熵、战斗率、资源采集率、社交率、探索率)
* 2. 使用滑动窗口(~200事件)计算每个特征的统计分布
* 3. 将玩家特征与全服基线进行Z-score比较,计算复合异常分数(0-100)
* 4. 将玩家行为分类为六种画像:NORMAL / GRINDER / EXPLORER / PVPER / SUSPICIOUS / BOT_LIKE
* 5. 追踪画像转换序列 — NORMAL→SUSPICIOUS 是高风险信号
* 6. 30分钟无活动自动清除玩家画像数据
*
* 数学基础:
* - 香农熵:H(X) = -Sum(p_i * log2(p_i)) 用于度量移动方向随机性
* - Z-score: z = (x - mu_pop) / sigma_pop 衡量玩家偏离全服均值的标准差倍数
* - 复合异常分数:加权各维度的Z-score绝对值,映射到0-100区间
*
* 依赖:Apache Commons Math3 — DescriptiveStatistics(统计描述)、TTest(假设检验)
*/
@Service
public class BehavioralProfilingEngine {
private static final Logger logger = LoggerFactory.getLogger(BehavioralProfilingEngine.class);
private final ServerGuardConfig config;
/** 每个玩家的行为事件滑动窗口(playerName -> Deque<BehaviorEvent>) */
private final Map<String, ConcurrentLinkedDeque<BehaviorEvent>> playerEventWindows = new ConcurrentHashMap<>();
/** 每个玩家的当前画像(playerName -> 画像枚举) */
private final Map<String, PlayerProfile> playerProfiles = new ConcurrentHashMap<>();
/** 每个玩家最近一次画像及转换时间(playerName -> ProfileTransition) */
private final Map<String, List<ProfileTransition>> profileTransitions = new ConcurrentHashMap<>();
/** 全服各维度的基线统计量 — 使用 DescriptiveStatistics 增量维护 */
private final Map<String, DescriptiveStatistics> populationBaselines = new ConcurrentHashMap<>();
private final AtomicLong totalEventsProcessed = new AtomicLong(0);
private final AtomicLong anomalyFlags = new AtomicLong(0);
/** 滑动窗口最大事件数 */
private static final int MAX_WINDOW_SIZE = 200;
/** 画像计算所需最小样本数 */
private static final int MIN_SAMPLES_FOR_PROFILE = 30;
/** 非活跃玩家清理超时(毫秒) */
private static final long INACTIVITY_CLEANUP_MS = 30 * 60 * 1000L;
/** 异常Z-score阈值 — 超过此值认为该维度显著偏离 */
private static final double ANOMALY_Z_THRESHOLD = 2.5;
/** 各维度的异常分数权重(总和为1.0,可根据业务调整) */
private static final double[] FEATURE_WEIGHTS = {0.25, 0.20, 0.15, 0.15, 0.25};
/** 画像转换风险权重乘数 — NORMAL→SUSPICIOUS 方向性加分 */
private static final double TRANSITION_RISK_MULTIPLIER = 1.5;
public BehavioralProfilingEngine() {
this(new ServerGuardConfig());
}
@Autowired
public BehavioralProfilingEngine(ServerGuardConfig config) {
this.config = config;
// 初始化全服基线统计容器
for (String feature : FeatureDimension.FEATURE_NAMES) {
populationBaselines.put(feature, new DescriptiveStatistics());
}
}
/**
* 记录一次玩家行为事件,更新玩家特征向量并触发画像评估。
*
* 调用时机:每次玩家产生可观测行为(移动、攻击、挖掘、发言、探索新区块)时调用。
*
* @param playerName 玩家名称
* @param eventType 事件类型(MOVE / ATTACK / BREAK_BLOCK / CHAT / EXPLORE)
* @param eventValue 事件关联数值(如移动距离、攻击次数等)
* @param timestamp 事件时间戳
* @return 画像评估结果
*/
public ProfilingResult recordEvent(String playerName, String eventType,
double eventValue, Instant timestamp) {
totalEventsProcessed.incrementAndGet();
// 获取或创建玩家事件窗口
ConcurrentLinkedDeque<BehaviorEvent> window = playerEventWindows.computeIfAbsent(
playerName, k -> new ConcurrentLinkedDeque<>());
// 添加新事件并维护窗口大小
window.addLast(new BehaviorEvent(timestamp, eventType, eventValue));
while (window.size() > MAX_WINDOW_SIZE) {
window.removeFirst();
}
// 样本不足时返回正常(避免小样本误判)
if (window.size() < MIN_SAMPLES_FOR_PROFILE) {
return ProfilingResult.clean(playerName, PlayerProfile.NORMAL, 0.0);
}
// 从滑动窗口中提取5维特征向量
double[] features = extractFeatureVector(window, timestamp);
// 将当前特征向量加入全服基线
updatePopulationBaselines(features);
// 计算各维度Z-score
double[] zScores = calculateZScores(features);
// 计算加权复合异常分数(0-100)
double anomalyScore = computeCompositeAnomalyScore(zScores);
// 基于特征向量确定玩家画像
PlayerProfile newProfile = classifyProfile(features, anomalyScore);
// 追踪画像转换并检测异常转换
PlayerProfile previousProfile = playerProfiles.get(playerName);
playerProfiles.put(playerName, newProfile);
boolean isSuspiciousTransition = false;
List<String> flags = new ArrayList<>();
if (previousProfile != null && previousProfile != newProfile) {
// 记录画像转换
List<ProfileTransition> transitions = profileTransitions.computeIfAbsent(
playerName, k -> new ArrayList<>());
transitions.add(new ProfileTransition(previousProfile, newProfile, timestamp));
// 保留最近20次转换记录
while (transitions.size() > 20) {
transitions.remove(0);
}
// 检测高风险转换:NORMAL/GRINDER → SUSPICIOUS/BOT_LIKE
if (isRiskyTransition(previousProfile, newProfile)) {
isSuspiciousTransition = true;
anomalyScore = Math.min(100.0, anomalyScore * TRANSITION_RISK_MULTIPLIER);
flags.add("RISKY_TRANSITION: " + previousProfile + " -> " + newProfile);
}
}
// 检查各维度是否超过异常阈值
for (int i = 0; i < zScores.length; i++) {
if (Math.abs(zScores[i]) > ANOMALY_Z_THRESHOLD) {
flags.add("FEATURE_ANOMALY[" + FeatureDimension.FEATURE_NAMES[i]
+ "]: z-score=" + String.format("%.2f", zScores[i]));
}
}
if (anomalyScore > 60.0 || isSuspiciousTransition || !flags.isEmpty()) {
anomalyFlags.incrementAndGet();
return ProfilingResult.flagged(playerName, newProfile, anomalyScore, flags);
}
if (anomalyScore > 30.0) {
return ProfilingResult.suspicious(playerName, newProfile, anomalyScore, flags);
}
return ProfilingResult.clean(playerName, newProfile, anomalyScore);
}
/**
* 从玩家事件滑动窗口提取5维特征向量。
*
* 特征维度:
* [0] 移动熵 — 方向变化的香农熵(正常玩家方向多变,bot方向高度一致)
* [1] 战斗参与率 — 每秒攻击次数(PVPer高,普通玩家低)
* [2] 资源采集率 — 每秒方块破坏数(矿工/挂机脚本高)
* [3] 社交交互率 — 每秒聊天消息数
* [4] 探索率 — 每分钟新探索区块数
*/
private double[] extractFeatureVector(Deque<BehaviorEvent> window, Instant now) {
double moveCount = 0, attackCount = 0, breakCount = 0, chatCount = 0, exploreCount = 0;
List<Double> moveDeltas = new ArrayList<>();
Instant oldest = window.isEmpty() ? now : window.peekFirst().timestamp;
double durationMinutes = Math.max(1.0 / 60.0,
(now.toEpochMilli() - oldest.toEpochMilli()) / 60000.0);
for (BehaviorEvent e : window) {
switch (e.eventType) {
case "MOVE":
moveCount++;
moveDeltas.add(e.eventValue);
break;
case "ATTACK":
attackCount++;
break;
case "BREAK_BLOCK":
breakCount++;
break;
case "CHAT":
chatCount++;
break;
case "EXPLORE":
exploreCount++;
break;
}
}
// [0] 移动熵 — 方向变化的信息熵
double movementEntropy = calculateDirectionEntropy(moveDeltas);
// [1] 战斗参与率 = 攻击次数 / 分钟
double combatRate = attackCount / durationMinutes;
// [2] 资源采集率 = 方块破坏数 / 分钟
double gatheringRate = breakCount / durationMinutes;
// [3] 社交交互率 = 聊天消息数 / 分钟
double socialRate = chatCount / durationMinutes;
// [4] 探索率 = 新区块探索数 / 分钟
double explorationRate = exploreCount / durationMinutes;
return new double[]{movementEntropy, combatRate, gatheringRate, socialRate, explorationRate};
}
/**
* 计算移动方向变量的香农熵。
* 将方向值离散化到12个桶(每30度一个桶),计算频率分布的熵。
* 高熵 = 方向随机多变 = 人类玩家;低熵 = 方向单一 = bot/脚本。
*/
private double calculateDirectionEntropy(List<Double> deltas) {
if (deltas.size() < 10) return 0.0;
int[] buckets = new int[12]; // 360度 / 30度 = 12个方向桶
for (double d : deltas) {
int idx = ((int) (Math.abs(d) % 360.0) / 30) % 12;
buckets[idx]++;
}
double entropy = 0.0;
double n = deltas.size();
for (int count : buckets) {
if (count > 0) {
double p = count / n;
entropy -= p * (Math.log(p) / Math.log(2));
}
}
return entropy;
}
/**
* 将当前特征向量更新至全服基线DescriptiveStatistics。
* DescriptiveStatistics 自动维护均值、方差、百分位数等统计量。
*/
private void updatePopulationBaselines(double[] features) {
for (int i = 0; i < features.length; i++) {
DescriptiveStatistics stats = populationBaselines.get(
FeatureDimension.FEATURE_NAMES[i]);
if (stats != null && !Double.isNaN(features[i]) && !Double.isInfinite(features[i])) {
stats.addValue(features[i]);
}
}
}
/**
* 计算各特征维度的Z-score(标准分数)。
* Z = (x - mu_population) / sigma_population
* 如果总体标准差为0(所有玩家值相同),Z-score设为0。
*/
private double[] calculateZScores(double[] features) {
double[] zScores = new double[features.length];
for (int i = 0; i < features.length; i++) {
DescriptiveStatistics stats = populationBaselines.get(
FeatureDimension.FEATURE_NAMES[i]);
if (stats != null && stats.getN() >= MIN_SAMPLES_FOR_PROFILE) {
double mu = stats.getMean();
double sigma = stats.getStandardDeviation();
zScores[i] = (sigma > 1e-10) ? (features[i] - mu) / sigma : 0.0;
} else {
zScores[i] = 0.0;
}
}
return zScores;
}
/**
* 计算加权复合异常分数(0-100)。
* 将各维度Z-score的绝对值加权求和,再通过sigmoid-like函数映射到0-100区间。
*/
private double computeCompositeAnomalyScore(double[] zScores) {
double weightedSum = 0.0;
for (int i = 0; i < zScores.length; i++) {
weightedSum += Math.abs(zScores[i]) * FEATURE_WEIGHTS[i];
}
// 使用双曲正切映射到0-100:score = 100 * tanh(weightedSum / 2)
// tanh(0) = 0, tanh(1.5) ≈ 0.9, tanh(3) ≈ 0.995
return Math.min(100.0, 100.0 * Math.tanh(weightedSum / 2.0));
}
/**
* 基于特征向量和异常分数对玩家画像进行分类。
*
* 分类规则(启发式阈值):
* - BOT_LIKE: 移动熵极低(< 0.5)且资源采集率极高(> 30/min)
* - SUSPICIOUS: 异常分数 > 50
* - PVPER: 战斗参与率 > 5/min
* - GRINDER: 资源采集率 > 20/min 或 探索率 > 15/min
* - EXPLORER: 探索率 > 10/min
* - NORMAL: 以上均不满足
*/
private PlayerProfile classifyProfile(double[] features, double anomalyScore) {
double movementEntropy = features[0];
double combatRate = features[1];
double gatheringRate = features[2];
double socialRate = features[3];
double explorationRate = features[4];
// Bot特征:移动高度规律 + 高采集频率
if (movementEntropy < 0.5 && gatheringRate > 30.0) {
return PlayerProfile.BOT_LIKE;
}
// 显著异常
if (anomalyScore > 50.0) {
return PlayerProfile.SUSPICIOUS;
}
// 高战斗率 = PVP玩家
if (combatRate > 5.0) {
return PlayerProfile.PVPER;
}
// 高采集率 = 刷资源型
if (gatheringRate > 20.0) {
return PlayerProfile.GRINDER;
}
// 高探索率 = 探险型
if (explorationRate > 10.0) {
return PlayerProfile.EXPLORER;
}
return PlayerProfile.NORMAL;
}
/**
* 判断画像转换是否为高风险转换。
* 从无害画像(NORMAL/GRINDER/EXPLORER)跳转到可疑画像(SUSPICIOUS/BOT_LIKE)视为危险信号。
*/
private boolean isRiskyTransition(PlayerProfile from, PlayerProfile to) {
Set<PlayerProfile> benignSet = EnumSet.of(PlayerProfile.NORMAL,
PlayerProfile.GRINDER, PlayerProfile.EXPLORER, PlayerProfile.PVPER);
Set<PlayerProfile> maliciousSet = EnumSet.of(PlayerProfile.SUSPICIOUS,
PlayerProfile.BOT_LIKE);
return benignSet.contains(from) && maliciousSet.contains(to);
}
/**
* 清理超过30分钟无活动的玩家数据,释放内存。
*/
public void cleanupInactiveProfiles() {
Instant cutoff = Instant.now().minusMillis(INACTIVITY_CLEANUP_MS);
Iterator<Map.Entry<String, ConcurrentLinkedDeque<BehaviorEvent>>> it =
playerEventWindows.entrySet().iterator();
while (it.hasNext()) {
Map.Entry<String, ConcurrentLinkedDeque<BehaviorEvent>> entry = it.next();
ConcurrentLinkedDeque<BehaviorEvent> window = entry.getValue();
if (window.isEmpty() || window.peekLast().timestamp.isBefore(cutoff)) {
it.remove();
playerProfiles.remove(entry.getKey());
profileTransitions.remove(entry.getKey());
}
}
}
/**
* 获取指定玩家的当前画像。
*/
public PlayerProfile getPlayerProfile(String playerName) {
return playerProfiles.getOrDefault(playerName, PlayerProfile.NORMAL);
}
/**
* 获取指定玩家的行为特征向量快照。
*/
public double[] getPlayerFeatureVector(String playerName) {
ConcurrentLinkedDeque<BehaviorEvent> window = playerEventWindows.get(playerName);
if (window == null || window.isEmpty()) {
return new double[]{0, 0, 0, 0, 0};
}
return extractFeatureVector(window, Instant.now());
}
/**
* 获取指定玩家的画像转换历史。
*/
public List<ProfileTransition> getTransitionHistory(String playerName) {
return profileTransitions.getOrDefault(playerName, Collections.emptyList());
}
/**
* 清除指定玩家的所有数据。
*/
public void clearPlayer(String playerName) {
playerEventWindows.remove(playerName);
playerProfiles.remove(playerName);
profileTransitions.remove(playerName);
}
/**
* 获取引擎运行状态统计。
*/
public Map<String, Object> getStatus() {
Map<String, Object> status = new LinkedHashMap<>();
status.put("totalEvents", totalEventsProcessed.get());
status.put("anomalyFlags", anomalyFlags.get());
status.put("activePlayers", playerEventWindows.size());
status.put("populationSampleSize",
populationBaselines.values().iterator().next().getN());
// 各画像玩家分布
Map<PlayerProfile, Long> profileDistribution = new EnumMap<>(PlayerProfile.class);
for (PlayerProfile p : playerProfiles.values()) {
profileDistribution.merge(p, 1L, Long::sum);
}
status.put("profileDistribution", profileDistribution);
return status;
}
// ==================== 内部数据类 ====================
/**
* 行为事件记录 — 包含时间戳、事件类型和关联数值。
*/
private static class BehaviorEvent {
final Instant timestamp;
final String eventType;
final double eventValue;
BehaviorEvent(Instant timestamp, String eventType, double eventValue) {
this.timestamp = timestamp;
this.eventType = eventType;
this.eventValue = eventValue;
}
}
/**
* 特征维度常量定义。
*/
private static final class FeatureDimension {
static final String[] FEATURE_NAMES = {
"MOVEMENT_ENTROPY", "COMBAT_RATE", "GATHERING_RATE",
"SOCIAL_RATE", "EXPLORATION_RATE"
};
}
/**
* 画像转换记录 — 记录从哪个画像切换到哪个画像及时间。
*/
public static class ProfileTransition {
public final PlayerProfile from;
public final PlayerProfile to;
public final Instant timestamp;
ProfileTransition(PlayerProfile from, PlayerProfile to, Instant timestamp) {
this.from = from;
this.to = to;
this.timestamp = timestamp;
}
@Override
public String toString() {
return from + " -> " + to + " @ " + timestamp;
}
}
// ==================== 枚举与结果类 ====================
/**
* 玩家行为画像枚举。
*/
public enum PlayerProfile {
NORMAL, // 正常玩家 — 无异常行为模式
GRINDER, // 刷资源型 — 高频率采集/挖掘
EXPLORER, // 探险型 — 高频率新区块探索
PVPER, // PVP型 — 高频率战斗参与
SUSPICIOUS, // 可疑 — 多维度偏离基线
BOT_LIKE // Bot特征 — 移动极度规律 + 自动化采集
}
/**
* 行为画像评估结果 — 不可变结果类。
*/
public static class ProfilingResult {
private final boolean flagged;
private final boolean suspicious;
private final String playerName;
private final PlayerProfile profile;
private final double anomalyScore;
private final List<String> flags;
private ProfilingResult(boolean flagged, boolean suspicious, String playerName,
PlayerProfile profile, double anomalyScore, List<String> flags) {
this.flagged = flagged;
this.suspicious = suspicious;
this.playerName = playerName;
this.profile = profile;
this.anomalyScore = anomalyScore;
this.flags = flags;
}
/** 正常 — 无任何异常信号 */
public static ProfilingResult clean(String playerName, PlayerProfile profile,
double anomalyScore) {
return new ProfilingResult(false, false, playerName, profile,
anomalyScore, List.of());
}
/** 可疑 — 存在异常信号但未达到标记阈值 */
public static ProfilingResult suspicious(String playerName, PlayerProfile profile,
double anomalyScore, List<String> flags) {
return new ProfilingResult(false, true, playerName, profile,
anomalyScore, flags);
}
/** 已标记 — 检测到显著异常行为 */
public static ProfilingResult flagged(String playerName, PlayerProfile profile,
double anomalyScore, List<String> flags) {
return new ProfilingResult(true, true, playerName, profile,
anomalyScore, flags);
}
public boolean isFlagged() { return flagged; }
public boolean isSuspicious() { return suspicious; }
public boolean isClean() { return !flagged && !suspicious; }
public String getPlayerName() { return playerName; }
public PlayerProfile getProfile() { return profile; }
public double getAnomalyScore() { return anomalyScore; }
public List<String> getFlags() { return flags; }
}
}