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 个用例(含三种故障判定的回归用例与控制台接口用例),全绿。
这个提交包含在:
@@ -476,6 +476,37 @@ class WebConsole {
|
||||
});
|
||||
});
|
||||
|
||||
// 物理 tick 录制:抓包回答「客户端发了什么」,这里回答
|
||||
// 「物理引擎读到了什么输入、算出了什么、有没有真的移动」。
|
||||
// 两者一起看,才能把「输入没进物理 / 被方块挡住 / 服务端回弹」三种原因切开。
|
||||
app.post('/api/bots/:id/physics/start', (req, res) => {
|
||||
this._withEntry(req, res, (entry) => {
|
||||
const body = req.body || {};
|
||||
const result = this.pool.startPhysicsRecorder(entry.id, body.seconds);
|
||||
res.status(result.ok ? 200 : 409).json(result);
|
||||
return null;
|
||||
});
|
||||
});
|
||||
|
||||
app.post('/api/bots/:id/physics/stop', (req, res) => {
|
||||
this._withEntry(req, res, (entry) => {
|
||||
const result = this.pool.stopPhysicsRecorder(entry.id);
|
||||
res.json({ ok: true, message: result.message });
|
||||
return null;
|
||||
});
|
||||
});
|
||||
|
||||
app.get('/api/bots/:id/physics', (req, res) => {
|
||||
this._withEntry(req, res, (entry) => {
|
||||
const limit = Number.parseInt(req.query.limit, 10);
|
||||
const data = this.pool.physicsSnapshot(entry.id, {
|
||||
limit: Number.isFinite(limit) ? limit : undefined
|
||||
});
|
||||
res.json({ ok: true, data });
|
||||
return null;
|
||||
});
|
||||
});
|
||||
|
||||
// 控制台 / 聊天框操作:都要求该机器人已完成游戏内登录验证
|
||||
app.post('/api/bots/:id/command', (req, res) => {
|
||||
const command = req.body && req.body.command;
|
||||
@@ -1301,6 +1332,47 @@ class SingleBotPool {
|
||||
}
|
||||
return entry.manager.packetCaptureSnapshot(options);
|
||||
}
|
||||
|
||||
/**
|
||||
* 开始录制物理 tick。
|
||||
* @param {string} id
|
||||
* @param {number} [seconds]
|
||||
* @returns {{ok: boolean, message: string}}
|
||||
*/
|
||||
startPhysicsRecorder(id, seconds) {
|
||||
const entry = this.require(id);
|
||||
if (typeof entry.manager.startPhysicsRecorder !== 'function') {
|
||||
return { ok: false, message: '当前运行模式不支持物理录制' };
|
||||
}
|
||||
return entry.manager.startPhysicsRecorder(seconds);
|
||||
}
|
||||
|
||||
/**
|
||||
* 停止物理 tick 录制。
|
||||
* @param {string} id
|
||||
* @returns {{ok: boolean, message: string}}
|
||||
*/
|
||||
stopPhysicsRecorder(id) {
|
||||
const entry = this.require(id);
|
||||
if (typeof entry.manager.stopPhysicsRecorder !== 'function') {
|
||||
return { ok: true, message: '当前运行模式不支持物理录制' };
|
||||
}
|
||||
return entry.manager.stopPhysicsRecorder();
|
||||
}
|
||||
|
||||
/**
|
||||
* 取出物理 tick 录制结果。
|
||||
* @param {string} id
|
||||
* @param {{limit?: number}} [options]
|
||||
* @returns {object}
|
||||
*/
|
||||
physicsSnapshot(id, options) {
|
||||
const entry = this.require(id);
|
||||
if (typeof entry.manager.physicsSnapshot !== 'function') {
|
||||
return { running: false, count: 0, inputTicks: 0, inputMovedTicks: 0, ticks: [] };
|
||||
}
|
||||
return entry.manager.physicsSnapshot(options);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
在新工单中引用
屏蔽一个用户