fix: 修掉 yaw 无界增长 —— 它让机器人原地走之字形、净位移恒为 0(1.11.0)

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)。

这是扩地跑不完那 763 格长途的直接原因,也解释了出站包里 look 包(38490)
比 position_look(23804)还多。

修复:新增 wrapAngle,_onPhysicsTick 每 tick 归一化 entity.yaw 到 (-π, π],
setLook 也归一化传入的目标角度。归一化只改表示不改朝向
(shortestAngleDelta(huge, wrapped) === 0),对旋转逻辑完全透明。

测试 478 → 480。
这个提交包含在:
lyizai
2026-10-01 18:08:41 +08:00
父节点 841d20afef
当前提交 ab598a7101
共修改 5 个文件,包含 94 行新增和 6 行删除
+31
查看文件
@@ -2,6 +2,37 @@
本项目遵循 [约定式提交](https://www.conventionalcommits.org/zh-hans/) 与 [语义化版本](https://semver.org/lang/zh-CN/)。 本项目遵循 [约定式提交](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.10.1] - 2026-10-01
修掉 1.9.3 自动恢复里的一个会让恢复永久丢失的缺陷。 修掉 1.9.3 自动恢复里的一个会让恢复永久丢失的缺陷。
自动生成
+2 -2
查看文件
@@ -1,12 +1,12 @@
{ {
"name": "autostore-bot", "name": "autostore-bot",
"version": "1.10.1", "version": "1.11.0",
"lockfileVersion": 3, "lockfileVersion": 3,
"requires": true, "requires": true,
"packages": { "packages": {
"": { "": {
"name": "autostore-bot", "name": "autostore-bot",
"version": "1.10.1", "version": "1.11.0",
"license": "MIT", "license": "MIT",
"dependencies": { "dependencies": {
"chalk": "^4.1.2", "chalk": "^4.1.2",
+1 -1
查看文件
@@ -1,6 +1,6 @@
{ {
"name": "autostore-bot", "name": "autostore-bot",
"version": "1.10.1", "version": "1.11.0",
"description": "AutoStore Bot —— Minecraft 自动扩地机器人(mineflayer 重写版,多机器人挂机,Web 控制台,开拓日志,大地图,Grim 反作弊适配)", "description": "AutoStore Bot —— Minecraft 自动扩地机器人(mineflayer 重写版,多机器人挂机,Web 控制台,开拓日志,大地图,Grim 反作弊适配)",
"main": "src/index.js", "main": "src/index.js",
"scripts": { "scripts": {
+34 -3
查看文件
@@ -227,7 +227,7 @@ class GrimCompat {
*/ */
setLook(yaw, pitch) { setLook(yaw, pitch) {
if (typeof yaw !== 'number' || Number.isNaN(yaw)) return Promise.resolve(); 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) this.targetPitch = typeof pitch === 'number' && !Number.isNaN(pitch)
? clamp(pitch, -Math.PI / 2, Math.PI / 2) ? clamp(pitch, -Math.PI / 2, Math.PI / 2)
: this.targetPitch; : 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() { _onPhysicsTick() {
const bot = this.bot; const bot = this.bot;
if (!bot.entity) return; if (!bot.entity) return;
const step = this.maxStepRad; const step = this.maxStepRad;
const yawDelta = shortestAngleDelta(bot.entity.yaw, this.targetYaw); const yawDelta = shortestAngleDelta(bot.entity.yaw, this.targetYaw);
if (Math.abs(yawDelta) > 1e-6) { 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 { } else {
bot.entity.yaw = this.targetYaw; bot.entity.yaw = this.targetYaw;
} }
@@ -613,6 +624,25 @@ function shortestAngleDelta(from, to) {
return delta; 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 * @param {import('mineflayer').Bot} bot
@@ -658,6 +688,7 @@ module.exports = {
VANILLA_PHYSICS_BASELINE, VANILLA_PHYSICS_BASELINE,
ATTACK_SPEED_RULES, ATTACK_SPEED_RULES,
shortestAngleDelta, shortestAngleDelta,
wrapAngle,
eyeDistanceToEntity, eyeDistanceToEntity,
degToRad, degToRad,
radToDeg radToDeg
+26
查看文件
@@ -8,6 +8,7 @@ const { FakeBot } = require('./helpers/fakeBot');
const { const {
GrimCompat, GrimCompat,
shortestAngleDelta, shortestAngleDelta,
wrapAngle,
eyeDistanceToEntity, eyeDistanceToEntity,
VANILLA_PHYSICS_BASELINE VANILLA_PHYSICS_BASELINE
} = require('../src/bot/grimCompat'); } = require('../src/bot/grimCompat');
@@ -111,6 +112,31 @@ test('shortestAngleDelta 走最短路径并处理环绕', () => { assert.ok(Mat
assert.ok(Math.abs(shortestAngleDelta(0, Math.PI * 2)) < 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 计算眼睛到碰撞箱的最近距离', () => { test('eyeDistanceToEntity 计算眼睛到碰撞箱的最近距离', () => {
const bot = new FakeBot({ x: 0, y: 64, z: 0 }); const bot = new FakeBot({ x: 0, y: 64, z: 0 });
// 目标在正前方 2 格,碰撞箱中心高度与眼睛同高 // 目标在正前方 2 格,碰撞箱中心高度与眼睛同高