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:
1 parent
2ce2fd0363
commit
b083d27165
16 files changed
+773
-60
No files matched your search
@@ -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",
|
||||
|
||||
@@ -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(', '));
|
||||
@@ -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);
|
||||
}
|
||||
@@ -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;
|
||||
|
||||
@@ -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, ' ')));
|
||||
}
|
||||
}
|
||||
@@ -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 };
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
@@ -342,6 +342,8 @@ function Load(NamesDir) {
|
||||
}
|
||||
Off += 16 + R.Consumed;
|
||||
}
|
||||
// 诊断用:本帧消费到的位索引(nullmap 容量自检见 tools/_nullmap_capacity.js)
|
||||
Out.Bits = Ctx.Index;
|
||||
return Out;
|
||||
}
|
||||
|
||||
|
||||
@@ -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 @@
|
||||
{"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)
|
||||
}
|
||||
}
|
||||
@@ -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)
|
||||
|
||||
Reference in new issue
Block a user