feat: 新增物理 tick 录制,把「走不动」的三种原因切开(1.9.0)

抓包已经证明「手动按键时客户端坐标一字未变、服务端入站位置包为 0」,
问题在 prismarine-physics 这一层;但抓包只能看到「发出去的包」,
看不到物理引擎内部读到了什么输入、算出了什么、有没有真的移动。
本提交补上这最后一块拼图。

新增 src/core/physicsRecorder.js + 控制台接口
(POST /api/bots/:id/physics/start|stop、GET /api/bots/:id/physics):

逐 tick 记录位置增量、速度、onGround、isCollidedHorizontally、控制键快照、
脚下/脚部/头部方块名、physicsEnabled;汇总给出四个判定计数:
- inputTicks        有移动输入的 tick 数
- inputMovedTicks   其中真的产生了水平位移的
- blockedTicks      有输入、没位移、物理报告碰撞  → 被方块挡住(地形)
- frozenTicks       有输入、没位移、物理没报告碰撞 → 输入根本没进物理

即:抓包看「发了什么」,本录制器看「物理读到了什么、动没动」,两者配合即可定位。

细节:
- 录制开始时先取一次当前位置作基线,避免第一个 tick 因「没有上一 tick 可比」
  被误判成「有输入但没动」;
- 滚动缓冲只保留最近 240 个 tick(约 12 秒),只放内存、不写盘;
- 录制全程 try 包裹,诊断自身出异常绝不影响物理循环;
- FakeBot 补上与 mineflayer 一致的 controlState(带 getter 的实时控制键代理)。

测试:451 → 468 个用例(含三种故障判定的回归用例与控制台接口用例),全绿。
这个提交包含在:
lyizai
2026-09-30 21:05:38 +08:00
父节点 0c6700642e
当前提交 b2b0317af7
共修改 12 个文件,包含 764 行新增和 5 行删除
+47
查看文件
@@ -12,6 +12,7 @@ const { AutoBoatController } = require('./autoBoat');
const { DurabilityGuard } = require('./durabilityGuard');
const { ChatKind } = require('../core/chatLog');
const { PacketCapture } = require('../core/packetCapture');
const { PhysicsRecorder } = require('../core/physicsRecorder');
/** 服务器要求登录 / 注册时的提示关键词 */
const LOGIN_PROMPT_PATTERN = /请(先)?(输入)?登录|请登录|请输入密码|login|登入/i;
@@ -207,6 +208,14 @@ class BotManager extends EventEmitter {
* 与「能走」时的序列逐包 diff(TODO.md 假设 5)。
*/
this.packetCapture = new PacketCapture();
/**
* 物理 tick 录制器(诊断用,默认不录制)。
*
* 抓包只能看到「发出去的包」,看不到物理引擎内部:它这一 tick 读到的 control、
* 算出的速度、有没有真的移动、有没有报告水平碰撞。而「走不动」正卡在这一层,
* 所以单独再要一个逐 tick 录制器。
*/
this.physicsRecorder = new PhysicsRecorder();
}
// ==================== 连接生命周期 ====================
@@ -521,6 +530,7 @@ class BotManager extends EventEmitter {
this._wirePacketStats(bot);
// 重连会换新的 client,这里每次都重新登记(只在录制期间才会真正挂钩 write)
this.packetCapture.setClient(bot._client);
this.physicsRecorder.setBot(bot);
bot.on('login', () => {
this.state = 'online';
@@ -1241,6 +1251,43 @@ class BotManager extends EventEmitter {
return this.packetCapture.snapshot(options);
}
/**
* 开始录制物理 tick(诊断用)。
*
* 与抓包配合使用:抓包回答「客户端发了什么」,本录制器回答
* 「物理引擎这一 tick 读到了什么输入、算出了什么、有没有真的移动」。
* 两者一起看,才能把「输入没进物理」「被方块挡住」「服务端回弹」三种原因切开。
*
* @param {number} [seconds] 录制时长(秒),省略则需手动停止
* @returns {{ok: boolean, message: string}}
*/
startPhysicsRecorder(seconds) {
const result = this.physicsRecorder.start(seconds);
if (result.ok) this._log('物理录制:' + result.message);
return result;
}
/**
* 停止物理 tick 录制。
*
* @returns {{ok: boolean, message: string}}
*/
stopPhysicsRecorder() {
const result = this.physicsRecorder.stop();
this._log('物理录制:' + result.message);
return result;
}
/**
* 取出物理 tick 录制结果。
*
* @param {{limit?: number}} [options]
* @returns {object}
*/
physicsSnapshot(options) {
return this.physicsRecorder.snapshot(options);
}
/**
* 手动移动测试:按住某个方向键若干秒,然后松开。
*
+29
查看文件
@@ -625,6 +625,35 @@ class BotPool extends EventEmitter {
return this.require(id).manager.packetCaptureSnapshot(options);
}
/**
* 开始录制物理 tick(诊断用)。
* @param {string} id
* @param {number} [seconds] 录制时长(秒),省略则需手动停止
* @returns {{ok: boolean, message: string}}
*/
startPhysicsRecorder(id, seconds) {
return this.require(id).manager.startPhysicsRecorder(seconds);
}
/**
* 停止物理 tick 录制。
* @param {string} id
* @returns {{ok: boolean, message: string}}
*/
stopPhysicsRecorder(id) {
return this.require(id).manager.stopPhysicsRecorder();
}
/**
* 取出物理 tick 录制结果。
* @param {string} id
* @param {{limit?: number}} [options]
* @returns {object}
*/
physicsSnapshot(id, options) {
return this.require(id).manager.physicsSnapshot(options);
}
// ==================== 状态快照 ====================
/**