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。
这个提交包含在:
+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
|
||||||
|
|||||||
@@ -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 格,碰撞箱中心高度与眼睛同高
|
||||||
|
|||||||
在新工单中引用
屏蔽一个用户