Files
TankIC/3DTank/tools/probe_pt_record.js
T
TermiNexus b083d27165 fix(protocol): TankState 按 23 字节定长 blob(T5)解码,修复 TankCC 后续字节错位
- codec_fields:module_87072::T5 记为单字段 bytes23;pq.tankState 的 TypeCodec 由 85183::jq 改指 87072::T5
- 生成器支持 bytes<N>(ByteArray / R.ReadRaw(N) / W.WriteRaw),重生成 Module87072、Module85183
- 审计链:lib_space_codec 新增 bytes<N>;audit_pt_boundaries / audit_bit_budget 支持 --body/--plain;semantic 暴露 Out.Bits
- 新增工具:audit_bit_budget(nullmap 位预算)、probe_pt_record(pt 项级)、analyze_item_trace(客户端项级打点对照)、dump_codec_src
- wss_server 新增 --dump-plain:把解密+解压后的裸帧体落到 plain/,供离线审计
- 镜像打点:opt 全量位位置、pt 项级字节/位区间、TankCC 逐字段区间;重新打包 unpacked.pack.json
2026-10-21 15:21:10 +08:00

145 lines
6.0 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_record.js —— 列出某个 pt 记录里每一项的 modelId / codec / 字节区间 / opt 位数
//
// 用途:定位「位游标与客户端不一致」时,究竟是前面哪一项少算了 opt 位。
// 用法: node _probe_record.js <相对 packets/plain 的文件> [记录序号=1] [最多项=20]
'use strict';
const fs = require('fs');
const path = require('path');
const { LoadTables, MakeReader } = require('./lib_space_codec');
const Link = require('./server/link');
const Semantic = require('./server/semantic').Load(path.join(__dirname, '..', 'names'));
const PROJECT = path.resolve(__dirname, '..');
const File = path.resolve(PROJECT, 'packets', 'plain', process.argv[2]);
const WantRecord = Number(process.argv[3] || '1');
const MaxItems = Number(process.argv[4] || '20');
// 可选:只打印 codec 键包含该子串的项(其余项照常推进,只是不打印)
const KeyFilter = process.argv[5] || '';
// 可选:<项序号>:<位数> —— 在第 N 项(1 基)开始前额外推进位游标(模拟「前面的项多读了几位」)
const InsertArg = (process.argv[6] || '').split(':');
const InsertAt = InsertArg.length === 2 ? Number(InsertArg[0]) - 1 : -1;
const InsertBits = InsertArg.length === 2 ? Number(InsertArg[1]) : 0;
// 可选:--tree 打印每一项的字段树(含嵌套模型 / 列表元素),用来核对「嵌套里到底有几个元素」
const Tree = process.argv.indexOf('--tree') >= 0;
const Tables = LoadTables(path.join(PROJECT, 'names'));
const Binding = JSON.parse(fs.readFileSync(path.join(PROJECT, 'names', 'model_binding.json'), 'utf8'));
const CodecKeyByModelId = new Map();
for (const E of Binding.Entries) CodecKeyByModelId.set(String(E.ModelId), E.Key);
const RESOURCE_CLASS_WIRE = {
Ea: 'i64', nh: 'i64', W_: 'i64', uo: 'i64', pv: 'i64', B_: 'i64', Da: 'i64',
qs: 'i64', uh: 'i64', Gf: 'i64', kv: 'i64', Pc: 'i64',
};
const Alias = {};
for (const [N, R] of Object.entries(Tables.ResourceClasses || {})) Alias[N] = R.Wire;
for (const [N, W] of Object.entries(RESOURCE_CLASS_WIRE)) Alias[N] = W;
let TreeOn = false;
const Reader = MakeReader(Tables, {
EnumBytes: 4, EnumInList: 4, OptBits: true, TypeAlias: Alias, StringLiteralOnly: true,
OnField: (I) => {
if (!TreeOn) return;
console.log(' ' + ' '.repeat(I.Depth) + I.Name + ' <' + I.Type + '> ' + I.Start + '→' + I.Next +
' bit ' + I.BitBefore + '→' + I.BitAfter + (I.Bad ? ' BAD ' + I.Why : ''));
},
});
// 位流(与 audit 同口径)
function NullmapBits(Bytes) {
const Bits = [];
const Push = (B) => { for (let K = 7; K >= 0; K--) Bits.push((B >> K) & 1); };
if (Bytes.length && Bytes[0] & 0x80) {
for (let I = Bytes[0] & 0x40 ? 3 : 1; I < Bytes.length; I++) Push(Bytes[I]);
} else {
for (const B of Bytes) Push(B);
Bits.splice(0, 3);
}
return Bits;
}
// 一个 codec(含嵌套)会消耗的 opt 位数(只看字段表)
function CountOptBits(Key, Depth) {
if (Depth > 6) return 0;
const C = Tables.CodecByKey.get(Key);
if (!C) return 0;
let N = 0;
for (const F of C.Fields || []) N += CountTypeOptBits(F.Type, Depth);
return N;
}
function CountTypeOptBits(Type, Depth) {
if (!Type) return 0;
let N = 0;
let T = Type.trim();
if (T.startsWith('opt<')) { N++; T = T.slice(4, -1).trim(); }
const M = /^(list|enum|map)<([\s\S]*)>$/.exec(T);
if (M) {
const Inner = M[2].trim();
if (M[1] === 'map') {
const P = Inner.split(',');
N += CountTypeOptBits(P[0], Depth + 1) + CountTypeOptBits(P[1], Depth + 1);
} else N += CountTypeOptBits(Inner, Depth + 1);
return N;
}
const K = FindCodecKey(T);
if (K) N += CountOptBits(K, Depth + 1);
return N;
}
function FindCodecKey(Name) {
if (Tables.CodecByKey.has(Name)) return Name;
for (const [K] of Tables.CodecByKey) if (K.endsWith('::' + Name)) return K;
return null;
}
const Body = process.argv.indexOf('--body') >= 0
? fs.readFileSync(File)
: Link.DecodeFrame(Link.ParseFrames(fs.readFileSync(File))[0]);
const NmSize = Link.NullmapSize(Body, 0);
const Bits = NullmapBits(Body.slice(0, NmSize));
console.log('帧体 ' + Body.length + ' 字节,nullmap ' + NmSize + ' 字节 = ' + Bits.length + ' 位');
const Decoded = Semantic.DecodeFrameBody(Body);
let Seen = 0;
for (const R of Decoded.Records || []) {
if (String(R.MethodId) !== '7640916300855664666') continue;
Seen++;
if (Seen !== WantRecord) continue;
const At0 = R.At + 16;
const C = Body.readInt32BE(At0);
let At = At0 + 4 + 16 * C;
const D = Body.readInt32BE(At);
At += 4;
Reader.AttachBits(Bits, R.BitStart + 1);
console.log('记录#' + WantRecord + ' 项数 ' + D + ' 起始 byte=' + At + ' bit=' + (R.BitStart + 1));
for (let I = 0; I < D && I < MaxItems; I++) {
if (InsertBits && I === InsertAt) {
Reader.AttachBits(Bits, Reader.BitCursor().Index + InsertBits);
console.log(' --- 第 ' + (I + 1) + ' 项前额外推进 ' + InsertBits + ' 位 ---');
}
const AtId = At;
const Id = Body.readBigInt64BE(At).toString();
const Key = CodecKeyByModelId.get(Id) || '?';
At += 8;
const Bit0 = Reader.BitCursor().Index;
const B0 = At;
if (Id === '0') {
At += 8;
console.log(' [' + String(I + 1).padStart(3) + '] <内联> id@' + AtId + ' byte ' + B0 + '→' + At + ' bit ' + Bit0 + '→' + Reader.BitCursor().Index);
continue;
}
if (Tree && Key && Key.includes(KeyFilter || '::')) {
console.log(' [树] 第 ' + (I + 1) + ' 项 ' + Key);
TreeOn = true;
}
const Res = Reader.ReadFields(Body, At, Tables.CodecByKey.get(Key), 0);
TreeOn = false;
At = Res.Next;
const Bit1 = Reader.BitCursor().Index;
const Opt = Key && Key !== '?' ? CountOptBits(Key, 0) : -1;
if (KeyFilter && !String(Key).includes(KeyFilter)) continue;
console.log(' [' + String(I + 1).padStart(3) + '] ' + Key + ' model=' + Id + ' id@' + AtId +
' byte ' + B0 + '→' + At + '(' + (At - B0) + ') bit ' + Bit0 + '→' + Bit1 +
'(+' + (Bit1 - Bit0) + ') 字段表 opt 共 ' + Opt + (Res.Bad ? ' BAD ' + Res.Why : ''));
}
break;
}