工具链(3DTank/tools/):extract(模块提取)、mine_evidence/apply_names(命名证据与别名回填)、extract_codec_fields(字段表 + 全库槽位类型 + PrimClassWire/ResourceClasses)、mine_downlink(下行字段表)、extract_space_init/rebase_space_init/audit_space_init(空间模板与记录切分审计)、extract_space_session(把真实会话切成按上行触发的回放模板)、pair_flow(请求→应答配对)、probe_containers、ingest/decrypt_frames(抓包入库与解密)、serve/fetch_assets(本地站点镜像)、import_db(抓包对象仓库导入 MySQL)。 服务器(3DTank/tools/server/):link/entry/codec/container/dispatch/world/space/semantic(协议层与 lt/pt/zt 三个容器布局)、session(会话回放,顺序推进)、db + schema.sql(MySQL 连接层与对象仓库表结构)、store/responder(查库组帧与定向应答;接管范围默认只开 ba.lcr)。 同时修 pair_flow 的时间轴偏移:timeline_c2s 与 timeline_s2c 的 0 点相差 144~4453ms,改前配对整体偏约 2 秒(实测 space-100073070 偏 2084ms),改后 22.4% 的触发者被纠正。 src/、src_named/、packets/、3DTank/local/(含数据库口令)不进库,可用上述工具重新生成。
137 lines
6.9 KiB
JavaScript
137 lines
6.9 KiB
JavaScript
// probe_containers.js - 只读诊断:空间初始化模板里「容器记录」到底卡在哪
|
||
//
|
||
// 为什么单独有这个工具:容器记录(modelService 的 4 条方法)过去只会被整帧留成 hex,
|
||
// 而「解不动」有三种完全不同的原因,混在一起就变成了「再试一次也不会有进展」:
|
||
// ① 布局/边界:负载的切分口径不对(多记录帧里容器负载不该按「吃到帧尾」定界)
|
||
// ② 叶子 codec 缺失:pt/lt 项里的 modelId 在 names/codec_fields.json 查不到 codec,
|
||
// 或字段类型是推不出来的裸类名(16 个 token 之一,见 codec_fields.json 的 SlotTypeStats)
|
||
// ③ nullmap 位游标:opt 字段的「空/非空」位与记录形态位共用同一条比特流,
|
||
// 拿不到位游标时只能如实报不可解(不能退回按字节猜标志位)
|
||
//
|
||
// 本工具只读模板与 packets/plain,不写任何文件。
|
||
// 用法: node probe_containers.js [--names ../names] [--plain ../packets/plain]
|
||
'use strict';
|
||
const fs = require('fs');
|
||
const path = require('path');
|
||
const Link = require('./server/link');
|
||
const Semantic = require('./server/semantic').Load(path.join(__dirname, '..', 'names'));
|
||
|
||
const Argv = process.argv.slice(2);
|
||
const Arg = (Name, Def) => {
|
||
const I = Argv.indexOf('--' + Name);
|
||
return I >= 0 && Argv[I + 1] ? Argv[I + 1] : Def;
|
||
};
|
||
const NamesDir = path.resolve(__dirname, Arg('names', path.join('..', 'names')));
|
||
const PlainDir = path.resolve(__dirname, Arg('plain', path.join('..', 'packets', 'plain')));
|
||
|
||
const LAYOUT_BY_METHOD = Semantic.Container.LAYOUT_BY_METHOD;
|
||
const NAME_BY_METHOD = {
|
||
'3216143066888387731': 'ModelService.LoadDependencies',
|
||
'7640916300855664666': 'ModelService.LoadObjectsData',
|
||
'9038756763225879447': 'ModelService.UnloadObjects',
|
||
'8179162314198150518': 'ModelService.SetModelCommandAlias',
|
||
};
|
||
const LAYOUTS = ['zt', 'pt', 'lt'];
|
||
// model_table(modelId → 模型类)用来区分「这个 modelId 是真模型、只是 codec_fields 没抽出 codec」
|
||
// 与「这个 modelId 根本不存在 → 说明前面的项读串位了(字段表抽错/不全)」
|
||
const ModelTable = new Map(
|
||
JSON.parse(fs.readFileSync(path.join(NamesDir, 'model_table.json'), 'utf8')).Rows.map((R) => [String(R.ModelId), R]),
|
||
);
|
||
|
||
// 原因分类(每条记录只归一类,取「最先卡住」的那个)
|
||
const Causes = new Map();
|
||
function Classify(Rec) {
|
||
if (Rec.Accepted) return '✓ 已解出(布局/边界正确)';
|
||
const Why = Rec.Why || '';
|
||
if (/没有 nullmap 比特流|位游标/.test(Why)) return '③ nullmap 位游标拿不到(前一条记录的字段表不在 codec_fields)';
|
||
const M = /modelId=(\d+)/.exec(Why);
|
||
if (M && !ModelTable.has(M[1])) return '⑤ 字段表抽错/不全 → 项读串位(例如 MapCodecInfo 被抽成 enum<…>)';
|
||
if (M) return '② 叶子 codec 缺失(modelId 在 model_table 里有,codec_fields 里没有 codec)';
|
||
if (/无 codec:|没有对应 codec/.test(Why)) return '② 叶子 codec 缺失(字段类型是推不出 codec 的裸类名)';
|
||
if (/抛出异常|越界|不合理|超上限/.test(Why)) return '① 布局/边界不对(负载切分口径)';
|
||
return '④ 其它:' + Why.split(':')[0];
|
||
}
|
||
|
||
const MissingModelIds = new Map();
|
||
const MissingTypes = new Map();
|
||
const Rows = [];
|
||
|
||
for (const File of fs.readdirSync(NamesDir).filter((X) => /^space-init-.*\.json$/.test(X)).sort()) {
|
||
if (/^space-init-login-/.test(File)) continue;
|
||
const T = JSON.parse(fs.readFileSync(path.join(NamesDir, File), 'utf8'));
|
||
const Dir = path.join(PlainDir, 'space-' + T.SpaceId);
|
||
for (const Fr of T.Frames || []) {
|
||
const Plain = path.join(Dir, Fr.File);
|
||
if (!fs.existsSync(Plain)) continue;
|
||
const Parsed = Link.ParseFrames(fs.readFileSync(Plain));
|
||
if (!Parsed || Parsed.length !== 1) continue;
|
||
const Body = Link.DecodeFrame(Parsed[0]);
|
||
const Decoded = Semantic.DecodeFrameBody(Body);
|
||
for (const [Idx, R] of Decoded.Records.entries()) {
|
||
if (R.ObjectId === undefined || R.At === undefined) continue;
|
||
const MethodId = String(R.MethodId);
|
||
const Expect = LAYOUT_BY_METHOD[MethodId];
|
||
if (!Expect) continue;
|
||
const Off = R.At + 16;
|
||
const Limit = Body.length - Off;
|
||
const Row = {
|
||
File,
|
||
Frame: Fr.File,
|
||
Ord: Idx + 1,
|
||
Method: NAME_BY_METHOD[MethodId] || MethodId,
|
||
Expect,
|
||
Load: Limit,
|
||
Why: '',
|
||
Accepted: false,
|
||
Tried: [],
|
||
};
|
||
// 三布局各试一遍,把「吃到哪 / 在哪中断 / 为什么」都记下来
|
||
let Hit = null;
|
||
for (const L of LAYOUTS) {
|
||
const B = Semantic.Container.Read(L, Body, Off);
|
||
Row.Tried.push(L + (B.Bad ? ' 中断@+' + (B.Next - Off) : ' 吃到 +' + (B.Next - Off)) + (B.Why ? '(' + B.Why + ')' : ''));
|
||
if (!B.Bad && L === Expect) Hit = L;
|
||
}
|
||
const R2 = Semantic.Container.Read(Expect, Body, Off);
|
||
if (!R2.Bad) {
|
||
Row.Accepted = true;
|
||
Row.Consumed = R2.Next - Off;
|
||
Row.Layout = Expect;
|
||
} else {
|
||
Row.Why = R2.Why || '(无原因)';
|
||
}
|
||
// 缺 codec 统计:从容器值里扫 modelId / 从原因串里抠 modelId、类型名
|
||
const M = /modelId=(\d+)/.exec(Row.Why);
|
||
if (M) MissingModelIds.set(M[1], (MissingModelIds.get(M[1]) || 0) + 1);
|
||
const MT = /无 codec:([^)]+)/.exec(Row.Why);
|
||
if (MT) MissingTypes.set(MT[1], (MissingTypes.get(MT[1]) || 0) + 1);
|
||
Row.Cause = Classify(Row);
|
||
Causes.set(Row.Cause, (Causes.get(Row.Cause) || 0) + 1);
|
||
Rows.push(Row);
|
||
}
|
||
}
|
||
}
|
||
|
||
console.log('=== 容器记录诊断(只读;数据源 ' + NamesDir + ' + ' + PlainDir + ')===');
|
||
console.log('');
|
||
for (const R of Rows) {
|
||
console.log(R.File.padEnd(32) + ' ' + String(R.Frame).padEnd(14) + ' 第' + String(R.Ord).padStart(2) + '条 ' +
|
||
R.Method.padEnd(30) + ' 负载 ' + String(R.Load).padStart(7) + 'B 期望布局=' + R.Expect);
|
||
console.log(' 三布局:' + R.Tried.join(' | '));
|
||
if (R.Accepted) console.log(' ✓ 解出 ' + R.Consumed + 'B(' + R.Layout + ');原因分类:' + R.Cause);
|
||
else console.log(' ✗ ' + R.Cause + ';卡点:' + R.Why);
|
||
}
|
||
console.log('');
|
||
console.log('=== 汇总 ===');
|
||
console.log('容器记录 ' + Rows.length + ' 条:已解出 ' + Rows.filter((R) => R.Accepted).length +
|
||
' / 仍不可解 ' + Rows.filter((R) => !R.Accepted).length);
|
||
for (const [K, V] of [...Causes].sort((A, B) => B[1] - A[1])) console.log(' ' + String(V).padStart(3) + ' ' + K);
|
||
if (MissingModelIds.size) {
|
||
console.log(' 缺 codec 的 modelId(去重 ' + MissingModelIds.size + ' 个):');
|
||
for (const [K, V] of [...MissingModelIds].sort((A, B) => B[1] - A[1])) console.log(' ' + K + ' ×' + V);
|
||
}
|
||
if (MissingTypes.size) {
|
||
console.log(' 缺 codec 的裸类名(去重 ' + MissingTypes.size + ' 个):');
|
||
for (const [K, V] of [...MissingTypes].sort((A, B) => B[1] - A[1])) console.log(' ' + K + ' ×' + V);
|
||
}
|