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
This commit is contained in:
TermiNexus committed 2026-10-21 15:21:10 +08:00
1 parent 2ce2fd0363
commit b083d27165
16 files changed
+773 -60

No files matched your search

+16 -3
View File
@@ -51416,7 +51416,7 @@
"Prop": "ml5_1",
"Wire": "",
"TypeRaw": "opt<GV>",
"TypeCodec": "module_85183::jq"
"TypeCodec": "module_87072::T5"
},
{
"Order": 7,
@@ -56319,8 +56319,21 @@
"ModelClassReal": "Vy",
"ModelName": "TankState",
"Slots": [],
"Fields": [],
"ModelKeys": []
"Fields": [
{
"Order": 1,
"Name": "packed",
"Type": "bytes23",
"Js": "",
"Slot": "",
"Desc": "",
"Prop": "",
"Wire": "",
"TypeRaw": "bytes23"
}
],
"ModelKeys": [],
"RebuiltFrom": "module_87072.js:53876 T5.t7h:this.j101_1.v100(t.g7h_1, 23)"
},
"module_88693::tt": {
"Module": "module_88693",
+164
View File
@@ -0,0 +1,164 @@
// analyze_item_trace.js —— 用客户端 pt 项打点(module_45686::lt.t7h)核对「每个模型到底消费几个 opt 位」
//
// 打点行(names/item_trace.json):{ own, b0, b1, p0, p1 }
// own = modelId 对象(Kotlin Long)的自有属性串,如 "f20_1=0;g20_1=123;"
// b0/b1 = 本项负载起止字节;p0/p1 = 本项消费的绝对位区间(p1-p0 就是该项的 opt 位数)
//
// 做法:把 own 还原成 modelId(用 model_binding 校验哪种高低位组合能命中已知 modelId),
// 再按 modelId 汇总「客户端实测位数」,与字段表递归算出的位数对比,打印不一致的模型。
//
// 用法: node analyze_item_trace.js [--top 30]
'use strict';
const fs = require('fs');
const path = require('path');
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 Top = Number(Arg('top', '30')) || 30;
const J = JSON.parse(fs.readFileSync(path.join(PROJECT, 'names', 'codec_fields.json'), 'utf8'));
const Binding = JSON.parse(fs.readFileSync(path.join(PROJECT, 'names', 'model_binding.json'), 'utf8'));
const KeyByModelId = new Map();
const KnownIds = new Set();
for (const E of Binding.Entries) {
KeyByModelId.set(String(E.ModelId), E.Key);
KnownIds.add(String(E.ModelId));
}
// 字段表递归数 opt 位(与 audit_bit_budget 同口径;list/map 不计元素,元素数由数据决定)
function OptBitsOfKey(Key, Depth) {
if (Depth > 8) return null;
const C = J.Codecs[Key];
if (!C) return null;
let N = 0;
for (const F of C.Fields || []) {
const V = OptBitsOfType(F.Type, Depth);
if (V === null) return null;
N += V;
}
return N;
}
function OptBitsOfType(Type, Depth) {
if (!Type) return null;
let T = String(Type).trim();
let N = 0;
if (T.startsWith('opt<')) {
N++;
T = T.slice(4, -1).trim();
}
const M = /^(list|enum|map)<([\s\S]*)>$/.exec(T);
if (M) {
// 列表/映射的元素在运行时才知道有几个 → 这里只算元素类型自身的「固定」部分
if (M[1] === 'map') {
const P = M[2].split(',');
const A = OptBitsOfType(P[0], Depth + 1);
const B = OptBitsOfType(P[1], Depth + 1);
return A === null || B === null ? N : N + A + B;
}
const Inner = OptBitsOfType(M[2], Depth + 1);
return Inner === null ? N : N + Inner;
}
const K = FindKey(T);
if (!K) return N; // 基础类型 / 未知:只算自身的 opt 位
const Sub = OptBitsOfKey(K, Depth + 1);
if (Sub === null) return N;
return N + Sub;
}
function FindKey(Name) {
if (J.Codecs[Name]) return Name;
for (const K of Object.keys(J.Codecs)) if (K.endsWith('::' + Name)) return K;
// 字段里写的是「模型真名」(如 ParticleEffectsEntity)而不是 codec 键
for (const [K, C] of Object.entries(J.Codecs)) if (C.ModelName === Name) return K;
return null;
}
// own → modelId
function ParseId(Own) {
if (!Own) return null;
const Pairs = Own.split(';').filter(Boolean).map((P) => P.split('='));
const Nums = Pairs.map((P) => Number(P[1])).filter((V) => Number.isFinite(V));
if (Nums.length === 1) return String(Nums[0]);
if (Nums.length !== 2) return null;
const [A, B] = Nums;
const Cand = [
String(BigInt(A) * 4294967296n + BigInt(B >>> 0)), // (high, low)
String(BigInt(B) * 4294967296n + BigInt(A >>> 0)), // (low, high)
];
for (const C of Cand) if (KnownIds.has(C)) return C;
return Cand[0];
}
const Raw = JSON.parse(fs.readFileSync(path.join(PROJECT, 'names', 'item_trace.json'), 'utf8'));
const Rows = Raw.trace || Raw.rows || [];
console.log('pt 项打点 ' + Rows.length + ' 行(at ' + Raw.at + ')');
// --frames N:按「b0 回落」切帧,打印每项 modelId / 位区间 / 字节区间,便于和本地 packets 对照
const WantFrames = Number(Arg('frames', '0')) || 0;
if (WantFrames) {
const Frames = [];
let Cur = null;
let PrevB0 = -1;
for (const R of Rows) {
if (!Cur || R.b0 < PrevB0) { Cur = []; Frames.push(Cur); }
Cur.push(R);
PrevB0 = R.b0;
}
console.log('切出 ' + Frames.length + ' 帧');
const PerFrame = Number(Arg('per-frame', '40')) || 40;
Frames.slice(0, WantFrames).forEach((F, I) => {
console.log('帧#' + (I + 1) + ' 共 ' + F.length + ' 项:');
let PrevBuf = null;
let PrevNm = null;
for (const R of F.slice(0, PerFrame)) {
const Id = ParseId(R.own) || '?';
const Key = KeyByModelId.get(Id) || '';
// buf 变化 = 换了位流(嵌套消息新开一条);nm = 位流总位数
const Sw = PrevBuf !== null && R.buf !== undefined && R.buf !== PrevBuf;
const NmSw = PrevNm !== null && R.nm !== undefined && R.nm !== PrevNm;
if (Sw || NmSw) console.log(' ---- 位流切换:buf ' + PrevBuf + '→' + R.buf + ',nm ' + PrevNm + '→' + R.nm + ' ----');
PrevBuf = R.buf;
PrevNm = R.nm;
console.log(' id=' + Id.padStart(20) + ' ' + Key.padEnd(22) + ' byte ' + R.b0 + '→' + R.b1 +
'(' + (R.b1 - R.b0) + ') 位 ' + R.p0 + '→' + R.p1 + '(' + (R.p1 - R.p0) + ')' +
(R.nm === undefined ? '' : ' 流长 ' + R.nm + ' buf#' + R.buf));
}
if (F.length > PerFrame) console.log(' …还有 ' + (F.length - PerFrame) + ' 项');
});
process.exit(0);
}
const Stats = new Map(); // modelId -> { Count, Bits: Map }
let UnknownId = 0;
for (const R of Rows) {
const Id = ParseId(R.own);
if (!Id) { UnknownId++; continue; }
const Key = KeyByModelId.get(Id);
if (!Key) { UnknownId++; continue; }
const Bits = R.p1 - R.p0;
let S = Stats.get(Id);
if (!S) { S = { Id, Key, Count: 0, Bits: new Map() }; Stats.set(Id, S); }
S.Count++;
S.Bits.set(Bits, (S.Bits.get(Bits) || 0) + 1);
}
console.log('可识别项 ' + [...Stats.values()].reduce((A, S) => A + S.Count, 0) + ',不可识别 ' + UnknownId);
const Diffs = [];
const Same = [];
for (const S of Stats.values()) {
const Our = OptBitsOfKey(S.Key, 0);
const BitsList = [...S.Bits.entries()].sort((A, B) => B[1] - A[1]);
const Main = BitsList[0][0];
const Rec = { Key: S.Key, Id: S.Id, Count: S.Count, Our, Main, BitsList };
if (Our === null || Main !== Our) Diffs.push(Rec);
else Same.push(Rec);
}
Diffs.sort((A, B) => B.Count - A.Count);
console.log('一致 ' + Same.length + ' 个模型;不一致 ' + Diffs.length + ' 个:');
for (const D of Diffs.slice(0, Top)) {
console.log(' ' + D.Key + '(modelId ' + D.Id + ')出现 ' + D.Count + ' 次:客户端实测位 ' +
JSON.stringify(D.BitsList) + ',字段表递归算 ' + D.Our);
}
const SameTop = Same.sort((A, B) => B.Count - A.Count).slice(0, 10);
console.log('一致的模型(前 10):' + SameTop.map((S) => S.Key + '×' + S.Count + '(' + S.Main + '位)').join(', '));
+153
View File
@@ -0,0 +1,153 @@
// 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);
}
+16 -13
View File
@@ -27,6 +27,8 @@ const Arg = (Name, Def) => {
const PROJECT = path.resolve(__dirname, '..');
const PlainDir = path.resolve(Arg('plain', path.join(PROJECT, 'packets', 'plain')));
const Limit = Number(Arg('limit', '0')) || 0;
// --body:文件即帧体(无长度头),配合 wss_server.js --dump-plain 的产物
const BodyMode = Argv.indexOf('--body') >= 0;
const Tables = LoadTables(path.join(PROJECT, 'names'));
const Binding = JSON.parse(fs.readFileSync(path.join(PROJECT, 'names', 'model_binding.json'), 'utf8'));
@@ -59,6 +61,7 @@ if (!Number.isFinite(EnumBytes) || !Number.isFinite(EnumInList)) throw new Error
const Reader = MakeReader(Tables, {
EnumBytes,
EnumInList,
EnumListOptional: Argv.indexOf('--enum-list-optional') >= 0,
OptBits: true,
TypeAlias: Alias,
StringLiteralOnly: true,
@@ -210,20 +213,20 @@ function AuditPt(Body, Off, File, BitStart, Label) {
function ScanFile(File) {
Stats.Files++;
let Frames = null;
try {
Frames = Link.ParseFrames(fs.readFileSync(File));
} catch (E) {
return;
}
if (!Frames) return;
for (const F of Frames) {
let Body;
try {
Body = Link.DecodeFrame(F);
} catch (E) {
continue;
// --body:文件内容就是「解密 + 解压后的帧体」(wss_server.js --dump-plain 的产物),
// 没有 2/4 字节长度头,不能再走 Link.ParseFrames。
const Bodies = [];
if (BodyMode) {
try { Bodies.push(fs.readFileSync(File)); } catch (E) { return; }
} else {
let Frames = null;
try { Frames = Link.ParseFrames(fs.readFileSync(File)); } catch (E) { return; }
if (!Frames) return;
for (const F of Frames) {
try { Bodies.push(Link.DecodeFrame(F)); } catch (E) {}
}
}
for (const Body of Bodies) {
// 用 semantic 顺序解完整帧:它维护跨记录的位游标,并给每条记录标出 BitStart。
// 只审计 pt 记录;其它记录的负载长度由 semantic 负责推进(短命令会中断整帧,属已知限制)。
let Decoded = null;
+40
View File
@@ -0,0 +1,40 @@
// _dump_codec_src.js —— 打印指定 codec 的 u7h / t7h 源码体(临时诊断用)
// 用法: node _dump_codec_src.js 85183::Tl 85183::Rl ...
'use strict';
const fs = require('fs');
const path = require('path');
const SRC = path.join(__dirname, '..', 'src');
function MatchBrace(Src, Start) {
let Depth = 0;
for (let I = Start; I < Src.length; I++) {
if (Src[I] === '{') Depth++;
else if (Src[I] === '}') { Depth--; if (Depth === 0) return I; }
}
return -1;
}
function ClassMethod(Src, Cls, Kind) {
const Head = new RegExp('\\(\\s*' + Cls.replace(/\$/g, '\\$') + '\\s*\\)\\.' + Kind + '\\s*=\\s*function\\s*\\([^)]*\\)\\s*\\{', 'g');
const Out = [];
let M;
while ((M = Head.exec(Src))) {
const Start = M.index + M[0].length - 1;
const End = MatchBrace(Src, Start);
if (End > 0) Out.push(Src.slice(Start, End + 1));
}
return Out;
}
for (const Key of process.argv.slice(2)) {
const [Mod, Cls] = Key.split('::');
const File = path.join(SRC, (Mod.startsWith('module_') ? Mod : 'module_' + Mod) + '.js');
if (!fs.existsSync(File)) { console.log('!! 无源码 ' + File); continue; }
const Src = fs.readFileSync(File, 'utf8');
console.log('=== ' + Key + ' ===');
for (const Kind of ['u7h', 't7h']) {
const Bodies = ClassMethod(Src, Cls, Kind);
if (!Bodies.length) { console.log(' [' + Kind + '] 未找到'); continue; }
Bodies.forEach((B, I) => console.log(' [' + Kind + '#' + I + '] ' + B.replace(/\s+/g, ' ')));
}
}
+10
View File
@@ -270,6 +270,16 @@ function MakeReader(Tables, Options) {
return { Value: Pairs, Next: At, Bad };
}
// 定长二进制块:Type = bytes<N>
// 出处:module_87072.js:53876 `(Zy(T5).t7h = function (t) { this.j101_1.v100(t.g7h_1, 23); … })`
// —— TankState 的线上形态是一个 23 字节的打包姿态 blob(内部按 19/13/13/13 位量化 4 个向量)。
// 我们不解释内容,只按定长吃掉,保证后续字节对得上。
const Fixed = /^bytes(\d+)$/.exec(T);
if (Fixed) {
const N = Number(Fixed[1]);
if (Off + N > Buf.length) return { Value: '<越界>', Next: Off, Bad: true };
return { Value: '0x' + Buf.slice(Off, Off + N).toString('hex'), Next: Off + N };
}
switch (T) {
case 'bool':
if (Off >= Buf.length) return { Value: '<越界>', Next: Off, Bad: true };
+144
View File
@@ -0,0 +1,144 @@
// _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;
}
+2
View File
@@ -342,6 +342,8 @@ function Load(NamesDir) {
}
Off += 16 + R.Consumed;
}
// 诊断用:本帧消费到的位索引(nullmap 容量自检见 tools/_nullmap_capacity.js)
Out.Bits = Ctx.Index;
return Out;
}
+14
View File
@@ -64,6 +64,10 @@ const PACKET_DIR = path.resolve(Arg('dir', path.join(PROJECT, 'packets', LABEL))
// --no-hex 1 log.txt 不写十六进制
// --quiet 1 只写 log.txt,不刷控制台
const DUMP_EVERY = Number(Arg('dump-every', '1'));
// 额外落一份「解密 + 解压后的帧体」到 <session>/plain/s2c_%04d.bin:
// 审计工具(audit_pt_boundaries / audit_bit_budget)要的是这种裸帧体,
// 原始 .bin 落的是收到的原始字节(加密段里是密文),离线没法直接解。
const DUMP_PLAIN = Arg('dump-plain', '1') === '1';
const WRITE_LOG = Arg('log', '1') === '1';
const WRITE_HEX = Arg('no-hex', '0') !== '1';
const QUIET = Arg('quiet', '0') === '1';
@@ -500,6 +504,16 @@ class StreamDecoder {
}
}
const At = NullmapSize(Body, 0);
// 明文帧体落盘(审计用,见文件头 --dump-plain):<session>/plain/s2c_%04d.bin
if (DUMP_PLAIN && this.Direction === 's2c' && this.State.Dir) {
this.PlainSeq = (this.PlainSeq || 0) + 1;
const P = path.join(this.State.Dir, 'plain', 's2c_' + String(this.PlainSeq).padStart(4, '0') + '.bin');
EnqueueSave(Body.length, (Done) => {
fs.mkdir(path.dirname(P), { recursive: true }, () => {
fs.writeFile(P, Body, () => Done());
});
});
}
if (At >= Body.length) {
this.State.Log(Tag + '保活 ping (' + Buf.length + 'B)' + Note);
continue;
+1 -1
View File
@@ -1 +1 @@
{"version":"18a538259d2f8c3a","base":"/play/static/js/unpacked/","count":127,"files":{"module_00806.js":"// ============================================================\n// Module ID: 806\n// ============================================================\n;(window.__modules__ = window.__modules__ || {})[806] = (t, n, i) => {\n \"use strict\";\n var r = i(19487),\n e = {\n childContextTypes: !0,\n contextType: !0,\n contextTypes: !0,\n defaultProps: !0,\n displayName: !0,\n getDefaultProps: !0,\n getDerivedStateFromError: !0,\n getDerivedStateFromProps: !0,\n mixins: !0,\n propTypes: !0,\n type: !0,\n },\n u = { name: !0, length: !0, prototype: !0, caller: !0, callee: !0, arguments: !0, arity: !0 },\n s = { $$typeof: !0, compare: !0, defaultProps: !0, displayName: !0, propTypes: !0, type: !0 },\n o = {};\n function _(t) {\n return r.isMemo(t) ? s : o[t.$$typeof] || e;\n }\n ((o[r.ForwardRef] = { $$typeof: !0, render: !0, defaultProps: !0, displayName: !0, propTypes: !0 }),\n (o[r.Memo] = s));\n var h = Object.defineProperty,\n a = Object.getOwnPropertyNames,\n c = Object.getOwnPropertySymbols,\n l = Object.getOwnPropertyDescriptor,\n f = Object.getPrototypeOf,\n v = Object.prototype;\n t.exports = function t(n, i, r) {\n if (\"string\" != typeof i) {\n if (v) {\n var e = f(i);\n e && e !== v && t(n, e, r);\n }\n var s = a(i);\n c && (s = s.concat(c(i)));\n for (var o = _(n), p = _(i), w = 0; w < s.length; ++w) {\n var d = s[w];\n if (!(u[d] || (r && r[d]) || (p && p[d]) || (o && o[d]))) {\n var g = l(i, d);\n try {\n h(n, d, g);\n } catch (t) {}\n }\n }\n }\n return n;\n };\n};\n","module_00905.js":"// ============================================================\n// Module ID: 905\n// ============================================================\n;(window.__modules__ = window.__modules__ || {})[905] = (t) => {\n \"use strict\";\n t.exports = \"SECRET_DO_NOT_PASS_THIS_OR_YOU_WILL_BE_FIRED\";\n};\n","module_01510.js":"// ============================================================\n// Module ID: 1510\n// ============================================================\n;(window.__modules__ = window.__modules__ || {})[1510] = (t, n, i) => {\n !(function (t, n, i, r, e) {\n \"use strict\";\n var u,\n s,\n o,\n _,\n h,\n a,\n c,\n l,\n f,\n v,\n p,\n w,\n d,\n g,\n m,\n k,\n z = n.$_$.ob,\n b = n.$_$.w4,\n q = i.$_$.e,\n y = n.$_$.he,\n $ = n.$_$.xj,\n x = n.$_$.h5,\n j = n.$_$.o1,\n S = n.$_$.gd,\n C = i.$_$.d,\n E = n.$_$.r5,\n T = n.$_$.ud,\n I = i.$_$.f,\n A = n.$_$.nf,\n R = r.$_$.s1,\n P = e.$_$.n,\n N = n.$_$.rf,\n M = n.$_$.dd,\n O = n.$_$.sj,\n D = i.$_$.r,\n L = i.$_$.b,\n B = e.$_$.f,\n U = n.$_$.yb,\n F = n.$_$.c,\n G = e.$_$.f1,\n H = n.$_$.ge,\n W = e.$_$.l,\n V = n.$_$.nj,\n K = n.$_$.ke;\n function Y(t) {\n var n = t.zuy_1;\n if (null != n) return n;\n z(\"codec_appId\");\n }\n function X() {}\n function J() {}\n function Z(t) {\n var n = t.cuz_1;\n if (null != n) return n;\n z(\"codec_questToAchievementMap\");\n }\n function Q() {}\n function tt(t) {\n var n = t.fuz_1;\n if (null != n) return n;\n z(\"codec_itemIdMap\");\n }\n function nt() {}\n function it() {\n ((this.iuz_1 = new R(A(I), null)), (this.juz_1 = new R(A(P), null)));\n }\n function rt() {\n (B.call(this),\n (this.nuz_1 = new R(A(I), null)),\n (this.ouz_1 = new R(A(P), null)),\n (this.quz_1 = new O(-621796162, 1132145095)),\n (this.ruz_1 = new O(1635469714, 1785333818)),\n (this.suz_1 = new O(-773564903, 1989636776)),\n (this.tuz_1 = new O(497901439, 366177258)),\n this.tay(),\n (this.uuz_1 = this.quz_1));\n }\n function et(t) {\n var n = N;\n return t.xuz_1.e2b(\n t,\n M(\n \"protocol\",\n 1,\n n,\n function (t) {\n return et(t);\n },\n null,\n ),\n );\n }\n function ut() {\n ((this.xuz_1 = new R(A(I), null)), (this.zuz_1 = new O(-1272608814, 1097348888)));\n }\n function st() {\n return ((t = H(y(ot))), ot.call(t), t);\n var t;\n }\n function ot() {}\n function _t(t) {\n var n = N;\n return t.fv0_1.e2b(\n t,\n M(\n \"protocol\",\n 1,\n n,\n function (t) {\n return _t(t);\n },\n null,\n ),\n );\n }\n function ht(t) {\n var n = N;\n return t.gv0_1.e2b(\n t,\n M(\n \"modelRegistry\",\n Line truncated
{"version":"c0f14bae364a627c","base":"/play/static/js/unpacked/","count":127,"files":{"module_00806.js":"// ============================================================\n// Module ID: 806\n// ============================================================\n;(window.__modules__ = window.__modules__ || {})[806] = (t, n, i) => {\n \"use strict\";\n var r = i(19487),\n e = {\n childContextTypes: !0,\n contextType: !0,\n contextTypes: !0,\n defaultProps: !0,\n displayName: !0,\n getDefaultProps: !0,\n getDerivedStateFromError: !0,\n getDerivedStateFromProps: !0,\n mixins: !0,\n propTypes: !0,\n type: !0,\n },\n u = { name: !0, length: !0, prototype: !0, caller: !0, callee: !0, arguments: !0, arity: !0 },\n s = { $$typeof: !0, compare: !0, defaultProps: !0, displayName: !0, propTypes: !0, type: !0 },\n o = {};\n function _(t) {\n return r.isMemo(t) ? s : o[t.$$typeof] || e;\n }\n ((o[r.ForwardRef] = { $$typeof: !0, render: !0, defaultProps: !0, displayName: !0, propTypes: !0 }),\n (o[r.Memo] = s));\n var h = Object.defineProperty,\n a = Object.getOwnPropertyNames,\n c = Object.getOwnPropertySymbols,\n l = Object.getOwnPropertyDescriptor,\n f = Object.getPrototypeOf,\n v = Object.prototype;\n t.exports = function t(n, i, r) {\n if (\"string\" != typeof i) {\n if (v) {\n var e = f(i);\n e && e !== v && t(n, e, r);\n }\n var s = a(i);\n c && (s = s.concat(c(i)));\n for (var o = _(n), p = _(i), w = 0; w < s.length; ++w) {\n var d = s[w];\n if (!(u[d] || (r && r[d]) || (p && p[d]) || (o && o[d]))) {\n var g = l(i, d);\n try {\n h(n, d, g);\n } catch (t) {}\n }\n }\n }\n return n;\n };\n};\n","module_00905.js":"// ============================================================\n// Module ID: 905\n// ============================================================\n;(window.__modules__ = window.__modules__ || {})[905] = (t) => {\n \"use strict\";\n t.exports = \"SECRET_DO_NOT_PASS_THIS_OR_YOU_WILL_BE_FIRED\";\n};\n","module_01510.js":"// ============================================================\n// Module ID: 1510\n// ============================================================\n;(window.__modules__ = window.__modules__ || {})[1510] = (t, n, i) => {\n !(function (t, n, i, r, e) {\n \"use strict\";\n var u,\n s,\n o,\n _,\n h,\n a,\n c,\n l,\n f,\n v,\n p,\n w,\n d,\n g,\n m,\n k,\n z = n.$_$.ob,\n b = n.$_$.w4,\n q = i.$_$.e,\n y = n.$_$.he,\n $ = n.$_$.xj,\n x = n.$_$.h5,\n j = n.$_$.o1,\n S = n.$_$.gd,\n C = i.$_$.d,\n E = n.$_$.r5,\n T = n.$_$.ud,\n I = i.$_$.f,\n A = n.$_$.nf,\n R = r.$_$.s1,\n P = e.$_$.n,\n N = n.$_$.rf,\n M = n.$_$.dd,\n O = n.$_$.sj,\n D = i.$_$.r,\n L = i.$_$.b,\n B = e.$_$.f,\n U = n.$_$.yb,\n F = n.$_$.c,\n G = e.$_$.f1,\n H = n.$_$.ge,\n W = e.$_$.l,\n V = n.$_$.nj,\n K = n.$_$.ke;\n function Y(t) {\n var n = t.zuy_1;\n if (null != n) return n;\n z(\"codec_appId\");\n }\n function X() {}\n function J() {}\n function Z(t) {\n var n = t.cuz_1;\n if (null != n) return n;\n z(\"codec_questToAchievementMap\");\n }\n function Q() {}\n function tt(t) {\n var n = t.fuz_1;\n if (null != n) return n;\n z(\"codec_itemIdMap\");\n }\n function nt() {}\n function it() {\n ((this.iuz_1 = new R(A(I), null)), (this.juz_1 = new R(A(P), null)));\n }\n function rt() {\n (B.call(this),\n (this.nuz_1 = new R(A(I), null)),\n (this.ouz_1 = new R(A(P), null)),\n (this.quz_1 = new O(-621796162, 1132145095)),\n (this.ruz_1 = new O(1635469714, 1785333818)),\n (this.suz_1 = new O(-773564903, 1989636776)),\n (this.tuz_1 = new O(497901439, 366177258)),\n this.tay(),\n (this.uuz_1 = this.quz_1));\n }\n function et(t) {\n var n = N;\n return t.xuz_1.e2b(\n t,\n M(\n \"protocol\",\n 1,\n n,\n function (t) {\n return et(t);\n },\n null,\n ),\n );\n }\n function ut() {\n ((this.xuz_1 = new R(A(I), null)), (this.zuz_1 = new O(-1272608814, 1097348888)));\n }\n function st() {\n return ((t = H(y(ot))), ot.call(t), t);\n var t;\n }\n function ot() {}\n function _t(t) {\n var n = N;\n return t.fv0_1.e2b(\n t,\n M(\n \"protocol\",\n 1,\n n,\n function (t) {\n return _t(t);\n },\n null,\n ),\n );\n }\n function ht(t) {\n var n = N;\n return t.gv0_1.e2b(\n t,\n M(\n \"modelRegistry\",\n Line truncated
@@ -413,22 +413,23 @@
null == n ? t.f7h_1.b7h(!0) : (t.f7h_1.b7h(!1), this.r7h_1.s7h(t, n));
}),
(z(Ht).t7h = function (t) {
// [Hornet 调试] 可选读器打点(全量 opt,按帧分组):记录
// rid = 位读取器实例 id(一帧内所有记录共享同一个位读取器 → 同一 rid 即同一帧)
// pos = 读位前的绝对位索引;bit = 读到的位
// slots = 内层读器的槽位名(模型字段 → 槽位名全局唯一,可离线反查是哪个模型)
// 目的:直接看客户端对「某个模型」到底消费了几个 opt 位,从而定位我们少算的那一位。
// 位只读一次;打点异常不影响解码。
// [Hornet 调试] 可选读器全量打点:**每一次** opt 读位都记一条(不再按槽位过滤)。
// pos = 读位前的绝对位索引;bit = 读到的位;off = 读位时的字节游标(g2t)
// keys = 内层读器自有槽位(模型/集合可据此反查);src = 内层 t7h 源码前 72 字符
// elemKeys / elemSrc = 内层的列表元素读器(opt<list<…> 用)
// 为什么要 src:opt<基础类型>(除 string 外)和 opt<无槽位模型> 的内层读器没有任何自有属性,
// 旧版按「槽位非空」过滤会把它们全部丢掉,导致打点里只剩 opt<string>。
var Inner = this.r7h_1;
var BR = t && t.f7h_1;
if (BR && typeof BR.xy === "function" && BR.y7g_1 !== undefined) {
var B0 = BR.xy();
try {
var B0 = false;
try {
if (BR && typeof BR.xy === "function" && BR.y7g_1 !== undefined) {
B0 = BR.xy();
if (typeof window !== "undefined") {
var T0 = (window.__HornetOptTrace__ = window.__HornetOptTrace__ || {
rows: [], timer: 0, nextRid: 1,
rows: [], timer: 0,
push: function (Row) {
if (this.rows.length >= 8000) return;
if (this.rows.length >= 60000) return;
this.rows.push(Row);
if (!this.timer) {
var Me = this;
@@ -444,31 +445,30 @@
}
},
});
var Slots = [];
var Keys = [];
var Src = "";
var ElemKeys = [];
var ElemSrc = "";
var Off = -1;
try { Keys = Inner ? Object.keys(Inner) : []; } catch (E5) {}
try { Src = Inner && Inner.t7h ? String(Inner.t7h).slice(0, 72) : ""; } catch (E6) {}
try {
Slots = Inner ? Object.keys(Inner) : [];
ElemKeys = Inner && Inner.z7h_1 ? Object.keys(Inner.z7h_1) : [];
} catch (E5) {}
// 只记「能唯一定位」的:模型槽位 或 列表元素槽位;基础类型 opt 太碎且无指纹,跳过
if (Slots.length > 0 || ElemKeys.length > 0) {
if (!BR.__hornetRid) {
BR.__hornetRid = T0.nextRid || 1;
T0.nextRid = BR.__hornetRid + 1;
var Elem = Inner && Inner.z7h_1;
if (Elem) {
ElemKeys = Object.keys(Elem);
ElemSrc = Elem.t7h ? String(Elem.t7h).slice(0, 72) : "";
}
T0.push({
rid: BR.__hornetRid,
pos: BR.y7g_1 - 1,
bit: B0,
slots: Slots,
elemKeys: ElemKeys,
});
}
} catch (E7) {}
try {
var S = t.e7h_1;
Off = S && typeof S.g2t === "function" ? S.g2t() : -1;
} catch (E8) {}
T0.push({ pos: BR.y7g_1 - 1, bit: !!B0, off: Off, keys: Keys, src: Src, elemKeys: ElemKeys, elemSrc: ElemSrc });
}
} catch (E4) {}
return B0 ? null : Inner.t7h(t);
}
return t.f7h_1.xy() ? null : this.r7h_1.t7h(t);
}
} catch (E4) {}
if (!BR || typeof BR.xy !== "function" || BR.y7g_1 === undefined) return t.f7h_1.xy() ? null : this.r7h_1.t7h(t);
return B0 ? null : Inner.t7h(t);
}),
(z(Ht).u7h = function (t) {
this.r7h_1.u7h(t);
@@ -485,12 +485,79 @@
}),
(x(lt).s7h = function (t, n) {}),
(x(lt).t7h = function (t) {
// [Hornet 调试] pt 项打点:记录每一项的 modelId 与「字节/位」区间。
// b0/b1 = 本项负载起止字节游标(g2t);p0/p1 = 本项消费的绝对位区间(y7g_1)
// own = id 对象(Kotlin Long)的自有属性,离线可还原 modelId
// 目的:直接看客户端对「每一项」读了多少 opt 位,定位我们字段表少算的那几位。
var H0 = -1;
var HBit0 = -1;
var HNm = -1;
var HBuf = -1;
try {
H0 = t && t.e7h_1 && typeof t.e7h_1.g2t === "function" ? t.e7h_1.g2t() : -1;
HBit0 = t && t.f7h_1 ? t.f7h_1.y7g_1 : -1;
// nm = 当前位流总位数;buf = 位缓冲数组的身份 id(换流就换 id)——
// 用来区分「同一条位流里的下一个记录」与「嵌套消息新开的一条位流」
var HBr = t && t.f7h_1;
if (HBr) {
HNm = typeof HBr.w7g_1 === "number" ? HBr.w7g_1 : -1;
if (HBr.x7g_1 && typeof window !== "undefined") {
var Hw = (window.__HornetBitBufs__ = window.__HornetBitBufs__ || { map: new WeakMap(), next: 1 });
HBuf = Hw.map.get(HBr.x7g_1);
if (HBuf === undefined) {
HBuf = Hw.next++;
Hw.map.set(HBr.x7g_1, HBuf);
}
}
}
} catch (Eh) {}
var n = ct(this).t7h(t),
i = n instanceof w ? n : p();
if (d(i, new w(0, 0))) return I(T(ct(this).t7h(t)), i);
var r = at().a1x2().o7o(i),
e = T(r.t7h(t));
return I(e, i);
var HRet;
if (d(i, new w(0, 0))) HRet = I(T(ct(this).t7h(t)), i);
else {
var r = at().a1x2().o7o(i),
e = T(r.t7h(t));
HRet = I(e, i);
}
try {
if (typeof window !== "undefined") {
var T0 = (window.__HornetItemTrace__ = window.__HornetItemTrace__ || {
rows: [], timer: 0,
push: function (Row) {
if (this.rows.length >= 40000) return;
this.rows.push(Row);
if (!this.timer) {
var Me = this;
this.timer = setTimeout(function () {
Me.timer = 0;
try {
fetch("/__dump__/item_trace.json", {
method: "POST",
body: JSON.stringify({ at: new Date().toISOString(), count: Me.rows.length, trace: Me.rows }),
});
} catch (E2) {}
}, 1500);
}
},
});
var Own = "";
try {
var Nk = Object.keys(i);
for (var Ik = 0; Ik < Nk.length; Ik++) Own += Nk[Ik] + "=" + i[Nk[Ik]] + ";";
} catch (Eh2) {}
T0.push({
own: Own,
b0: H0,
b1: t && t.e7h_1 && typeof t.e7h_1.g2t === "function" ? t.e7h_1.g2t() : -1,
p0: HBit0,
p1: t && t.f7h_1 ? t.f7h_1.y7g_1 : -1,
nm: HNm,
buf: HBuf,
});
}
} catch (Eh3) {}
return HRet;
}),
(x(vt).e1x2 = function () {
var t = a;
@@ -23566,20 +23566,71 @@
(this.gl5_1 = t.r7g(new ti(Dn(ei), !1))));
}),
(Bn(pq).t7h = function (t) {
// [Hornet 调试] TankCC 逐字段打点:tag / 字节区间 / 位区间。
// 目的:实测每个字段宽度,定位 tankState(opt<TankState>)在线上到底占几字节。
var HB = function () { try { return t.e7h_1 && t.e7h_1.g2t ? t.e7h_1.g2t() : -1; } catch (EB) { return -1; } };
var HP = function () { try { return t.f7h_1 ? t.f7h_1.y7g_1 : -1; } catch (EP) { return -1; } };
var HF = function (Tag, B0, P0) {
try {
if (typeof window === "undefined") return;
var T0 = (window.__HornetFieldTrace__ = window.__HornetFieldTrace__ || {
rows: [], timer: 0,
push: function (Row) {
if (this.rows.length >= 60000) return;
this.rows.push(Row);
if (!this.timer) {
var Me = this;
this.timer = setTimeout(function () {
Me.timer = 0;
try {
fetch("/__dump__/field_trace.json", {
method: "POST",
body: JSON.stringify({ at: new Date().toISOString(), count: Me.rows.length, trace: Me.rows }),
});
} catch (E2) {}
}, 1500);
}
},
});
T0.push({ model: "TankCC", tag: Tag, b0: B0, b1: HB(), p0: P0, p1: HP() });
} catch (E3) {}
};
var B0 = HB(), P0 = HP();
var n = xW();
n.hl5_1 = t.e7h_1.q2u();
HF("i16", B0, P0);
B0 = HB(); P0 = HP();
var i = n,
r = cq(this).t7h(t);
i.il5_1 = null != r && "boolean" == typeof r ? r : Un();
HF("local", B0, P0);
B0 = HB(); P0 = HP();
var e = n,
u = lq(this).t7h(t);
((e.jl5_1 = u instanceof CW ? u : Un()), (n.kl5_1 = t.e7h_1.r2u()), (n.ll5_1 = t.e7h_1.r2u()));
HF("logicState+i32a+i32b", B0, P0);
B0 = HB(); P0 = HP();
var s = n,
o = fq(this).t7h(t);
s.ml5_1 = null == o || o instanceof GV ? o : Un();
// 顺带把 tankState 的**内层读器身份**打出来:构造名 / 自有槽位 / t7h 源码头部。
// 目的:确认 KClass(GV) 在运行期到底解析成哪个 codec(jq 的钩子一条都没触发)。
var HInnerInfo = "?";
try {
var HW = fq(this);
var HI = HW && HW.r7h_1;
if (HI) {
var HK = "";
try { HK = Object.keys(HI).join(","); } catch (EK) {}
HInnerInfo = (HI.constructor && HI.constructor.name ? HI.constructor.name : "?") +
"|keys=" + HK + "|src=" + (HI.t7h ? String(HI.t7h).replace(/\s+/g, " ").slice(0, 110) : "");
}
} catch (EI) {}
HF("tankState|" + HInnerInfo, B0, P0);
B0 = HB(); P0 = HP();
var _ = n,
h = vq(this).t7h(t);
return ((_.nl5_1 = h instanceof ei ? h : Un()), n);
return (HF("team", B0, P0), (_.nl5_1 = h instanceof ei ? h : Un()), n);
}),
(Bn(pq).s7h = function (t, n) {
if (null == n) throw Gn("Object is null. Use @ProtocolOptional annotation.");
@@ -23770,19 +23821,55 @@
(this.zl5_1 = t.r7g(new Ln(Dn(ui), !1))));
}),
(Bn(jq).t7h = function (t) {
// [Hornet 调试] TankState 逐字段打点:字段名 / 字节区间 / 位区间。
// 目的:`ui` 的静态身份是 85196 枚举成员访问器,宽度不明;这里直接量出 4 个字段各占几字节。
var HB = function () { try { return t.e7h_1 && t.e7h_1.g2t ? t.e7h_1.g2t() : -1; } catch (EB) { return -1; } };
var HP = function () { try { return t.f7h_1 ? t.f7h_1.y7g_1 : -1; } catch (EP) { return -1; } };
var HF = function (Tag, B0, P0) {
try {
if (typeof window === "undefined") return;
var T0 = (window.__HornetFieldTrace__ = window.__HornetFieldTrace__ || {
rows: [], timer: 0,
push: function (Row) {
if (this.rows.length >= 60000) return;
this.rows.push(Row);
if (!this.timer) {
var Me = this;
this.timer = setTimeout(function () {
Me.timer = 0;
try {
fetch("/__dump__/field_trace.json", {
method: "POST",
body: JSON.stringify({ at: new Date().toISOString(), count: Me.rows.length, trace: Me.rows }),
});
} catch (E2) {}
}, 1500);
}
},
});
T0.push({ model: "TankState", tag: Tag, b0: B0, b1: HB(), p0: P0, p1: HP() });
} catch (E3) {}
};
var B0 = HB(), P0 = HP();
var n = FV(),
i = n,
r = qq(this).t7h(t);
i.al6_1 = r instanceof ui ? r : Un();
HF("angularVelocity", B0, P0);
B0 = HB(); P0 = HP();
var e = n,
u = yq(this).t7h(t);
e.bl6_1 = u instanceof ui ? u : Un();
HF("linearVelocity", B0, P0);
B0 = HB(); P0 = HP();
var s = n,
o = $q(this).t7h(t);
s.cl6_1 = o instanceof ui ? o : Un();
HF("orientation", B0, P0);
B0 = HB(); P0 = HP();
var _ = n,
h = xq(this).t7h(t);
return ((_.dl6_1 = h instanceof ui ? h : Un()), n);
return (HF("position", B0, P0), (_.dl6_1 = h instanceof ui ? h : Un()), n);
}),
(Bn(jq).s7h = function (t, n) {
if (null == n) throw Gn("Object is null. Use @ProtocolOptional annotation.");
@@ -7246,7 +7246,7 @@ data class TankCC(
val LogicState: Int,
val Field4: Int,
val Field5: Int,
val TankState: TankState?,
val TankState: TankStateM87072T5?,
val Team: Int,
)
@@ -7257,7 +7257,7 @@ object TankCCCodec : ModelCodec<TankCC> {
val LogicState = R.ReadInt()
val Field4 = R.ReadInt()
val Field5 = R.ReadInt()
val TankState = Wire.ReadOpt(R, N) { TankStateCodec.Decode(R, N) }
val TankState = Wire.ReadOpt(R, N) { TankStateM87072T5Codec.Decode(R, N) }
val Team = R.ReadInt()
return TankCC(Field1, Local, LogicState, Field4, Field5, TankState, Team)
}
@@ -7268,7 +7268,7 @@ object TankCCCodec : ModelCodec<TankCC> {
W.WriteInt(V.LogicState)
W.WriteInt(V.Field4)
W.WriteInt(V.Field5)
Wire.WriteOpt(N, V.TankState) { TankStateCodec.Encode(W, N, it) }
Wire.WriteOpt(N, V.TankState) { TankStateM87072T5Codec.Encode(W, N, it) }
W.WriteInt(V.Team)
}
}
@@ -17,13 +17,17 @@ import Net.Protocol.ZtCodec
import Net.Protocol.AliasCodec
// TankState|module_87072
class TankStateM87072T5
data class TankStateM87072T5(
val Packed: ByteArray,
)
object TankStateM87072T5Codec : ModelCodec<TankStateM87072T5> {
override fun Decode(R: ByteReader, N: NullmapCursor): TankStateM87072T5 {
return TankStateM87072T5()
val Packed = R.ReadRaw(23)
return TankStateM87072T5(Packed)
}
override fun Encode(W: ByteWriter, N: NullmapWriter, V: TankStateM87072T5) {
W.WriteRaw(V.Packed)
}
}
+12
View File
@@ -129,6 +129,12 @@ class Resolver:
if S.startswith("map<") and S.endswith(">"):
K, V = SplitTopLevel(S[4:-1])
return ("map", self.Parse(K, TypeCodec), self.Parse(V, TypeCodec))
# 定长二进制块 bytes<N>:出处 module_87072.js:53876 的 T5.t7h ——
# TankState 在线上是 23 字节打包姿态(内部按 19/13/13/13 位量化 4 个向量),
# 我们只按定长吃掉、原样写回,不解释内容。
Fixed = re.fullmatch(r"bytes(\d+)", S)
if Fixed:
return ("bytes", int(Fixed.group(1)))
if S in PRIMS:
return (S,)
if TypeCodec and TypeCodec in self.CodecKeyToName:
@@ -148,6 +154,8 @@ def KotlinType(Node) -> str:
return f"Map<{KotlinType(Node[1])}, {KotlinType(Node[2])}>"
if Kind == "enum":
return "Int"
if Kind == "bytes":
return "ByteArray"
if Kind == "model":
return Node[1]
return PRIMS[Kind][0]
@@ -168,6 +176,8 @@ def DecodeExpr(Node) -> str:
# · 实测 B:Item3DCC 之外有 21 个模型在 enum=0 时整体错位(P_ 的 category 必须 4 字节)。
# 全量审计:enum=4 → 6 处未命中 / 127 项;enum=0 → 21 处。先保留 4,等客户端位痕迹定案。
return "R.ReadInt()"
if Kind == "bytes":
return f"R.ReadRaw({Node[1]})"
if Kind == "model":
return Node[1] + "Codec.Decode(R, N)"
return PRIMS[Kind][1]
@@ -184,6 +194,8 @@ def EncodeExpr(Node, Value: str) -> str:
EncodeExpr(Node[2], "it") + " })")
if Kind == "enum":
return "W.WriteInt(" + Value + ")"
if Kind == "bytes":
return "W.WriteRaw(" + Value + ")"
if Kind == "model":
return Node[1] + "Codec.Encode(W, N, " + Value + ")"
return PRIMS[Kind][2].replace("{}", Value)