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 个用例(含三种故障判定的回归用例与控制台接口用例),全绿。
这个提交包含在:
+48
-2
@@ -6,6 +6,8 @@ const assert = require('node:assert');
|
||||
const { flattenChunks, boundsOf, timingSafeEqual, WebConsole } = require('../src/web/server');
|
||||
const { TerritoryModel } = require('../src/core/territoryModel');
|
||||
const { PacketCapture } = require('../src/core/packetCapture');
|
||||
const { PhysicsRecorder } = require('../src/core/physicsRecorder');
|
||||
const { FakeBot } = require('./helpers/fakeBot');
|
||||
const { defaultConfig } = require('../src/config/expansionConfig');
|
||||
const { Logger } = require('../src/util/logger');
|
||||
const chunkPos = require('../src/core/chunkPos');
|
||||
@@ -59,12 +61,19 @@ function setupConsole(options) {
|
||||
};
|
||||
const packetCapture = new PacketCapture();
|
||||
packetCapture.setClient(captureClient);
|
||||
// 物理录制接口的桩:同样挂真的 PhysicsRecorder + FakeBot
|
||||
const physicsBot = new FakeBot({ x: 0, y: 64, z: 0 });
|
||||
const physicsRecorder = new PhysicsRecorder();
|
||||
physicsRecorder.setBot(physicsBot);
|
||||
const botManager = {
|
||||
snapshot: () => ({ state: 'online', username: 'TestBot' }),
|
||||
sendCommand: () => {},
|
||||
startPacketCapture: (seconds) => packetCapture.start(seconds),
|
||||
stopPacketCapture: () => packetCapture.stop(),
|
||||
packetCaptureSnapshot: (options) => packetCapture.snapshot(options)
|
||||
packetCaptureSnapshot: (options) => packetCapture.snapshot(options),
|
||||
startPhysicsRecorder: (seconds) => physicsRecorder.start(seconds),
|
||||
stopPhysicsRecorder: () => physicsRecorder.stop(),
|
||||
physicsSnapshot: (options) => physicsRecorder.snapshot(options)
|
||||
};
|
||||
const controller = {
|
||||
target: { x: 2, z: 0 },
|
||||
@@ -92,7 +101,7 @@ function setupConsole(options) {
|
||||
getConfig: () => config,
|
||||
applyConfig: (next) => next
|
||||
}, { host: '127.0.0.1', port: 0 });
|
||||
return { webConsole, model, logger, controller, captureClient, packetCapture };
|
||||
return { webConsole, model, logger, controller, captureClient, packetCapture, physicsBot, physicsRecorder };
|
||||
}
|
||||
|
||||
test('mapPayload 返回己方 / 会话 / 飞地 / 目标与各邦国摘要', () => {
|
||||
@@ -273,3 +282,40 @@ test('HTTP 接口:抓包可开始、取结果(含按包名过滤)、停止
|
||||
await webConsole.stop();
|
||||
}
|
||||
});
|
||||
|
||||
test('HTTP 接口:物理录制可开始、取结果、停止', async () => {
|
||||
const { webConsole, physicsBot } = setupConsole();
|
||||
await webConsole.start();
|
||||
const base = webConsole.baseUrl();
|
||||
try {
|
||||
const started = await fetch(base + '/api/bots/bot/physics/start', {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({ seconds: 5 })
|
||||
}).then((r) => r.json());
|
||||
assert.strictEqual(started.ok, true);
|
||||
assert.match(started.message, /已开始录制物理 tick/);
|
||||
|
||||
// 模拟「按着前进但物理没动」:应当被统计成 frozenTicks
|
||||
physicsBot.setControlState('forward', true);
|
||||
physicsBot.emit('physicsTick');
|
||||
physicsBot.emit('physicsTick');
|
||||
|
||||
const snap = await fetch(base + '/api/bots/bot/physics').then((r) => r.json());
|
||||
assert.strictEqual(snap.ok, true);
|
||||
assert.strictEqual(snap.data.running, true);
|
||||
assert.strictEqual(snap.data.count, 2);
|
||||
assert.strictEqual(snap.data.inputTicks, 2);
|
||||
assert.strictEqual(snap.data.inputMovedTicks, 0);
|
||||
assert.strictEqual(snap.data.frozenTicks, 2);
|
||||
|
||||
const limited = await fetch(base + '/api/bots/bot/physics?limit=1').then((r) => r.json());
|
||||
assert.strictEqual(limited.data.count, 2);
|
||||
assert.strictEqual(limited.data.returned, 1);
|
||||
|
||||
const stopped = await fetch(base + '/api/bots/bot/physics/stop', { method: 'POST' }).then((r) => r.json());
|
||||
assert.strictEqual(stopped.ok, true);
|
||||
} finally {
|
||||
await webConsole.stop();
|
||||
}
|
||||
});
|
||||
|
||||
在新工单中引用
屏蔽一个用户