diff --git a/CHANGELOG.md b/CHANGELOG.md index 6f34c0c..47b46cd 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,37 @@ 本项目遵循 [约定式提交](https://www.conventionalcommits.org/zh-hans/) 与 [语义化版本](https://semver.org/lang/zh-CN/)。 +## [1.11.0] - 2026-10-01 + +**修掉「机器人原地走之字形、净位移恒为 0」的朝向 bug** —— 这是扩地跑不完 +那 763 格长途的直接原因。 + +### 修复 + +- **`bot.entity.yaw` 无界增长,导致发包朝向在两个方向间来回跳**。 + mineflayer 收到服务端的 `position` 包时会直接 `bot.entity.yaw = conv.fromNotchianYaw(…)` + **不做取模**,而 `GrimCompat` 又在它基础上逐 tick 累加 —— 两者叠加让 yaw 一路涨到 + **19254°(≈336 弧度)**。 + 抓包实测的后果: + + ``` + yaw°: -1373 → 19254 → -1650 → -1407 → -1373 → 19254 → … + 坐标: (6757.50, 11953.70) ⇄ (6757.89, 11954.12) 0.4 格内反复振荡 + ``` + + 相邻两包的朝向折合相差约 **114°**,而机器人一直按着前进键 —— + 于是它**原地走之字形、净位移恒为 0**,看起来像「卡在墙前反复起跳」 + (物理录制里 `jump 74/240`、`水平碰撞 195/240`、`onGround` 只有 79/240)。 + + 现在 `_onPhysicsTick` 每 tick 都调 `wrapAngle` 把 yaw 归一化到 `(-π, π]`, + `setLook` 也会归一化传入的目标角度。归一化只改变表示、不改变朝向 + (`shortestAngleDelta(huge, wrapped) === 0`),所以对旋转逻辑完全透明。 + +### 测试 + +新增 2 个用例(478 → 480):`wrapAngle` 的归一化与边界(含 19254° 这个实测值、 +NaN / Infinity),以及「从未归一化的巨大 yaw 出发、推进 40 tick 后仍归一化且朝向正确」。 + ## [1.10.1] - 2026-10-01 修掉 1.9.3 自动恢复里的一个会让恢复永久丢失的缺陷。 diff --git a/package-lock.json b/package-lock.json index bc0d34b..ac02467 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "autostore-bot", - "version": "1.10.1", + "version": "1.11.0", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "autostore-bot", - "version": "1.10.1", + "version": "1.11.0", "license": "MIT", "dependencies": { "chalk": "^4.1.2", diff --git a/package.json b/package.json index 7ff1482..d9bffe2 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "autostore-bot", - "version": "1.10.1", + "version": "1.11.0", "description": "AutoStore Bot —— Minecraft 自动扩地机器人(mineflayer 重写版,多机器人挂机,Web 控制台,开拓日志,大地图,Grim 反作弊适配)", "main": "src/index.js", "scripts": { diff --git a/src/bot/grimCompat.js b/src/bot/grimCompat.js index 8393681..dc086b2 100644 --- a/src/bot/grimCompat.js +++ b/src/bot/grimCompat.js @@ -227,7 +227,7 @@ class GrimCompat { */ setLook(yaw, pitch) { if (typeof yaw !== 'number' || Number.isNaN(yaw)) return Promise.resolve(); - this.targetYaw = yaw; + this.targetYaw = wrapAngle(yaw); this.targetPitch = typeof pitch === 'number' && !Number.isNaN(pitch) ? clamp(pitch, -Math.PI / 2, Math.PI / 2) : this.targetPitch; @@ -297,14 +297,25 @@ class GrimCompat { }); } - /** physicsTick:把 entity.yaw/pitch 朝目标推进一小步(mineflayer 会在同一 tick 把该朝向发包) */ + /** + * physicsTick:把 entity.yaw/pitch 朝目标推进一小步(mineflayer 会在同一 tick 把该朝向发包)。 + * + * ⚠️ 每 tick 都要把 yaw **归一化到 (-π, π]**。 + * + * mineflayer 收到服务端的 `position` 包时会直接 `bot.entity.yaw = conv.fromNotchianYaw(…)`, + * **不做取模**;本层又在它基础上累加。两者叠加会让 yaw 无界增长 —— + * 实测抓到过 **19254°(≈336 弧度)** 的 yaw:此时 mineflayer 发包时算出的 + * `lastSentYaw` 与真实朝向差了整整若干圈,服务端收到的朝向在两个方向间来回跳 + * (实测相邻两包折合相差约 **114°**),而机器人一直在按前进键, + * 于是它**原地走之字形、净位移恒为 0**(抓包里坐标在 0.4 格内反复振荡)。 + */ _onPhysicsTick() { const bot = this.bot; if (!bot.entity) return; const step = this.maxStepRad; const yawDelta = shortestAngleDelta(bot.entity.yaw, this.targetYaw); if (Math.abs(yawDelta) > 1e-6) { - bot.entity.yaw += clamp(yawDelta, -step, step); + bot.entity.yaw = wrapAngle(bot.entity.yaw + clamp(yawDelta, -step, step)); } else { bot.entity.yaw = this.targetYaw; } @@ -613,6 +624,25 @@ function shortestAngleDelta(from, to) { return delta; } +/** + * 把角度归一化到 (-π, π]。 + * + * 必须做这一步:mineflayer 在 `position` 包里直接赋值 `entity.yaw`(不取模), + * 本层又在其上累加,两者叠加会让 yaw 无界增长 —— 实测抓到过 19254°(≈336 弧度), + * 后果是发包朝向在两个方向间来回跳、机器人原地走之字形。 + * + * @param {number} angle 弧度 + * @returns {number} 归一化后的弧度 + */ +function wrapAngle(angle) { + if (!Number.isFinite(angle)) return 0; + const TWO_PI = Math.PI * 2; + let wrapped = angle % TWO_PI; + if (wrapped > Math.PI) wrapped -= TWO_PI; + else if (wrapped <= -Math.PI) wrapped += TWO_PI; + return wrapped; +} + /** * 眼睛到实体碰撞箱的最近距离。 * @param {import('mineflayer').Bot} bot @@ -658,6 +688,7 @@ module.exports = { VANILLA_PHYSICS_BASELINE, ATTACK_SPEED_RULES, shortestAngleDelta, + wrapAngle, eyeDistanceToEntity, degToRad, radToDeg diff --git a/test/grimCompat.test.js b/test/grimCompat.test.js index 2750477..3bf01c9 100644 --- a/test/grimCompat.test.js +++ b/test/grimCompat.test.js @@ -8,6 +8,7 @@ const { FakeBot } = require('./helpers/fakeBot'); const { GrimCompat, shortestAngleDelta, + wrapAngle, eyeDistanceToEntity, VANILLA_PHYSICS_BASELINE } = require('../src/bot/grimCompat'); @@ -111,6 +112,31 @@ test('shortestAngleDelta 走最短路径并处理环绕', () => { assert.ok(Mat 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 格,碰撞箱中心高度与眼睛同高