由 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 语法自检通过。
227 行
8.7 KiB
JavaScript
227 行
8.7 KiB
JavaScript
'use strict';
|
||
|
||
const test = require('node:test');
|
||
const assert = require('node:assert');
|
||
const { Vec3 } = require('vec3');
|
||
|
||
const { FakeBot } = require('./helpers/fakeBot');
|
||
const {
|
||
GrimCompat,
|
||
shortestAngleDelta,
|
||
eyeDistanceToEntity,
|
||
VANILLA_PHYSICS_BASELINE
|
||
} = require('../src/bot/grimCompat');
|
||
const { defaultConfig } = require('../src/config/expansionConfig');
|
||
|
||
/**
|
||
* 构造一个已安装合规层的假 bot。
|
||
* @param {object} [options]
|
||
*/
|
||
function setup(options) {
|
||
const opts = options || {};
|
||
const config = Object.assign(defaultConfig(), opts.config || {});
|
||
const bot = new FakeBot(opts.bot);
|
||
const grim = new GrimCompat(bot, config, null);
|
||
grim.install();
|
||
return { bot, grim, config };
|
||
}
|
||
|
||
test('shortestAngleDelta 走最短路径并处理环绕', () => {
|
||
assert.ok(Math.abs(shortestAngleDelta(0, 0.5) - 0.5) < 1e-9);
|
||
// 从 170° 到 -170° 应走 +20° 而不是 -340°
|
||
const from = (170 * Math.PI) / 180;
|
||
const to = (-170 * Math.PI) / 180;
|
||
assert.ok(Math.abs(shortestAngleDelta(from, to) - (20 * Math.PI) / 180) < 1e-9);
|
||
assert.ok(Math.abs(shortestAngleDelta(0, Math.PI * 2)) < 1e-9);
|
||
});
|
||
|
||
test('eyeDistanceToEntity 计算眼睛到碰撞箱的最近距离', () => {
|
||
const bot = new FakeBot({ x: 0, y: 64, z: 0 });
|
||
// 目标在正前方 2 格,碰撞箱中心高度与眼睛同高
|
||
const entity = { position: new Vec3(2, 64, 0), width: 0.6, height: 1.8 };
|
||
const distance = eyeDistanceToEntity(bot, entity);
|
||
assert.ok(Math.abs(distance - (2 - 0.3)) < 1e-6, `距离应为 1.7,实际 ${distance}`);
|
||
});
|
||
|
||
test('旋转被限制在每 tick 的最大步长内,且不会超前于服务端', async () => {
|
||
const { bot, grim } = setup();
|
||
const targetYaw = Math.PI / 2; // 90°
|
||
const promise = grim.setLook(targetYaw, 0);
|
||
assert.strictEqual(grim.isRotationSettled(), false, '刚设置目标时不应认为已同步');
|
||
|
||
bot.tick();
|
||
const step = grim.maxStepRad;
|
||
assert.ok(Math.abs(bot.entity.yaw - step) < 1e-9, `单 tick 只应前进 ${step} 弧度`);
|
||
assert.ok(bot.entity.yaw < targetYaw, '本地朝向不得一次跳到目标值(否则与服务端预测分歧)');
|
||
|
||
// 推进足够多 tick 后应到位
|
||
for (let i = 0; i < 60; i++) bot.tick();
|
||
await promise;
|
||
assert.ok(Math.abs(bot.entity.yaw - targetYaw) < 1e-4);
|
||
assert.strictEqual(grim.isRotationSettled(), true);
|
||
});
|
||
|
||
test('每 tick 旋转步长不超过 mineflayer 物理限速', () => {
|
||
const { grim } = setup();
|
||
// 物理限速 = 0.05s × 3.0 rad/s = 0.15 rad ≈ 8.59°
|
||
assert.ok(grim.maxStepRad <= 0.15 + 1e-9);
|
||
assert.ok(grim.maxStepRad > 0.14);
|
||
});
|
||
|
||
test('pitch 被钳制在 ±90° 内', async () => {
|
||
const { bot, grim } = setup();
|
||
grim.setLook(0, Math.PI); // 超出范围
|
||
for (let i = 0; i < 60; i++) bot.tick();
|
||
assert.ok(bot.entity.pitch <= Math.PI / 2 + 1e-9);
|
||
assert.ok(bot.entity.pitch >= -Math.PI / 2 - 1e-9);
|
||
});
|
||
|
||
test('拦截疾跑 + 潜行的非法组合(MultiActions)', () => {
|
||
const { bot, grim } = setup();
|
||
grim.setControlState('sprint', true);
|
||
assert.strictEqual(bot.getControlState('sprint'), true);
|
||
grim.setControlState('sneak', true);
|
||
assert.strictEqual(bot.getControlState('sneak'), false, '疾跑时不允许潜行');
|
||
assert.strictEqual(grim.blockedActions, 1);
|
||
});
|
||
|
||
test('拦截疾跑 + 使用物品的非法组合', () => {
|
||
const { bot, grim } = setup();
|
||
grim.setControlState('sprint', true);
|
||
bot.usingHeldItem = true;
|
||
grim.setControlState('forward', true);
|
||
assert.strictEqual(bot.getControlState('forward'), false, '进食时不允许前进(NoSlow)');
|
||
});
|
||
|
||
test('默认放行疾跑跳跃(原版合法动作)', () => {
|
||
const { bot, grim } = setup();
|
||
grim.setControlState('jump', true);
|
||
grim.setControlState('sprint', true);
|
||
assert.strictEqual(bot.getControlState('sprint'), true);
|
||
});
|
||
|
||
test('blockSprintJump 开启时拦下疾跑跳跃', () => {
|
||
const config = defaultConfig();
|
||
config.grim.blockSprintJump = true;
|
||
const { bot, grim } = setup({ config });
|
||
grim.setControlState('jump', true);
|
||
grim.setControlState('sprint', true);
|
||
assert.strictEqual(bot.getControlState('sprint'), false);
|
||
});
|
||
|
||
test('攻击冷却按手持武器攻速计算', () => {
|
||
const config = defaultConfig();
|
||
|
||
const swordBot = new FakeBot({ heldItem: { name: 'diamond_sword' } });
|
||
const swordGrim = new GrimCompat(swordBot, config, null);
|
||
assert.strictEqual(swordGrim.attackCooldownMs(), 625, '钻石剑 1.6 次/秒 → 625ms');
|
||
|
||
const axeBot = new FakeBot({ heldItem: { name: 'netherite_axe' } });
|
||
const axeGrim = new GrimCompat(axeBot, config, null);
|
||
assert.strictEqual(axeGrim.attackCooldownMs(), 1000, '下界合金斧 1.0 次/秒 → 1000ms');
|
||
|
||
const handBot = new FakeBot({ heldItem: null });
|
||
const handGrim = new GrimCompat(handBot, config, null);
|
||
assert.strictEqual(handGrim.attackCooldownMs(), 250, '空手 4 次/秒 → 250ms');
|
||
});
|
||
|
||
test('canAttack 校验距离、视线与冷却', async () => {
|
||
const { bot, grim } = setup();
|
||
bot.entity.position = new Vec3(0, 64, 0);
|
||
const far = { position: new Vec3(10, 64, 0), width: 0.6, height: 1.8, isValid: true };
|
||
assert.deepStrictEqual(grim.canAttack(far), { ok: false, reason: 'out_of_reach' });
|
||
|
||
const near = { position: new Vec3(2, 64, 0), width: 0.6, height: 1.8, isValid: true };
|
||
assert.deepStrictEqual(grim.canAttack(near), { ok: true });
|
||
|
||
// 攻击后进入冷却
|
||
const attacked = await grim.attackEntity(near, { skipRotation: true });
|
||
assert.strictEqual(attacked, true);
|
||
assert.strictEqual(bot.lastAttack.entity, near);
|
||
assert.deepStrictEqual(grim.canAttack(near), { ok: false, reason: 'cooldown' });
|
||
});
|
||
|
||
test('canAttack 拒绝视线被遮挡的目标', () => {
|
||
const config = defaultConfig();
|
||
const bot = new FakeBot({
|
||
world: { raycast: () => ({ position: new Vec3(1, 64, 0) }) } // 总是命中方块
|
||
});
|
||
const grim = new GrimCompat(bot, config, null);
|
||
const target = { position: new Vec3(2, 64, 0), width: 0.6, height: 1.8, isValid: true };
|
||
assert.deepStrictEqual(grim.canAttack(target), { ok: false, reason: 'no_line_of_sight' });
|
||
|
||
config.grim.requireLineOfSight = false;
|
||
assert.deepStrictEqual(grim.canAttack(target), { ok: true });
|
||
});
|
||
|
||
test('canAttack 拒绝骑乘状态下的攻击', () => {
|
||
const { bot, grim } = setup();
|
||
bot.vehicle = { id: 1 };
|
||
const target = { position: new Vec3(2, 64, 0), width: 0.6, height: 1.8, isValid: true };
|
||
assert.deepStrictEqual(grim.canAttack(target), { ok: false, reason: 'riding' });
|
||
});
|
||
|
||
test('attackEntity 会先同步朝向再出手', async () => {
|
||
const { bot, grim } = setup();
|
||
const target = { position: new Vec3(2, 64, 0), width: 0.6, height: 1.8, isValid: true, id: 7 };
|
||
const promise = grim.attackEntity(target);
|
||
// 旋转尚未完成时不应出手
|
||
bot.tick();
|
||
assert.strictEqual(bot.lastAttack, undefined, '朝向未同步前不得攻击');
|
||
for (let i = 0; i < 60; i++) bot.tick();
|
||
await promise;
|
||
assert.ok(bot.lastAttack, '朝向同步后应完成攻击');
|
||
});
|
||
|
||
test('关闭合规层后不做任何限速与拦截', () => {
|
||
const config = defaultConfig();
|
||
config.grim.enabled = false;
|
||
const bot = new FakeBot();
|
||
const grim = new GrimCompat(bot, config, null);
|
||
grim.install();
|
||
bot.setControlState('sprint', true);
|
||
bot.setControlState('sneak', true);
|
||
assert.strictEqual(bot.getControlState('sneak'), true, '关闭后不拦截');
|
||
assert.strictEqual(grim.blockedActions, 0);
|
||
});
|
||
|
||
test('audit 检出被篡改的物理常量与创造模式', () => {
|
||
const { bot, grim } = setup();
|
||
assert.deepStrictEqual(grim.audit(), { ok: true, issues: [] });
|
||
|
||
bot.physics.gravity = 0.02;
|
||
const tampered = grim.audit();
|
||
assert.strictEqual(tampered.ok, false);
|
||
assert.ok(tampered.issues.some((issue) => issue.includes('gravity')));
|
||
|
||
bot.physics.gravity = VANILLA_PHYSICS_BASELINE.gravity;
|
||
bot.game.gameMode = 'creative';
|
||
assert.ok(grim.audit().issues.some((issue) => issue.includes('创造模式')));
|
||
});
|
||
|
||
test('audit 检出被关闭的本地物理预测', () => {
|
||
const { bot, grim } = setup();
|
||
bot.physicsEnabled = false;
|
||
assert.ok(grim.audit().issues.some((issue) => issue.includes('physicsEnabled')));
|
||
});
|
||
|
||
test('snapshot 暴露合规状态供控制台展示', () => {
|
||
const { grim } = setup();
|
||
const snapshot = grim.snapshot();
|
||
assert.strictEqual(snapshot.enabled, true);
|
||
assert.strictEqual(snapshot.attackReach, 3.0);
|
||
assert.strictEqual(snapshot.physicsOk, true);
|
||
assert.ok(snapshot.effectiveStepDeg > 0 && snapshot.effectiveStepDeg <= 9);
|
||
assert.strictEqual(snapshot.blockedActions, 0);
|
||
});
|
||
|
||
test('releaseAllControls 松开全部控制键', () => {
|
||
const { bot, grim } = setup();
|
||
grim.setControlState('forward', true);
|
||
grim.setControlState('sprint', true);
|
||
grim.releaseAllControls();
|
||
for (const control of ['forward', 'back', 'left', 'right', 'jump', 'sprint', 'sneak']) {
|
||
assert.strictEqual(bot.getControlState(control), false, control + ' 应被松开');
|
||
}
|
||
});
|