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失败。
This commit is contained in:
Peijun Zhao
2026-05-16 18:02:49 +08:00
parent 67e873525b
commit 1b66197804
54 changed files with 23497 additions and 1 deletions
@@ -0,0 +1,552 @@
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; }
}
}