原来 frozenTicks 只看「有输入 + tick 级无位移 + 物理没报告碰撞」, 但服务端推回发生在 tick 之间,tick 级位移同样是 0 —— 实测大厅里就是这个形态: collidedHorizontally = false、frozenTicks = 194,而物理其实每 tick 都在动。 现在再按水平速度是否非零细分: - pushedBackTicks:速度非零 → 物理确实动了,位移被服务端位置包抹掉; - frozenTicks:速度也是 0 → 这才是「输入根本没进物理」。 类注释里的判定表也同步改成三分法(被挡住 / 被推回 / 输入没生效)。 测试 469 → 470。
240 行
8.6 KiB
JavaScript
240 行
8.6 KiB
JavaScript
'use strict';
|
|
|
|
const test = require('node:test');
|
|
const assert = require('node:assert');
|
|
|
|
const { PhysicsRecorder, DEFAULT_MAX_TICKS } = require('../src/core/physicsRecorder');
|
|
const { FakeBot } = require('./helpers/fakeBot');
|
|
|
|
/** 造一个站在石头上的假 bot(脚下是 stone,便于断言方块名) */
|
|
function setup(options) {
|
|
const opts = options || {};
|
|
const bot = new FakeBot({ x: 0, y: 64, z: 0 });
|
|
bot.blockMap.set('0,63,0', { name: 'stone', boundingBox: 'block' });
|
|
bot.blockMap.set('0,64,0', { name: 'air', boundingBox: 'empty' });
|
|
bot.blockMap.set('0,65,0', { name: 'air', boundingBox: 'empty' });
|
|
const recorder = new PhysicsRecorder(opts);
|
|
recorder.setBot(bot);
|
|
return { bot, recorder };
|
|
}
|
|
|
|
/** 模拟一 tick:先改位置/速度/碰撞状态,再触发 physicsTick */
|
|
function tick(bot, patch) {
|
|
Object.assign(bot.entity, patch || {});
|
|
bot.emit('physicsTick');
|
|
}
|
|
|
|
test('没有 bot 时拒绝开始录制', () => {
|
|
const recorder = new PhysicsRecorder();
|
|
const result = recorder.start(1);
|
|
assert.strictEqual(result.ok, false);
|
|
assert.match(result.message, /没有可录制的连接/);
|
|
assert.strictEqual(recorder.running, false);
|
|
});
|
|
|
|
test('逐 tick 记录输入、速度、位移与脚下方块', () => {
|
|
const { bot, recorder } = setup();
|
|
assert.strictEqual(recorder.start(1).ok, true);
|
|
assert.strictEqual(recorder.running, true);
|
|
|
|
bot.setControlState('forward', true);
|
|
bot.entity.velocity.set(0.1, -0.078, 0);
|
|
tick(bot, { position: bot.entity.position.offset(0, 0, 0.1) });
|
|
tick(bot, { position: bot.entity.position.offset(0, 0, 0.1) });
|
|
|
|
const snap = recorder.snapshot();
|
|
assert.strictEqual(snap.count, 2);
|
|
assert.strictEqual(snap.physicsEnabled, true);
|
|
const first = snap.ticks[0];
|
|
assert.strictEqual(first.control.forward, true);
|
|
assert.strictEqual(first.control.back, false);
|
|
assert.strictEqual(first.vx, 0.1);
|
|
assert.strictEqual(first.onGround, true);
|
|
assert.strictEqual(first.blockBelow, 'stone');
|
|
assert.strictEqual(first.blockAtFeet, 'air');
|
|
assert.strictEqual(first.blockAtHead, 'air');
|
|
// 录制开始时会先取一次当前位置当基线,所以第一个 tick 也有有效位移
|
|
assert.strictEqual(first.dz, 0.1);
|
|
assert.strictEqual(snap.ticks[1].dz, 0.1);
|
|
assert.strictEqual(snap.inputTicks, 2);
|
|
assert.strictEqual(snap.inputMovedTicks, 2);
|
|
assert.strictEqual(snap.maxHorizDelta, 0.1);
|
|
});
|
|
|
|
test('有输入但没位移且物理报告水平碰撞 → 记为「被方块挡住」', () => {
|
|
const { bot, recorder } = setup();
|
|
recorder.start();
|
|
bot.setControlState('forward', true);
|
|
// 位置不变、物理报告水平碰撞:典型的「撞墙」
|
|
tick(bot, { isCollidedHorizontally: true });
|
|
tick(bot, { isCollidedHorizontally: true });
|
|
|
|
const snap = recorder.snapshot();
|
|
assert.strictEqual(snap.inputTicks, 2);
|
|
assert.strictEqual(snap.inputMovedTicks, 0);
|
|
assert.strictEqual(snap.blockedTicks, 2);
|
|
assert.strictEqual(snap.frozenTicks, 0, '报告了碰撞就不算「输入没生效」');
|
|
});
|
|
|
|
test('有输入但没位移且物理没报告碰撞 → 记为「输入没进物理」', () => {
|
|
const { bot, recorder } = setup();
|
|
recorder.start();
|
|
bot.setControlState('forward', true);
|
|
// 位置不变、速度恒为 0、也没有碰撞报告:输入根本没生效
|
|
tick(bot, { isCollidedHorizontally: false });
|
|
tick(bot, { isCollidedHorizontally: false });
|
|
|
|
const snap = recorder.snapshot();
|
|
assert.strictEqual(snap.inputTicks, 2);
|
|
assert.strictEqual(snap.inputMovedTicks, 0);
|
|
assert.strictEqual(snap.frozenTicks, 2);
|
|
assert.strictEqual(snap.blockedTicks, 0, '没报告碰撞就不算「被挡住」');
|
|
});
|
|
|
|
test('有输入、没位移、没碰撞,但水平速度非零 → 记为「被服务端推回」', () => {
|
|
// 实测形态:大厅里物理每 tick 都在动,位移却被服务端的位置包抹掉
|
|
// (推回发生在 tick 之间,所以 tick 级位移为 0、也没有方块碰撞)
|
|
const { bot, recorder } = setup();
|
|
recorder.start();
|
|
bot.setControlState('forward', true);
|
|
bot.entity.velocity.set(0, -0.0784, 0.02319);
|
|
tick(bot, { isCollidedHorizontally: false });
|
|
tick(bot, { isCollidedHorizontally: false });
|
|
|
|
const snap = recorder.snapshot();
|
|
assert.strictEqual(snap.inputTicks, 2);
|
|
assert.strictEqual(snap.inputMovedTicks, 0);
|
|
assert.strictEqual(snap.pushedBackTicks, 2);
|
|
assert.strictEqual(snap.frozenTicks, 0, '速度非零就不该算「输入没进物理」');
|
|
assert.strictEqual(snap.blockedTicks, 0);
|
|
});
|
|
|
|
test('没有输入时不计入 inputTicks', () => {
|
|
const { bot, recorder } = setup();
|
|
recorder.start();
|
|
tick(bot);
|
|
tick(bot);
|
|
const snap = recorder.snapshot();
|
|
assert.strictEqual(snap.count, 2);
|
|
assert.strictEqual(snap.inputTicks, 0);
|
|
assert.strictEqual(snap.movedTicks, 0);
|
|
});
|
|
|
|
test('停止后摘掉监听并不再记录', () => {
|
|
const { bot, recorder } = setup();
|
|
recorder.start();
|
|
tick(bot);
|
|
const result = recorder.stop();
|
|
assert.strictEqual(result.ok, true);
|
|
assert.strictEqual(recorder.running, false);
|
|
assert.match(result.message, /共 1 个 tick/);
|
|
|
|
tick(bot);
|
|
assert.strictEqual(recorder.snapshot().count, 1, '停止后不应再记录');
|
|
assert.strictEqual(bot.listenerCount('physicsTick'), 0, '监听应已摘掉');
|
|
});
|
|
|
|
test('重复 stop 不报错', () => {
|
|
const { recorder } = setup();
|
|
recorder.start();
|
|
assert.strictEqual(recorder.stop().ok, true);
|
|
assert.match(recorder.stop().message, /没有在录制/);
|
|
});
|
|
|
|
test('滚动缓冲只保留最近 maxTicks 个 tick', () => {
|
|
const { bot, recorder } = setup({ maxTicks: 3 });
|
|
recorder.start();
|
|
for (let i = 0; i < 10; i++) {
|
|
tick(bot, { position: bot.entity.position.offset(0, 0, 1) });
|
|
}
|
|
const snap = recorder.snapshot();
|
|
assert.strictEqual(snap.count, 3);
|
|
assert.strictEqual(snap.ticks.length, 3);
|
|
});
|
|
|
|
test('snapshot 的 limit 只影响返回条数,不影响统计', () => {
|
|
const { bot, recorder } = setup();
|
|
recorder.start();
|
|
bot.setControlState('forward', true);
|
|
for (let i = 0; i < 5; i++) tick(bot, { position: bot.entity.position.offset(0, 0, 1) });
|
|
|
|
const snap = recorder.snapshot({ limit: 2 });
|
|
assert.strictEqual(snap.count, 5);
|
|
assert.strictEqual(snap.returned, 2);
|
|
assert.strictEqual(snap.ticks.length, 2);
|
|
assert.strictEqual(snap.inputMovedTicks, 5);
|
|
});
|
|
|
|
test('给出时长后到点自动停止并摘监听', async () => {
|
|
const { bot, recorder } = setup();
|
|
recorder.start(0.05);
|
|
assert.strictEqual(recorder.running, true);
|
|
await new Promise((resolve) => setTimeout(resolve, 150));
|
|
assert.strictEqual(recorder.running, false);
|
|
assert.strictEqual(bot.listenerCount('physicsTick'), 0);
|
|
assert.ok(recorder.snapshot().stoppedAt > 0);
|
|
});
|
|
|
|
test('录制时长被上限截断', () => {
|
|
const { recorder } = setup();
|
|
const result = recorder.start(9999);
|
|
assert.match(result.message, /120 秒/);
|
|
recorder.stop();
|
|
});
|
|
|
|
test('换 bot 时先收尾旧录制', () => {
|
|
const { bot, recorder } = setup();
|
|
recorder.start();
|
|
tick(bot);
|
|
const other = new FakeBot();
|
|
recorder.setBot(other);
|
|
assert.strictEqual(recorder.running, false, '换 bot 应停止录制');
|
|
assert.strictEqual(recorder.snapshot().count, 1, '旧 bot 录到的内容保留');
|
|
tick(bot);
|
|
assert.strictEqual(recorder.snapshot().count, 1, '旧 bot 不再被记录');
|
|
});
|
|
|
|
test('clear 清空已录内容且不影响录制状态', () => {
|
|
const { bot, recorder } = setup();
|
|
recorder.start();
|
|
tick(bot);
|
|
assert.strictEqual(recorder.snapshot().count, 1);
|
|
recorder.clear();
|
|
const snap = recorder.snapshot();
|
|
assert.strictEqual(snap.count, 0);
|
|
assert.strictEqual(snap.running, true);
|
|
});
|
|
|
|
test('方块读不到(区块未加载)时记 null 而不是抛错', () => {
|
|
const { bot, recorder } = setup();
|
|
recorder.start();
|
|
bot.blockAt = () => null;
|
|
tick(bot);
|
|
const row = recorder.snapshot().ticks[0];
|
|
assert.strictEqual(row.blockBelow, null);
|
|
assert.strictEqual(row.blockAtFeet, null);
|
|
});
|
|
|
|
test('录制自身出异常也不影响物理循环', () => {
|
|
const { bot, recorder } = setup();
|
|
recorder.start();
|
|
// 位置是 NaN 时直接跳过,不抛错
|
|
tick(bot, { position: { x: NaN, y: 0, z: 0 } });
|
|
assert.strictEqual(recorder.snapshot().count, 0);
|
|
|
|
// blockAt 抛错也不能让 physicsTick 崩
|
|
bot.entity.position = { x: 1, y: 64, z: 1, offset: () => ({ x: 1, y: 63, z: 1 }) };
|
|
bot.blockAt = () => {
|
|
throw new Error('炸了');
|
|
};
|
|
assert.doesNotThrow(() => bot.emit('physicsTick'));
|
|
const snap = recorder.snapshot();
|
|
assert.strictEqual(snap.count, 1);
|
|
assert.strictEqual(snap.ticks[0].blockBelow, null);
|
|
});
|
|
|
|
test('默认上限是一个合理的正数', () => {
|
|
assert.ok(DEFAULT_MAX_TICKS >= 20);
|
|
assert.strictEqual(new PhysicsRecorder().maxTicks, DEFAULT_MAX_TICKS);
|
|
});
|