feat: 新增出站包抓取,供「走不动」逐包对照(TODO 假设 5)
线上实测确认手动执行器(1.7.0 备选方案)不能解决「走不动」:前 20 秒走 13 格后 即卡在「执行中断(stuck)→重规划→缩短段长→跳过目标→重新规划同一目标」的死循环。 只靠 packetStats.outboundTypes 的计数看不出「发出去的内容对不对」,所以补一个 能录原始出站序列、并能在「能走」与「走不动」两种状态下 diff 的诊断能力。 新增 src/core/packetCapture.js: - 只在录制期间挂钩 client.write,其余时间零开销(该钩子每个出站包都要过一遍); - 录到的内容做降深 + 截断的 JSON 化处理,向量对象只留坐标 (move_minecart 这类包能带上万个浮点数,不处理会把缓冲区撑爆); - 只放内存、有上限(默认 4000 包),不写盘。 接口(控制台): - POST /api/bots/:id/capture/start(可带 seconds 到点自动停止) - POST /api/bots/:id/capture/stop - GET /api/bots/:id/capture(支持 ?types= 与 ?limit=) 顺带修掉两个先前就存在的问题: - scripts/check-protocol.js 其实一直跑不通:minecraft-data 的 types.packet[1][1].type[1].fields[包名] 给的是类型名字符串, 要再去 types[名字] 取 ['container', [字段...]] 才能展开,原实现直接 .map() 必然抛错; 另有读 node_modules 少一层 '..' 的路径问题。 - 两条租约用例(expansionController / targetLease)的时序余量只有 15ms, 全量并行跑测试时偶发失败;改为 TTL 400ms 配 280ms 睡眠,各留 120ms 余量, 断言内容不变。 测试:436 → 451 个用例,连续 16 轮全量运行全绿。
这个提交包含在:
+71
-50
@@ -1,71 +1,92 @@
|
||||
'use strict';
|
||||
|
||||
/**
|
||||
* 协议自检:核对 mineflayer 写出的 `teleport_confirm` 包在 1.21.4 上是否存在、
|
||||
* 字段名是否匹配。
|
||||
* 协议自检:核对 mineflayer 写出的移动相关包在目标版本上的字段名与结构。
|
||||
*
|
||||
* 背景:部分服务端在收到 `player_position` 后会每 tick 重发位置包,
|
||||
* 直到客户端回一个 teleport_confirm 确认。若 mineflayer 因为版本字段名不一致
|
||||
* 而写不出这个包,就会形成「服务端一直传送、客户端一直走不动」的死循环。
|
||||
* 背景:部分服务端在收到位置包后会持续重发 `position`,直到客户端回一个
|
||||
* `teleport_confirm` 确认;若 mineflayer 因为版本字段名不一致而写不出这个包,
|
||||
* 就会形成「服务端一直传送、客户端一直走不动」的死循环。
|
||||
*
|
||||
* 注意 `minecraft-data` 的结构:`types.packet[1][1].type[1].fields[包名]` 拿到的是
|
||||
* **类型名字符串**(如 `packet_position_look`),要再去 `types[名字]` 取
|
||||
* `['container', [字段...]]` 才能看到字段。早先的版本直接对它 `.map()`,
|
||||
* 所以这个脚本一直跑不通。
|
||||
*/
|
||||
|
||||
const fs = require('fs');
|
||||
const path = require('path');
|
||||
|
||||
const mcData = require('minecraft-data')('1.21.4');
|
||||
|
||||
const VERSION = '1.21.4';
|
||||
const toServer = mcData.protocol.play.toServer;
|
||||
const mappings = toServer.types.packet[1][0].type[1].mappings;
|
||||
const toClient = mcData.protocol.play.toClient;
|
||||
|
||||
console.log('=== 1.21.4 客户端 → 服务端 中与传送/位置相关的包名 ===');
|
||||
for (const [id, name] of Object.entries(mappings)) {
|
||||
if (/teleport|position|confirm/i.test(name)) {
|
||||
console.log(` ${id.padStart(3)} ${name}`);
|
||||
}
|
||||
/** 取某一方向「包名 → 类型名」的映射 */
|
||||
function packetNameMap(direction) {
|
||||
const paramsField = direction.types.packet[1][1];
|
||||
return paramsField.type[1].fields;
|
||||
}
|
||||
|
||||
console.log('\n=== 客户端 ← 服务端 中与传送/位置相关的包名 ===');
|
||||
const toClientMappings = mcData.protocol.play.toClient.types.packet[1][0].type[1].mappings;
|
||||
for (const [id, name] of Object.entries(toClientMappings)) {
|
||||
if (/teleport|position/i.test(name)) {
|
||||
console.log(` ${id.padStart(3)} ${name}`);
|
||||
}
|
||||
/** 把类型名解析成字段列表;解析不到返回 null */
|
||||
function resolveFields(direction, typeName) {
|
||||
const type = direction.types[typeName];
|
||||
if (!Array.isArray(type) || type[0] !== 'container') return null;
|
||||
return type[1];
|
||||
}
|
||||
|
||||
console.log('\n=== teleport_confirm / position_look 的字段定义(客户端 → 服务端)===');
|
||||
function dumpPacketFields(packetName) {
|
||||
// protocol 结构:types.packet = ['container', [nameField, paramsField]]
|
||||
// paramsField.type = ['switch', { compareTo: 'name', fields: { 包名: [字段...] } }]
|
||||
const paramsField = toServer.types.packet[1][1];
|
||||
const fields = paramsField.type[1].fields;
|
||||
const def = fields[packetName];
|
||||
if (!def) {
|
||||
console.log(` (没有名为 ${packetName} 的包)`);
|
||||
/** 打印一个包的字段定义 */
|
||||
function dumpFields(direction, label, packetName) {
|
||||
const typeName = packetNameMap(direction)[packetName];
|
||||
if (!typeName) {
|
||||
console.log(` ${packetName.padEnd(18)} (该版本没有这个包)`);
|
||||
return;
|
||||
}
|
||||
console.log(` ${packetName}: ` + def.map((f) => `${f.name}:${JSON.stringify(f.type)}`).join(', '));
|
||||
}
|
||||
|
||||
dumpPacketFields('teleport_confirm');
|
||||
dumpPacketFields('position');
|
||||
dumpPacketFields('position_look');
|
||||
|
||||
console.log('\n=== 客户端 ← 服务端 position 包的字段定义 ===');
|
||||
function dumpToClientFields(packetName) {
|
||||
const paramsField = mcData.protocol.play.toClient.types.packet[1][1];
|
||||
const def = paramsField.type[1].fields[packetName];
|
||||
if (!def) {
|
||||
console.log(` (没有名为 ${packetName} 的包)`);
|
||||
const fields = resolveFields(direction, typeName);
|
||||
if (!fields) {
|
||||
console.log(` ${packetName.padEnd(18)} ${typeName}(不是 container,无法展开)`);
|
||||
return;
|
||||
}
|
||||
console.log(` ${packetName}: ` + def.map((f) => `${f.name}:${JSON.stringify(f.type)}`).join(', '));
|
||||
const text = fields.map((f) => `${f.name}:${JSON.stringify(f.type)}`).join(', ');
|
||||
console.log(` ${packetName.padEnd(18)} ${text}`);
|
||||
}
|
||||
dumpToClientFields('position');
|
||||
|
||||
console.log('\n=== mineflayer 实际使用的包名 / 字段名 ===');
|
||||
const fs = require('fs');
|
||||
const path = require('path');
|
||||
const physics = fs.readFileSync(path.join(__dirname, 'node_modules', 'mineflayer', 'lib', 'plugins', 'physics.js'), 'utf8');
|
||||
const lines = physics.split('\n');
|
||||
lines.forEach((line, index) => {
|
||||
if (/teleport_confirm|teleportId|teleport_id/.test(line)) {
|
||||
console.log(` physics.js:${index + 1} ${line.trim()}`);
|
||||
console.log(`=== ${VERSION} 客户端 → 服务端:移动 / 传送相关包 ===`);
|
||||
const serverNames = packetNameMap(toServer);
|
||||
for (const name of Object.keys(serverNames).sort()) {
|
||||
if (/teleport|position|input|flying|^look$/i.test(name)) console.log(` ${name}`);
|
||||
}
|
||||
|
||||
console.log(`\n=== ${VERSION} 客户端 → 服务端:字段定义 ===`);
|
||||
for (const name of ['position', 'position_look', 'look', 'flying', 'player_input', 'teleport_confirm', 'tick_end']) {
|
||||
dumpFields(toServer, 'toServer', name);
|
||||
}
|
||||
|
||||
console.log('\n=== 关键位标志类型的展开 ===');
|
||||
for (const [name, def] of Object.entries(toServer.types)) {
|
||||
if (Array.isArray(def) && def[0] === 'bitflags' && /MovementFlags|Input/i.test(name)) {
|
||||
console.log(` ${name} = ${JSON.stringify(def)}`);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
console.log(`\n=== ${VERSION} 服务端 → 客户端:position 包字段 ===`);
|
||||
dumpFields(toClient, 'toClient', 'position');
|
||||
for (const [name, def] of Object.entries(toClient.types)) {
|
||||
if (Array.isArray(def) && def[0] === 'bitflags' && /PositionUpdateRelatives/i.test(name)) {
|
||||
console.log(` ${name} = ${JSON.stringify(def)}`);
|
||||
}
|
||||
}
|
||||
|
||||
console.log('\n=== mineflayer 实际写出的包 / 用到的字段名 ===');
|
||||
const physicsPath = path.join(__dirname, '..', 'node_modules', 'mineflayer', 'lib', 'plugins', 'physics.js');
|
||||
try {
|
||||
const lines = fs.readFileSync(physicsPath, 'utf8').split('\n');
|
||||
lines.forEach((line, index) => {
|
||||
if (/teleport_confirm|teleportId|_client\.write\(/.test(line)) {
|
||||
console.log(` physics.js:${index + 1} ${line.trim()}`);
|
||||
}
|
||||
});
|
||||
} catch (err) {
|
||||
console.error(' 读不到 mineflayer/physics.js:' + (err && err.message ? err.message : err));
|
||||
process.exitCode = 1;
|
||||
}
|
||||
|
||||
在新工单中引用
屏蔽一个用户