Files
TankIC/3DTank/tools/probe_containers.js
T
TermiNexus b6f2cb71ee feat(3dtank): 协议逆向工程与自研服务器入库(工具链 + 协议表 + 空间回放 + MySQL)
工具链(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/(含数据库口令)不进库,可用上述工具重新生成。
2026-09-17 20:00:41 +08:00

137 lines
6.9 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
// 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);
}