'use strict'; /** * 自动扩地配置。 * * 字段命名与原 Java 版 `ExpansionConfig` 保持一一对应(便于对照移植正确性), * 新增能力统一收拢在三个分组对象里: * - `pathing`:mineflayer-pathfinder 专属的寻路开关 * - `grim`:Grim 反作弊合规层的阈值 * - `server`:与服务器交互的指令与节流 */ /** 扩地策略枚举 */ const STRATEGY = { /** 方形外扩(优先填洞) */ SQUARE: 1, /** 区域扩地(S 形蛇形扫描) */ REGION: 2, /** 玩家周围扩地 */ AROUND_PLAYER: 3, /** 绕国一圈 */ PERIMETER: 4 }; /** 水域处理策略 */ const WATER_MODE = { /** 绕开水域(默认,最安全) */ AVOID: 'avoid', /** 游泳通过 */ SWIM: 'swim', /** 放船渡水 */ BOAT: 'boat' }; /** * 生成一份默认配置。 * @returns {object} */ function defaultConfig() { return { // ==================== 扩地策略(与 Java 版同名) ==================== /** 扩地策略:1=方形外扩,2=区域扩地,3=玩家周围,4=绕国一圈 */ strategy: STRATEGY.SQUARE, /** 目标邦国名称;为空时表示自动识别玩家当前所在邦国 */ stateName: '', /** 目标扩地数量;-1 表示持续扩地直到无可用区块 */ targetCount: -1, /** 扩圈上限(0 表示不限制) */ maxRings: 0, // ==================== 区域扩地范围(与 Java 版同名) ==================== regionCenterX: 0, regionCenterZ: 0, regionRadius: 5, /** 矩形边界(区块坐标);maxX < minX 表示未圈选,回退到中心+半径 */ regionMinX: 0, regionMaxX: -1, regionMinZ: 0, regionMaxZ: -1, // ==================== 超时与重试(与 Java 版同名) ==================== /** 等待 / 寻路超时时间(秒) */ waitTimeoutSeconds: 60, /** 超时后的最大重试次数 */ retries: 2, /** 连续寻路超时累计达到该秒数后,发送回城指令并重新开始 */ returnThresholdSeconds: 60, /** 发送回城指令后等待传送完成的时间(秒) */ returnWaitSeconds: 12, /** 单个目标最多触发「往边上走两步」自救的轮数 */ sideStepMaxRounds: 3, // ==================== 开垦时段限制(新增,原版硬编码 11:00-22:00) ==================== /** 是否启用开垦时段限制(服务器只在特定时段接受开拓) */ enforceClaimTime: true, /** 允许开拓的现实时间起始小时(含,本地时区) */ claimStartHour: 11, /** 允许开拓的现实时间结束小时(不含,本地时区) */ claimEndHour: 22, // ==================== 寻路基础开关(与 Java 版同名) ==================== allowSprint: true, allowSwim: true, allowJump: true, allowBreak: false, allowPlace: false, /** 寻路高度偏好:优先往高处走 */ altitudePreference: true, /** 高度偏好权重:每上升 1 格折算的启发成本减少量 */ altitudeWeight: 0.1, // ==================== 生存辅助(与 Java 版同名) ==================== avoidMobs: true, autoEat: true, /** 触发自动进食的饥饿值阈值(0-20) */ eatThreshold: 14, autoAttack: true, durabilityGuard: true, /** 耐久保护阈值:剩余耐久百分比 ≤ 该值时回城(1-99) */ durabilityThreshold: 15, /** 是否启用自动用船过水 */ autoBoat: false, /** 触发自动用船的水面最小宽度(格) */ autoBoatMinWidth: 4, /** 自动用船最短水域长度(格) */ autoBoatMinLength: 6, /** 水域处理策略:avoid / swim / boat */ waterMode: WATER_MODE.AVOID, // ==================== 数据源(与 Java 版同名) ==================== /** 地图自动刷新间隔(秒),最小 10 秒 */ refreshIntervalSeconds: 30, /** 是否启用 SimpCore 开放 API 作为占领判定补充来源 */ apiEnabled: false, /** SimpCore 开放 API Key(留空时回退读环境变量 SIMP_API_KEY) */ apiKey: '', /** API 领土数据轮询间隔(秒),最小 60 */ apiRefreshIntervalSeconds: 180, // ==================== 寻路器专属(新增) ==================== pathing: { /** 是否允许跑酷跳跃(mineflayer 起跳时机与原版有偏差,默认关闭更安全) */ allowParkour: false, /** 是否允许 1×1 搭塔(Grim 的 place 类检查极易命中,默认关闭) */ allow1by1towers: false, /** 允许的最大下落高度(格),超过则绕行 */ maxDropDown: 3, /** 进入液体格的额外代价(越大越倾向绕开水域) */ liquidCost: 40, /** 是否允许打开门/栅栏门 */ canOpenDoors: false }, // ==================== 战斗辅助(新增) ==================== combat: { /** 自动攻击索敌半径(格):只打进入该范围的敌对生物,不追击 */ scanRange: 4.5, /** 是否攻击末影人(mineflayer 读不到「是否被激怒」,默认不打) */ attackEnderman: false, /** 是否攻击蜘蛛(mineflayer 读不到环境亮度,默认不打) */ attackSpiders: false, /** 是否攻击猪灵(仅在未穿金盔甲时才会攻击) */ attackPiglins: true, /** 是否跳过高危生物(监守者 / 末影龙 / 凋灵),默认跳过 */ avoidDangerousMobs: true }, // ==================== Grim 反作弊合规层(新增) ==================== grim: { /** 总开关:关闭后不做任何限速/校验(仅用于本地测试服) */ enabled: true, /** 每 tick 允许的最大旋转角度(度);mineflayer 默认 yawSpeed=3.0 rad/s ≈ 8.6°/tick */ maxRotationPerTickDeg: 8.6, /** 攻击判定距离(格),生存模式实体交互距离为 3.0 */ attackReach: 3.0, /** 攻击前是否要求视线可达(防 Reach / LineOfSight 检查) */ requireLineOfSight: true, /** 是否遵守攻击冷却(1.9+ 武器攻速) */ respectAttackCooldown: true, /** 进食期间是否停止移动(原版进食会取消疾跑并减速,防 NoSlow / MultiActions) */ stopMovementWhileEating: true, /** * 是否禁止疾跑跳跃。 * 疾跑跳本身是原版合法动作,Grim 的预测支持;默认放行。 * 若实测被标记,可开启此项退化为普通跳跃(代价是移动变慢)。 */ blockSprintJump: false, /** 是否禁止疾跑与潜行/使用物品同时进行(防 MultiActions) */ blockConflictingActions: true, /** 是否在启动时校验物理参数未被篡改 */ checkPhysicsIntegrity: true, /** 是否禁止在骑乘状态下攻击 / 进食(防 Vehicle 类检查) */ blockActionsWhileRiding: true }, // ==================== 服务器交互(新增) ==================== server: { /** 启用开垦模式的指令(不带斜杠),留空表示服务器自动开启 */ claimCommand: 'u claimmode', /** 登录指令模板,`{password}` 会被替换为 .env 中的 MC_PASSWORD;留空则不发送 */ loginCommand: 'login {password}', /** 注册指令模板,`{password}` 会被替换;留空则不发送 */ registerCommand: '', /** 聊天指令最小间隔(毫秒),防刷屏 */ commandCooldownMs: 1200, /** 断线后是否自动重连 */ autoReconnect: true, /** 重连间隔(毫秒) */ reconnectDelayMs: 10000 } }; } /** * 深拷贝配置。 * @param {object} config * @returns {object} */ function copyConfig(config) { return JSON.parse(JSON.stringify(config)); } /** * 把外部传入(配置文件 / Web 控制台)的部分配置合并到默认配置上,并做范围钳制。 * 只接受已知字段,未知字段一律丢弃,避免脏配置污染运行状态。 * * @param {object} partial 用户提供的部分配置 * @param {object} [base] 基线配置(默认取 defaultConfig()) * @returns {object} 规范化后的完整配置 */ function normalizeConfig(partial, base) { const defaults = base || defaultConfig(); const merged = copyConfig(defaults); if (!partial || typeof partial !== 'object') return validateConfig(merged); for (const key of Object.keys(merged)) { const value = partial[key]; if (value === undefined) continue; if (value && typeof value === 'object' && !Array.isArray(value) && typeof merged[key] === 'object') { for (const subKey of Object.keys(merged[key])) { if (value[subKey] !== undefined) merged[key][subKey] = value[subKey]; } } else { merged[key] = value; } } return validateConfig(merged); } /** * 钳制配置中的越界值,防止异常配置导致「永不回城」或「刚出门就回城」。 * * 会先把缺失的字段(含嵌套分组)补齐为默认值,因此可以直接传入部分配置。 * * @param {object} config * @returns {object} 同一个对象(原地修正) */ function validateConfig(config) { const base = defaultConfig(); for (const key of Object.keys(base)) { const defaultValue = base[key]; if (defaultValue !== null && typeof defaultValue === 'object') { config[key] = Object.assign({}, defaultValue, config[key] || {}); } else if (config[key] === undefined) { config[key] = defaultValue; } } config.strategy = clampEnum(config.strategy, [1, 2, 3, 4], STRATEGY.SQUARE); config.targetCount = clampInt(config.targetCount, -1, 1e9, -1); config.maxRings = clampInt(config.maxRings, 0, 1000, 0); config.regionRadius = clampInt(config.regionRadius, 1, 512, 5); config.regionMinX = clampInt(config.regionMinX, -1e6, 1e6, 0); config.regionMaxX = clampInt(config.regionMaxX, -1e6, 1e6, -1); config.regionMinZ = clampInt(config.regionMinZ, -1e6, 1e6, 0); config.regionMaxZ = clampInt(config.regionMaxZ, -1e6, 1e6, -1); config.waitTimeoutSeconds = clampInt(config.waitTimeoutSeconds, 5, 3600, 60); config.retries = clampInt(config.retries, 1, 20, 2); config.returnThresholdSeconds = clampInt(config.returnThresholdSeconds, 0, 3600, 60); config.returnWaitSeconds = clampInt(config.returnWaitSeconds, 1, 120, 12); config.sideStepMaxRounds = clampInt(config.sideStepMaxRounds, 0, 10, 3); config.claimStartHour = clampInt(config.claimStartHour, 0, 23, 11); config.claimEndHour = clampInt(config.claimEndHour, 1, 24, 22); config.eatThreshold = clampInt(config.eatThreshold, 0, 20, 14); config.durabilityThreshold = clampInt(config.durabilityThreshold, 1, 99, 15); config.autoBoatMinWidth = clampInt(config.autoBoatMinWidth, 1, 64, 4); config.autoBoatMinLength = clampInt(config.autoBoatMinLength, 1, 128, 6); config.refreshIntervalSeconds = clampInt(config.refreshIntervalSeconds, 10, 3600, 30); config.apiRefreshIntervalSeconds = clampInt(config.apiRefreshIntervalSeconds, 60, 3600, 180); config.altitudeWeight = clampNumber(config.altitudeWeight, 0, 10, 0.1); config.pathing.maxDropDown = clampInt(config.pathing.maxDropDown, 0, 64, 3); config.pathing.liquidCost = clampInt(config.pathing.liquidCost, 1, 1000, 40); config.combat.scanRange = clampNumber(config.combat.scanRange, 1, 32, 4.5); config.grim.maxRotationPerTickDeg = clampNumber(config.grim.maxRotationPerTickDeg, 1, 180, 8.6); config.grim.attackReach = clampNumber(config.grim.attackReach, 1, 6, 3.0); config.server.commandCooldownMs = clampInt(config.server.commandCooldownMs, 0, 60000, 1200); config.server.reconnectDelayMs = clampInt(config.server.reconnectDelayMs, 1000, 600000, 10000); if (config.waterMode !== WATER_MODE.SWIM && config.waterMode !== WATER_MODE.BOAT) { config.waterMode = WATER_MODE.AVOID; } if (typeof config.stateName !== 'string') config.stateName = ''; if (typeof config.apiKey !== 'string') config.apiKey = ''; if (typeof config.server.claimCommand !== 'string') config.server.claimCommand = ''; if (typeof config.server.loginCommand !== 'string') config.server.loginCommand = ''; if (typeof config.server.registerCommand !== 'string') config.server.registerCommand = ''; return config; } function clampInt(value, min, max, fallback) { const n = Number(value); if (!Number.isFinite(n)) return fallback; return Math.min(max, Math.max(min, Math.trunc(n))); } /** * 枚举字段校验:不在允许集合内时回退到默认值。 * * 之所以不用「钳制到最近边界」,是因为策略这类枚举没有大小含义, * 把非法的 99 静默变成「策略 4 绕国一圈」比回退到默认策略更危险。 * * @param {*} value * @param {Array<*>} allowed * @param {*} fallback * @returns {*} */ function clampEnum(value, allowed, fallback) { return allowed.includes(value) ? value : fallback; } function clampNumber(value, min, max, fallback) { const n = Number(value); if (!Number.isFinite(n)) return fallback; return Math.min(max, Math.max(min, n)); } module.exports = { STRATEGY, WATER_MODE, defaultConfig, copyConfig, normalizeConfig, validateConfig };