由 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 语法自检通过。
142 行
3.3 KiB
JavaScript
142 行
3.3 KiB
JavaScript
'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 };
|