'use strict'; const test = require('node:test'); const assert = require('node:assert'); const { Vec3 } = require('vec3'); const { FakeBot } = require('./helpers/fakeBot'); const { GrimCompat, shortestAngleDelta, wrapAngle, 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 }; } // ==================== 协议补包(线上服务器「走不动」的根因)==================== /** 等一轮宏任务,让 process.nextTick 里发出的 tick_end 落地 */ function flushTick() { return new Promise((resolve) => setTimeout(resolve, 5)); } test('每 tick 补发 player_input,内容反映当前控制键', async () => { const { bot } = setup(); bot.setControlState('forward', true); bot.setControlState('sprint', true); bot.clearWritten(); bot.tick(); const packets = bot.writtenPackets('player_input'); assert.strictEqual(packets.length, 1, '每个 physicsTick 都应补发一次 player_input'); assert.strictEqual(packets[0].params.inputs.forward, true); assert.strictEqual(packets[0].params.inputs.sprint, true); assert.strictEqual(packets[0].params.inputs.jump, false); assert.strictEqual(packets[0].params.inputs.backward, false); assert.strictEqual(packets[0].params.inputs.shift, false); }); test('每 tick 补发 tick_end,且排在本帧位置包之后', async () => { const { bot } = setup(); bot.clearWritten(); bot.tick(); assert.strictEqual(bot.writtenPackets('tick_end').length, 0, 'tick_end 应延后到下一轮微任务/宏任务'); await flushTick(); assert.strictEqual(bot.writtenPackets('tick_end').length, 1); }); test('版本不支持 player_input 时只补 tick_end,不报错', async () => { const { bot } = setup({ bot: { features: { newPlayerInputPacket: false } } }); bot.clearWritten(); bot.tick(); await flushTick(); assert.strictEqual(bot.writtenPackets('player_input').length, 0); assert.strictEqual(bot.writtenPackets('tick_end').length, 1); }); test('关闭开关后不再补发对应包', async () => { const { bot } = setup({ config: { grim: { sendPlayerInput: false, sendTickEnd: false } } }); bot.clearWritten(); bot.tick(); await flushTick(); assert.strictEqual(bot._client.written.length, 0); }); test('关掉 Grim 合规层后协议补包仍然生效(否则机器人根本走不动)', async () => { const { bot } = setup({ config: { grim: { enabled: false } } }); bot.clearWritten(); bot.tick(); await flushTick(); assert.strictEqual(bot.writtenPackets('player_input').length, 1); assert.strictEqual(bot.writtenPackets('tick_end').length, 1); }); test('uninstall 后不再补包', async () => { const { bot, grim } = setup(); grim.uninstall(); bot.clearWritten(); bot.tick(); await flushTick(); assert.strictEqual(bot._client.written.length, 0); }); test('snapshot 暴露协议补包状态与计数', () => { const { bot, grim } = setup(); bot.tick(); const snap = grim.snapshot(); assert.strictEqual(snap.protocolPackets.playerInputSupported, true); assert.strictEqual(snap.protocolPackets.playerInputEnabled, true); assert.strictEqual(snap.protocolPackets.playerInputSent, 1); assert.strictEqual(snap.protocolPackets.tickEndEnabled, true); }); 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('wrapAngle 把任意角度归一化到 (-π, π]', () => { assert.ok(Math.abs(wrapAngle(0.5) - 0.5) < 1e-9, '范围内不动'); assert.ok(Math.abs(wrapAngle(Math.PI * 2) - 0) < 1e-9, '整圈归零'); // 实测抓到过 19254°(≈336 弧度)的 yaw —— 必须被归一化 const huge = (19254 * Math.PI) / 180; const wrapped = wrapAngle(huge); assert.ok(wrapped > -Math.PI && wrapped <= Math.PI, `归一化后应在 (-π, π],实际 ${wrapped}`); assert.ok(Math.abs(shortestAngleDelta(huge, wrapped)) < 1e-9, '归一化不应改变朝向'); assert.strictEqual(wrapAngle(NaN), 0); assert.strictEqual(wrapAngle(Infinity), 0); }); test('旋转推进后 yaw 始终保持归一化(回归:无界增长导致原地走之字形)', () => { // mineflayer 收到 position 包时会直接给 entity.yaw 赋一个未取模的值, // 本层又在其上累加;不归一化的话 yaw 会一路涨到上万度, // 发包朝向在两个方向间来回跳,机器人原地走之字形、净位移恒为 0。 const { grim, bot } = setup(); bot.entity.yaw = (19254 * Math.PI) / 180; // 未归一化的巨大值 grim.setLook(0.5, 0); for (let i = 0; i < 40; i++) grim._onPhysicsTick(); assert.ok(bot.entity.yaw > -Math.PI && bot.entity.yaw <= Math.PI, `yaw 应被归一化,实际 ${bot.entity.yaw}`); assert.ok(Math.abs(shortestAngleDelta(bot.entity.yaw, 0.5)) < 1e-4, '最终仍应朝向目标'); }); 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 + ' 应被松开'); } });