feat: 补上出站包一直漏报的 hasHorizontalCollision(1.10.0)

针对「机器人一直在走、净位移却只有物理速度的 1/5」这个现象。

mineflayer 在 physics.js 里把 flags.hasHorizontalCollision 写成 undefined
(源码注释还标着「1.21.3+」),位标志序列化后恒为 0 —— 而原版客户端在水平碰到
方块时会上报 1。机器人在崎岖地形上几乎一直在水平碰撞,却始终上报「没碰撞」,
正是反作弊 Prediction 最容易判错的形态:服务端按「该碰撞」预测、客户端说
「没碰撞」,于是被判异常并拉回。

物理引擎其实早已算出真实值(moveEntity 里给 entity.isCollidedHorizontally 赋值),
只是从没被发出去。现在在 _fixOutboundOnGround 里按真实值补上,由
grim.reportHorizontalCollision(默认开)控制。

实测依据(王城外山区,2 秒粒度采样 120 秒):
- controls 几乎每 tick 都是 forward+sprint,velocity 约 0.2~3 格/秒;
- 净位移只有 48.6 格 / 120 秒 = 0.405 格/秒,差额就是被服务端拉回的部分。

顺带把 _fixOutboundOnGround 拆成 _fixOnGroundFlag / _fixHorizontalCollisionFlag
两个职责单一的方法;控制台配置面板新增对应开关,
packetStats.horizontalCollisionFixed 暴露补写次数。

测试 473 → 477。
这个提交包含在:
lyizai
2026-10-01 17:04:25 +08:00
父节点 ed55308885
当前提交 2ae2833568
共修改 7 个文件,包含 139 行新增和 5 行删除
+52
查看文件
@@ -476,6 +476,58 @@ test('出站包结构异常时不抛异常', () => {
assert.doesNotThrow(() => manager._fixOutboundOnGround({ flags: {} }));
});
test('补上 hasHorizontalCollision:mineflayer 恒写 undefined(序列化后为 0)', () => {
// 回归:原版客户端碰到方块时上报 1,而 mineflayer 在 physics.js 里把它写成
// undefined —— 位标志序列化后恒为 0。机器人在崎岖地形上几乎一直水平碰撞,
// 却始终上报「没碰撞」,反作弊的 Prediction 会按「该碰撞」预测后判定异常并拉回。
const { manager } = setup();
const bot = botStandingOnStone();
bot.entity.isCollidedHorizontally = true;
manager.bot = bot;
const params = { flags: { onGround: true, hasHorizontalCollision: undefined } };
manager._fixOutboundOnGround(params);
assert.strictEqual(params.flags.hasHorizontalCollision, true, '应按物理引擎的真实值补上');
assert.strictEqual(manager.packetStats.horizontalCollisionFixed, 1);
// 已一致时不重复计数
manager._fixOutboundOnGround(params);
assert.strictEqual(manager.packetStats.horizontalCollisionFixed, 1);
});
test('没有水平碰撞时如实上报 false', () => {
const { manager } = setup();
const bot = botStandingOnStone();
bot.entity.isCollidedHorizontally = false;
manager.bot = bot;
const params = { flags: { onGround: true, hasHorizontalCollision: undefined } };
manager._fixOutboundOnGround(params);
assert.strictEqual(params.flags.hasHorizontalCollision, false);
assert.strictEqual(manager.packetStats.horizontalCollisionFixed, 1);
});
test('关闭 reportHorizontalCollision 后不碰这个标志', () => {
const { manager } = setup({ config: { grim: { reportHorizontalCollision: false } } });
const bot = botStandingOnStone();
bot.entity.isCollidedHorizontally = true;
manager.bot = bot;
const params = { flags: { onGround: true, hasHorizontalCollision: undefined } };
manager._fixOutboundOnGround(params);
assert.strictEqual(params.flags.hasHorizontalCollision, undefined);
assert.strictEqual(manager.packetStats.horizontalCollisionFixed, 0);
});
test('物理引擎没给出碰撞状态时不瞎猜', () => {
const { manager } = setup();
const bot = botStandingOnStone();
delete bot.entity.isCollidedHorizontally;
manager.bot = bot;
const params = { flags: { onGround: true, hasHorizontalCollision: undefined } };
manager._fixOutboundOnGround(params);
assert.strictEqual(params.flags.hasHorizontalCollision, undefined);
assert.strictEqual(manager.packetStats.horizontalCollisionFixed, 0);
});
test('位置包统计暴露在 snapshot 中,便于排查走不动的问题', () => {
const { manager } = setup();
const snap = manager.snapshot();