Files
TankIC/3DTank/tools/audit_bit_budget.js
TermiNexus 20cc0f9858 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

154 lines
6.4 KiB
JavaScript
Raw Permalink 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.
// audit_bit_budget.js —— 用 nullmap 长度形态反推「真实 opt 位总数」,校验字段表的 opt 判定
//
// 原理(编码端选最长?不,选最短):nullmap 的长度形态由编码端按本次用到的位数选最短——
// 短形态(bit7=0):总长 1..4 字节,容量 5/13/21/29(扣掉 3 位长度标签)
// 长形态(bit7=1,bit6=0):1 字节标签 + L 字节,容量 8L
// 长形态(bit7=1,bit6=1):3 字节标签 + L 字节,容量 8L
// 所以「实际容量」必然等于 minimalCapacity(真实位数)。若和我们算出的位数不一致,
// 说明字段表在某些模型上少算/多算了 opt 位——这类错误**看不出来**:opt 位为 0(有值)时字节完全一样。
//
// 两种模式:
// --calib 只看「不含容器、无短命令」的帧:这些帧 semantic 的位游标就是我们的位数,直接比容量
// --pt 只看向量记录(pt)是最后一帧、且前面记录都能解:容器项解完后位游标应正好用满容量
//
// 用法:
// node audit_bit_budget.js --calib
// node audit_bit_budget.js --pt [--top 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 Argv = process.argv.slice(2);
const Arg = (N, D) => { const I = Argv.indexOf('--' + N); return I >= 0 && Argv[I + 1] ? Argv[I + 1] : D; };
const Mode = Argv.indexOf('--pt') >= 0 ? 'pt' : 'calib';
const Top = Number(Arg('top', '20')) || 20;
// --body:文件即帧体(无长度头),配合 wss_server.js --dump-plain 的产物
const BodyMode = Argv.indexOf('--body') >= 0;
const PT_METHOD = '7640916300855664666';
function CapacityOf(Count) {
if (Count <= 29) return 8 * Math.ceil((Count + 3) / 8) - 3;
if (Count <= 504) return 8 * (Math.ceil((Count + 8) / 8) - 1);
return 8 * Math.ceil(Count / 8);
}
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;
const Reader = MakeReader(Tables, {
EnumBytes: 4, EnumInList: 4, OptBits: true, TypeAlias: Alias, StringLiteralOnly: true,
});
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;
}
const Dir = path.resolve(Arg('plain', path.join(PROJECT, 'packets', 'plain')));
const Files = [];
function Walk(P) {
for (const E of fs.readdirSync(P, { withFileTypes: true })) {
const Q = path.join(P, E.name);
if (E.isDirectory()) Walk(Q);
else if (/^s2c_\d+\.bin$/.test(E.name)) Files.push(Q);
}
}
Walk(Dir);
let Checked = 0;
const Delta = new Map();
const Rows = [];
for (const File of Files) {
// --body:文件即帧体(无长度头)
const Bodies = [];
if (BodyMode) {
try { Bodies.push(fs.readFileSync(File)); } catch (E) { continue; }
} else {
let Parsed;
try { Parsed = Link.ParseFrames(fs.readFileSync(File)); } catch (E) { continue; }
for (const F of Parsed || []) {
try { Bodies.push(Link.DecodeFrame(F)); } catch (E) {}
}
}
for (const Body of Bodies) {
let Dec;
try { Dec = Semantic.DecodeFrameBody(Body); } catch (E) { continue; }
const Recs = Dec.Records || [];
if (!Recs.length) continue;
const NmSize = Link.NullmapSize(Body, 0);
if (NmSize <= 0 || NmSize > Body.length) continue;
const Pb = Body[0];
const ActualCap = Pb & 0x80 ? 8 * (NmSize - (Pb & 0x40 ? 3 : 1)) : 8 * NmSize - 3;
let Our = null;
let Tail = '';
if (Mode === 'calib') {
// 纯字段表帧:所有记录都不走容器、不是短命令
if (Dec.Bad) continue;
if (Recs.some((R) => R.Layout || R.Short || R.Bad)) continue;
if (typeof Dec.Bits !== 'number') continue;
Our = Dec.Bits;
Tail = String(Recs[Recs.length - 1].MethodId);
} else {
// 容器帧:最后一条记录是 pt,前面没有解不动的
const R = Recs[Recs.length - 1];
if (String(R.MethodId) !== PT_METHOD || R.At === undefined) continue;
if (Recs.slice(0, -1).some((X) => X.Bad)) continue;
const Bits = NullmapBits(Body.slice(0, NmSize));
Reader.AttachBits(Bits, R.BitStart + 1);
let At = R.At + 16;
let Bad = false;
try {
const C = Body.readInt32BE(At);
At += 4 + 16 * C;
const N = Body.readInt32BE(At);
At += 4;
for (let I = 0; I < N; I++) {
const Id = Body.readBigInt64BE(At).toString();
At += 8;
if (Id === '0') { At += 8; continue; }
const Key = CodecKeyByModelId.get(Id) || '未注册';
Tail = Key;
const R2 = Reader.ReadFields(Body, At, Tables.CodecByKey.get(Key), 0);
At = R2.Next;
if (R2.Bad) { Bad = true; break; }
}
} catch (E) { Bad = true; }
Our = Reader.BitCursor().Index;
Reader.DetachBits();
if (Bad) continue;
}
Checked++;
const Want = CapacityOf(Our);
Delta.set(ActualCap - Want, (Delta.get(ActualCap - Want) || 0) + 1);
if (Want !== ActualCap) Rows.push({ File: path.relative(Dir, File), Our, ActualCap, Want, Tail });
}
}
console.log('[' + Mode + '] 可核对帧 ' + Checked + ',容量差分布(实际 - 期望):' +
JSON.stringify([...Delta.entries()].sort((A, B) => B[1] - A[1])));
Rows.sort((A, B) => (B.ActualCap - B.Our) - (A.ActualCap - A.Our));
console.log('不匹配 ' + Rows.length + ' 帧:');
for (const R of Rows.slice(0, Top)) {
console.log(' ' + R.File + ' 我们 ' + R.Our + ' 位(期望容量 ' + R.Want + ') 实际容量 ' + R.ActualCap +
' 差 ' + (R.ActualCap - R.Our) + ' 末项 ' + R.Tail);
}