由 Fabric 客户端模组 AutoStore v1.3.1 重构为独立 Node.js 机器人,无需游戏客户端。 核心实现: - 领土数据:Dynmap markers.json 拉取解析(奇偶填充识别国中飞地)、SimpCore 开放 API 第四数据源、 带缓存击穿的时间戳请求与内容未变化跳过解析 - 扩地规划:四种策略(方形外扩 / 区域 S 形扫描 / 玩家周围 / 绕国一圈)逐行对照移植, 接壤判定只算己方领土,飞地优先填补 - 扩地状态机:规划→寻路→等待开垦→成功/失败→下一目标→完成,含超时重试、 往边上走两步自救、连续超时回城、耐久保护回城 - 生存辅助:自动进食、自动攻击(豁免中立生物)、自动游泳防溺水、自动用船(默认关闭) - 寻路:Baritone → mineflayer-pathfinder,高度偏好用包装 Goal 等价移植 Grim 反作弊合规层(新增,核心差异点): - 旋转不超前:接管 bot.look 按 ≤8.6°/tick 推进朝向,消除 mineflayer 「本地朝向瞬间跳变、数据包滞后补发」导致的 Prediction 分歧 - 攻击三重校验:朝向已同步 + 距离 ≤3.0 格 + 视线可达 + 攻击冷却 + 非骑乘/进食 - 动作互斥:拦下疾跑+潜行、疾跑+使用物品等原版不可能出现的组合 - 物理参数只读自检 + 寻路约束(禁挖/禁放/禁跑酷/禁搭塔/限下落/绕开水域) 网页控制台(新增): - 状态面板、Canvas 领土地图(缩放/平移/图层)、分组配置表单、实时日志流 - REST + WebSocket 接口,支持 WEB_TOKEN 令牌鉴权 修复: - containsFuzzy 只取固定长度前缀的末行编辑距离,导致完全相同的子串也匹配不上 - 占领去重依赖 Set.add 返回值(JS 中恒为真),导致同一区块被重复计数 - 非法枚举值不再钳制到最近边界,改为回退默认值 测试:154 个用例,npm test 全绿;npm run check 语法自检通过。
329 行
13 KiB
JavaScript
329 行
13 KiB
JavaScript
'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
|
||
};
|