feat: 用 Node.js + mineflayer 重构 AutoStore,UI 改为网页控制台并适配 Grim 反作弊
由 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 语法自检通过。
这个提交包含在:
@@ -0,0 +1,141 @@
|
||||
'use strict';
|
||||
|
||||
const { EventEmitter } = require('events');
|
||||
const { Vec3 } = require('vec3');
|
||||
|
||||
/** 真实的 1.21.4 注册表(mineflayer-pathfinder 的 Movements 依赖它) */
|
||||
let cachedRegistry = null;
|
||||
function defaultRegistry() {
|
||||
if (!cachedRegistry) {
|
||||
cachedRegistry = require('minecraft-data')('1.21.4');
|
||||
}
|
||||
return cachedRegistry;
|
||||
}
|
||||
|
||||
/**
|
||||
* 测试用的假 bot:只实现被测模块真正用到的接口,
|
||||
* 避免测试依赖真实 Minecraft 服务器。
|
||||
*/
|
||||
class FakeBot extends EventEmitter {
|
||||
constructor(options) {
|
||||
super();
|
||||
const opts = options || {};
|
||||
this.username = opts.username || 'TestBot';
|
||||
this.physicsEnabled = true;
|
||||
this.usingHeldItem = false;
|
||||
this.vehicle = null;
|
||||
this.food = 20;
|
||||
this.health = 20;
|
||||
this.game = { gameMode: opts.gameMode || 'survival' };
|
||||
this.physics = Object.assign({
|
||||
gravity: 0.08,
|
||||
playerSpeed: 0.1,
|
||||
sprintSpeed: 0.3,
|
||||
sneakSpeed: 0.3,
|
||||
stepHeight: 0.6,
|
||||
airborneInertia: 0.91,
|
||||
waterInertia: 0.8,
|
||||
yawSpeed: 3.0,
|
||||
pitchSpeed: 3.0
|
||||
}, opts.physics || {});
|
||||
this.entity = {
|
||||
position: new Vec3(opts.x || 0, opts.y || 64, opts.z || 0),
|
||||
velocity: new Vec3(0, 0, 0),
|
||||
yaw: 0,
|
||||
pitch: 0,
|
||||
eyeHeight: 1.62,
|
||||
isInWater: false,
|
||||
isInLava: false,
|
||||
onGround: true
|
||||
};
|
||||
/** @type {Record<string, boolean>} */
|
||||
this.controls = {};
|
||||
this.heldItem = opts.heldItem || null;
|
||||
this.inventory = { slots: opts.slots || [] };
|
||||
this.entities = {};
|
||||
this.world = opts.world || { raycast: () => null };
|
||||
this.registry = opts.registry || defaultRegistry();
|
||||
this.pathfinder = null;
|
||||
this.blockMap = opts.blockMap || new Map();
|
||||
this.creative = { flying: false };
|
||||
}
|
||||
|
||||
getControlState(control) {
|
||||
return this.controls[control] === true;
|
||||
}
|
||||
|
||||
setControlState(control, state) {
|
||||
this.controls[control] = state === true;
|
||||
this.emit('controlState', control, state);
|
||||
}
|
||||
|
||||
blockAt(pos) {
|
||||
const key = `${Math.floor(pos.x)},${Math.floor(pos.y)},${Math.floor(pos.z)}`;
|
||||
if (this.blockMap.has(key)) return this.blockMap.get(key);
|
||||
return { name: 'air', boundingBox: 'empty' };
|
||||
}
|
||||
|
||||
look(yaw, pitch) {
|
||||
this.entity.yaw = yaw;
|
||||
this.entity.pitch = pitch === undefined ? this.entity.pitch : pitch;
|
||||
return Promise.resolve();
|
||||
}
|
||||
|
||||
lookAt(point) {
|
||||
return this.look(this.entity.yaw, this.entity.pitch);
|
||||
}
|
||||
|
||||
attack(entity, swing) {
|
||||
this.lastAttack = { entity, swing };
|
||||
}
|
||||
|
||||
activateItem() {
|
||||
this.usingHeldItem = true;
|
||||
}
|
||||
|
||||
deactivateItem() {
|
||||
this.usingHeldItem = false;
|
||||
}
|
||||
|
||||
async waitForTicks(ticks) {
|
||||
for (let i = 0; i < (ticks || 1); i++) this.emit('physicsTick');
|
||||
}
|
||||
|
||||
chat(message) {
|
||||
this.lastChat = message;
|
||||
}
|
||||
|
||||
equip() {
|
||||
return Promise.resolve();
|
||||
}
|
||||
|
||||
consume() {
|
||||
this.usingHeldItem = false;
|
||||
this.food = 20;
|
||||
return Promise.resolve();
|
||||
}
|
||||
|
||||
mount() {
|
||||
this.vehicle = { id: 99, position: this.entity.position.clone(), name: 'oak_boat' };
|
||||
}
|
||||
|
||||
dismount() {
|
||||
this.vehicle = null;
|
||||
}
|
||||
|
||||
moveVehicle() {
|
||||
this.lastVehicleMove = Array.from(arguments);
|
||||
}
|
||||
|
||||
/** 推进一个物理 tick(触发合规层的旋转推进) */
|
||||
tick() {
|
||||
this.emit('physicsTick');
|
||||
}
|
||||
|
||||
/** 推进 n 个物理 tick */
|
||||
tickTimes(n) {
|
||||
for (let i = 0; i < n; i++) this.emit('physicsTick');
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = { FakeBot };
|
||||
在新工单中引用
屏蔽一个用户