- extract_codec_fields:字段工厂 n7f→r7g、读写方法 p7g/o7g→t7h/s7h、槽位初始化 q7g→u7h - 资源类注册 u83→y84,核心类型按 模块::导出键 解析(Vector3/Long/实体引用) - mine_downlink:读取器映射按当前 ByteBuffer 修正(t2u=f32、u2u=f64 等),容器槽位泛化 - 重生成 codec_fields/uplink_table/downlink_table:762 下行字段 0 未解,361 上行命令全量有类型
499 lines
28 KiB
JavaScript
499 lines
28 KiB
JavaScript
// mine_downlink.js - 挖「下行(s2c)命令字段表」:methodId → 有序字段
|
||
// 用法:
|
||
// node mine_downlink.js [--src ../src] [--out ../names/downlink_table.json] [--check]
|
||
//
|
||
// ⚠ 必须跑在 **../src**(原始短名)而不是 ../src_named:
|
||
// names/*.json 各表(codec_fields / method_table / uplink_table)的键都是原始短名(如 hh4_1),
|
||
// 而 src_named/ 里 apply_names 会把这些槽位改成别名(hh4_1 → remember2FaHash_codecField),
|
||
// 于是「源码里的槽位名」和「表里的键」对不上,字段会整片挖空。
|
||
//
|
||
// 为什么需要它:上行命令有 names/uplink_table.json(methodId → 命令类 / 发送方法 / 负载字段),
|
||
// 下行只有 names/method_table.json(methodId → 模型 + 模型字段全集),**没有"这条方法实际带哪些字段"**。
|
||
// 于是下发的帧只能当字节回放,想改一个字段(比如用户名)就得做字节替换。
|
||
// 本工具把"每条 s2c 方法的有序字段"挖出来,server/semantic.js 就能据此把帧解成 JSON、再编码回字节。
|
||
//
|
||
// 依据全部来自 src/ 源码,不猜:
|
||
// ① 模型类的 q7i(t, n):t = 收到的 methodId,n = 读游标。
|
||
// `X(t, this.<slot>)` 是分支判定,<slot> 的 Long 值就是该分支的 methodId。
|
||
// (判定函数名逐模块不同:M / tu / Ae / …,所以只按「两个参数恰好是 t 与 this.<slot>」的形状匹配。)
|
||
// ② 分支里按线上次序读字段,两种形态:
|
||
// a) 嵌套 codec:`(function (t) { var x = t.<codecSlot>; … })(this).p7g(n)`
|
||
// 类型按两级优先级解(见 ResolveField):
|
||
// ① codec_fields.json 的 Codecs[*].Fields[].Slot(带 Name,因为那些 codec 有 codec_<名字> getter)
|
||
// ② codec_fields.json 的 SlotTypes["<模块>::<槽位>"](全模块扫 `<槽位> = <工厂>(this).n7f(new <描述符>)`
|
||
// 得来,只有 Type/TypeCodec;名称再从分支里的报错串 f("_方法_字段Codec") 取)
|
||
// 两级都查不到 / 查到的类型读取器没登记 → Type = '<无 codec>',并入该方法的 Unresolved。
|
||
// b) 基础类型:`n.a7g_1.<reader>()`(reader 名 → 类型见 ReaderType)
|
||
// ③ 容器型负载(_loadDependencies / _loadObjectsData / _unloadObjects / _setModelCommandAlias)
|
||
// 不走 q7i,由 container.js 的三种布局负责;这里把它们的 Kind 标成 container。
|
||
//
|
||
// ⚠ 帧结构(src/module_61860.js:2111-2131 的空间命令解析 u7m,据实修正旧注释):
|
||
// 帧体 = [nullmap][记录…],nullmap 是「一条记录 1 bit」的位图(MSB 在前,见 module_24635.js:357-364);
|
||
// 记录 = bit=1 → 1 字节短命令(查 Gr(space) 表) | bit=0 → [objectId i64][methodId i64][负载]。
|
||
// 也就是说**每条记录自带 objectId**(单记录帧里它就等于空间 id),不是「帧头一个 spaceId」。
|
||
// 旧版 extract_space_init.js 把"第一条记录的 objectId"当成整帧的 spaceId、并在解不动时把**帧体剩余字节**
|
||
// 整段存成一个记录的 Hex(见该文件 84-100 行),于是多记录帧的 Hex 里会带着下一条记录的头。
|
||
// --check 因此按「记录链」解:读完本条字段后若还有剩余,就再读 [objectId][methodId] 接着解。
|
||
'use strict';
|
||
const fs = require('fs');
|
||
const path = require('path');
|
||
|
||
const ToolsDir = __dirname;
|
||
const Argv = process.argv.slice(2);
|
||
const Arg = (Name, Def) => {
|
||
const I = Argv.indexOf('--' + Name);
|
||
return I >= 0 && Argv[I + 1] ? Argv[I + 1] : Def;
|
||
};
|
||
const SrcDir = path.resolve(ToolsDir, Arg('src', path.join('..', 'src')));
|
||
const NamesDir = path.resolve(ToolsDir, '..', 'names');
|
||
const OutPath = path.resolve(ToolsDir, Arg('out', path.join('..', 'names', 'downlink_table.json')));
|
||
const Check = Argv.indexOf('--check') >= 0;
|
||
|
||
// ByteBuffer 的定长读取器:名字来自 src/module_72834.js(p2u=xy=getInt8 … t2u=getFloat32、u2u=getFloat64)
|
||
const ReaderType = { xy: 'i8', p2u: 'i8', q2u: 'i16', r2u: 'i32', s2u: 'i64', t2u: 'f32', u2u: 'f64' };
|
||
|
||
// ---------------------------------------------------------------- 载入 codec 字段表
|
||
const CodecFields = JSON.parse(fs.readFileSync(path.join(NamesDir, 'codec_fields.json'), 'utf8'));
|
||
const SlotField = new Map(); // slot → { Codec: 'module_x::Y', Name, Type, TypeCodec, Order }
|
||
const SlotConflicts = [];
|
||
// 新增的顶层段 SlotTypes(extract_codec_fields.js 产出):'<模块>::<槽位>' → { Module, Slot, Type, TypeCodec, Desc }
|
||
// 为什么要它:q7i 读的槽位是模型类里的**局部 codec 实例**(chc_1 / jh4_1 / b1wd_1 …),
|
||
// 它们不是任何 codec 条目的字段,所以旧版三级查表在 codec_fields 里压根查不到,只能报未解。
|
||
const SlotTypes = CodecFields.SlotTypes || {};
|
||
// 字段名 → 类型:只作最末一级兜底(槽位没进 codec_fields / SlotTypes 时,用 f("_方法_字段Codec") 解出的字段名反查)
|
||
const NameType = new Map();
|
||
const NameAmbiguous = new Set();
|
||
const ModelNames = new Map(); // 模型真名 → 该真名对应的 codec 个数(>1 说明裸名定位不到唯一 codec,不能拿来解)
|
||
|
||
for (const [Key, C] of Object.entries(CodecFields.Codecs)) {
|
||
if (C.ModelName) ModelNames.set(C.ModelName, (ModelNames.get(C.ModelName) || 0) + 1);
|
||
for (const F of C.Fields || []) {
|
||
if (!F.Slot) continue;
|
||
const Rec = { Codec: Key, Name: F.Name || '', Type: F.Type || '', TypeCodec: F.TypeCodec || '', Order: F.Order || 0 };
|
||
const Prev = SlotField.get(F.Slot);
|
||
// 同名 slot 落在不同 codec 上 → 只记第一次,并把冲突报出来
|
||
if (Prev && Prev.Codec !== Key) {
|
||
SlotConflicts.push(F.Slot + ' → ' + Prev.Codec + ' / ' + Key);
|
||
continue;
|
||
}
|
||
SlotField.set(F.Slot, Rec);
|
||
// 同名字段在不同 codec 上类型不一致 → 这一级兜底作废,免得拿错类型去解字节
|
||
if (Rec.Name && Rec.Name !== '?' && Rec.Name !== '(匿名)') {
|
||
const Seen = NameType.get(Rec.Name);
|
||
if (!Seen) NameType.set(Rec.Name, { Type: Rec.Type, TypeCodec: Rec.TypeCodec });
|
||
else if (Seen.Type !== Rec.Type || Seen.TypeCodec !== Rec.TypeCodec) NameAmbiguous.add(Rec.Name);
|
||
}
|
||
}
|
||
}
|
||
for (const N of NameAmbiguous) NameType.delete(N);
|
||
|
||
// ---------------------------------------------------------------- 类型能不能被 lib_space_codec 解
|
||
// 读取器实际登记的类型(tools/lib_space_codec.js:120-171):
|
||
// 定长基础类型 bool/i8/i16/i32/i64/f32/f64、string/bytes/array、opt<T>/list<T>、enum<T>(固定 4 字节)、
|
||
// 以及「模型类」——但模型类只有在能定位到 codec 时才解得出(TypeCodec 链接,或该真名全局唯一)。
|
||
// 不在这个集合里的(any/number/nothing/throwable/xxArray/prim:xx/认不出的裸类名)一律算没解出来,
|
||
// 免得把解不动的类型写进表里、到 semantic.js 那一步才炸。
|
||
const ReaderLeaf = new Set(['bool', 'i8', 'i16', 'i32', 'i64', 'f32', 'f64', 'string', 'bytes', 'array', 'vector3']);
|
||
function Readable(Type, TypeCodec) {
|
||
const T = String(Type || '');
|
||
if (!T || T === '?' || T === '...' || T[0] === '<') return false;
|
||
const Opt = /^opt<([\s\S]+)>$/.exec(T);
|
||
if (Opt) return Readable(Opt[1], TypeCodec);
|
||
const List = /^list<([\s\S]+)>$/.exec(T);
|
||
if (List) return Readable(List[1], TypeCodec);
|
||
if (/^enum<[\s\S]+>$/.test(T)) return true; // 枚举固定 4 字节,不需要嵌套 codec
|
||
if (ReaderLeaf.has(T)) return true;
|
||
if (/^prim:/.test(T)) return false;
|
||
if (TypeCodec) return true;
|
||
// 裸模型真名:只有全局唯一时才敢当 codec 用(重名会解成别的模型的数据)
|
||
return ModelNames.get(T) === 1;
|
||
}
|
||
|
||
// f("_<方法名>_<字段名>Codec") → 字段名(惰性 getter 的报错串,形如 module_10283.js:2353 的 "_remember2FaHash_hashCodec")
|
||
function FieldNameOfTag(Tag) {
|
||
const S = String(Tag || '');
|
||
const M = /^_?[A-Za-z0-9_$]*?_([A-Za-z][A-Za-z0-9]*)Codec$/.exec(S);
|
||
if (M) return M[1];
|
||
const N = /^_?([A-Za-z][A-Za-z0-9]*)Codec$/.exec(S);
|
||
return N ? N[1] : '';
|
||
}
|
||
|
||
// ---------------------------------------------------------------- 文本扫描小工具
|
||
// 从 `{` 位置配平花括号,跳过字符串与注释(源码是 prettier 输出,但仍要防字符串里有花括号)
|
||
function MatchBrace(Text, Open) {
|
||
let Depth = 0;
|
||
let I = Open;
|
||
while (I < Text.length) {
|
||
const Ch = Text[I];
|
||
if (Ch === '"' || Ch === "'" || Ch === '`') {
|
||
const Q = Ch;
|
||
I++;
|
||
while (I < Text.length && Text[I] !== Q) {
|
||
if (Text[I] === '\\') I++;
|
||
I++;
|
||
}
|
||
I++;
|
||
continue;
|
||
}
|
||
if (Ch === '/' && Text[I + 1] === '/') {
|
||
while (I < Text.length && Text[I] !== '\n') I++;
|
||
continue;
|
||
}
|
||
if (Ch === '/' && Text[I + 1] === '*') {
|
||
I += 2;
|
||
while (I < Text.length && !(Text[I] === '*' && Text[I + 1] === '/')) I++;
|
||
I += 2;
|
||
continue;
|
||
}
|
||
if (Ch === '{') Depth++;
|
||
else if (Ch === '}') {
|
||
Depth--;
|
||
if (Depth === 0) return I;
|
||
}
|
||
I++;
|
||
}
|
||
return -1;
|
||
}
|
||
|
||
// 模块内 `this.<slot> = new <Cls>(-?\d+, -?\d+)` → slot → methodId(十进制字符串)
|
||
// Long 合成:value = hi*2^32 + (lo>>>0);Long 类名逐模块不同(R / Me / xi / $ / …),所以不限定类名。
|
||
const LongOf = (Lo, Hi) => (BigInt(Hi) * 4294967296n + BigInt(Lo >>> 0)).toString();
|
||
function FindLongs(Text) {
|
||
const Out = new Map();
|
||
const Re = /this\.([a-z0-9]+_\d+) = new ([A-Za-z_$][\w$]*)\((-?\d+), (-?\d+)\)/g;
|
||
let M;
|
||
while ((M = Re.exec(Text))) Out.set(M[1], { Value: LongOf(Number(M[3]), Number(M[4])), Lo: Number(M[3]), Hi: Number(M[4]) });
|
||
return Out;
|
||
}
|
||
|
||
// 分支里读字段:返回按出现位置排序的 [{At, Slot|Reader, Type, ...}]
|
||
function ReadOrder(Body) {
|
||
const Hits = [];
|
||
// 形态:`var x = (function (t) {\n var n = t.<codecSlot>;\n … f("_<方法名>_<字段名>Codec");\n })(this).p7g(n);`
|
||
// prettier 会在 `{` 后换行,所以必须用 \s* 而不是单个空格。
|
||
// 顺带把 f("…") 的报错串抓出来 —— 里面就写着字段名(见 FieldNameOfTag)。
|
||
const ReCodec =
|
||
/\(function \(t\) \{\s*var ([A-Za-z0-9_$]+) = t\.([a-z0-9]+_\d+);\s*if \(null != \1\) return \1;\s*[A-Za-z_$][\w$]*\(\s*"([^"]*)"\s*\);\s*\}\)\(this\)/g;
|
||
let M;
|
||
while ((M = ReCodec.exec(Body))) Hits.push({ At: M.index, Slot: M[2], Tag: M[3] });
|
||
// 基础类型:旧包固定缓冲槽位 a7g_1,新包槽位名会变,故泛化
|
||
const RePrim = /\.([a-z0-9]+_\d+)\.([A-Za-z0-9_$]+)\(\)/g;
|
||
while ((M = RePrim.exec(Body))) Hits.push({ At: M.index, Reader: M[2] });
|
||
Hits.sort((A, B) => A.At - B.At);
|
||
return Hits;
|
||
}
|
||
|
||
// ---------------------------------------------------------------- 容器型方法(不走 q7i)
|
||
// 声明得比挖掘循环早,是因为挖出来的条目要靠它标 Kind(容器型不能当普通字段解,见 server/container.js)
|
||
const Containers = [
|
||
{ MethodId: '3216143066888387731', Name: 'ModelService.LoadDependencies', Layouts: ['zt'] },
|
||
{ MethodId: '7640916300855664666', Name: 'ModelService.LoadObjectsData', Layouts: ['pt'] },
|
||
{ MethodId: '9038756763225879447', Name: 'ModelService.UnloadObjects', Layouts: ['lt'] },
|
||
{ MethodId: '8179162314198150518', Name: 'ModelService.SetModelCommandAlias', Layouts: ['alias'] },
|
||
];
|
||
const ContainerIds = new Set(Containers.map((C) => C.MethodId));
|
||
|
||
// ---------------------------------------------------------------- 逐模块挖 q7i
|
||
const Files = fs.readdirSync(SrcDir).filter((F) => /^module_\d+\.js$/.test(F)).sort();
|
||
const Commands = [];
|
||
const Problems = [];
|
||
let HandlerCount = 0;
|
||
let BySlotField = 0; // 类型来自 codec_fields 的 Codecs[*].Fields[].Slot
|
||
let BySlotTypes = 0; // 类型来自 codec_fields 的 SlotTypes(全模块「槽位 → 类型」)
|
||
let ByName = 0; // 类型是从同名字段反查出来的字段数
|
||
|
||
for (const File of Files) {
|
||
const ModuleId = String(Number(/module_(\d+)\.js$/.exec(File)[1]));
|
||
const Mod = 'module_' + ModuleId;
|
||
const Text = fs.readFileSync(path.join(SrcDir, File), 'utf8');
|
||
const Longs = FindLongs(Text);
|
||
// 找每个 `.q7i = function (...) {` 的函数体。源码形态是 `(p(Pt).q7i = function (t, n) {` ——
|
||
// 类名在包装函数的括号里(包装函数逐模块不同:p / ke / ee / nt / …),所以捕获 `)<方法名>` 前那个标识符。
|
||
const ReHead = /([A-Za-z_$][\w$]*)\)\.u7j = function \(([^)]*)\) \{/g;
|
||
let H;
|
||
while ((H = ReHead.exec(Text))) {
|
||
const Owner = H[1];
|
||
const Open = Text.indexOf('{', H.index + H[0].length - 1);
|
||
const Close = MatchBrace(Text, Open);
|
||
if (Close < 0) {
|
||
Problems.push(Mod + ' ' + Owner + '.q7i 花括号配平失败');
|
||
continue;
|
||
}
|
||
HandlerCount++;
|
||
const Body = Text.slice(Open + 1, Close);
|
||
|
||
// 分支判定:<任意函数名>(t, this.<slot>) —— 判定函数逐模块不同
|
||
const ReBranch = /[A-Za-z_$][\w$]*\(t, this\.([a-z0-9]+_\d+)\)/g;
|
||
const Marks = [];
|
||
let B;
|
||
while ((B = ReBranch.exec(Body))) Marks.push({ At: B.index, End: ReBranch.lastIndex, Slot: B[1] });
|
||
if (!Marks.length) {
|
||
// 空实现 `function (t, n) {}` 是正常的(该模型没有 s2c),只有真的有方法体才算可疑
|
||
if (Body.trim()) Problems.push(Mod + ' ' + Owner + '.q7i 有方法体但找不到分支判定(可能是 switch/表驱动)');
|
||
continue;
|
||
}
|
||
|
||
for (let I = 0; I < Marks.length; I++) {
|
||
const Slot = Marks[I].Slot;
|
||
const Seg = Body.slice(Marks[I].End, I + 1 < Marks.length ? Marks[I + 1].At : Body.length);
|
||
const Long = Longs.get(Slot);
|
||
if (!Long) {
|
||
Problems.push(Mod + ' ' + Owner + '.q7i 分支 ' + Slot + ' 找不到对应的 methodId 常量');
|
||
continue;
|
||
}
|
||
const Fields = [];
|
||
let Unresolved = 0;
|
||
for (const Hit of ReadOrder(Seg)) {
|
||
if (Hit.Slot) {
|
||
// 槽位查表顺序(两级,都是源码证据,不是猜):
|
||
// ① codec_fields 的 Codecs[*].Fields[].Slot —— 那些 codec 有 codec_<名字> getter,所以带 Name
|
||
// ② codec_fields 的 SlotTypes["<模块>::<槽位>"] —— q7i 读的局部 codec 实例都在这里,只有 Type
|
||
// 字段名:先取 ① 的 Name,没有再取分支里 f("_<方法名>_<字段名>Codec") 的报错串
|
||
const F = SlotField.get(Hit.Slot);
|
||
const S = SlotTypes[Mod + '::' + Hit.Slot];
|
||
const Name = (F && F.Name) || FieldNameOfTag(Hit.Tag) || '';
|
||
let Type = '';
|
||
let TypeCodec = '';
|
||
let Codec = '';
|
||
let Src = '';
|
||
if (F && Readable(F.Type, F.TypeCodec)) {
|
||
Type = F.Type;
|
||
TypeCodec = F.TypeCodec;
|
||
Codec = F.Codec;
|
||
Src = 'codec_fields:' + F.Codec + ' 的槽位 ' + Hit.Slot;
|
||
BySlotField++;
|
||
} else if (S && Readable(S.Type, S.TypeCodec)) {
|
||
Type = S.Type;
|
||
TypeCodec = S.TypeCodec;
|
||
Src = S.Module + '.js:' + S.Line + ' ' + S.Desc;
|
||
BySlotTypes++;
|
||
} else if (Name && NameType.has(Name) && Readable(NameType.get(Name).Type, NameType.get(Name).TypeCodec)) {
|
||
// 末级兜底:拿 f("…") 里的字段名去 codec_fields 反查同名字段(同名同类型才用,见 NameAmbiguous)
|
||
const N = NameType.get(Name);
|
||
Type = N.Type;
|
||
TypeCodec = N.TypeCodec;
|
||
Src = 'codec_fields:同名字段 ' + Name;
|
||
ByName++;
|
||
}
|
||
if (!Type) {
|
||
// 查不到类型、或者读到的类型读取器没登记 → 如实标未解,并把现场(描述符/出处)留下,不要编一个类型
|
||
Type = '<无 codec>';
|
||
Unresolved++;
|
||
if (S) Src = 'SlotTypes ' + S.Module + '.js:' + S.Line + ' ' + S.Desc + '(类型推不出来)';
|
||
else if (!Src) Src = 'SlotTypes 无此槽位 ' + Mod + '::' + Hit.Slot;
|
||
}
|
||
const Rec = { Order: Fields.length + 1, Slot: Hit.Slot, Name, Type };
|
||
if (TypeCodec) Rec.TypeCodec = TypeCodec;
|
||
if (Codec) Rec.Codec = Codec;
|
||
if (Src) Rec.Src = Src;
|
||
Fields.push(Rec);
|
||
} else {
|
||
const T = ReaderType[Hit.Reader] || '<未知读取器:' + Hit.Reader + '>';
|
||
if (T[0] === '<') Unresolved++;
|
||
Fields.push({ Order: Fields.length + 1, Reader: Hit.Reader, Name: '', Type: T });
|
||
}
|
||
}
|
||
Commands.push({
|
||
MethodId: Long.Value,
|
||
Module: Mod,
|
||
OwnerClass: Owner,
|
||
LongLiteral: [Long.Lo, Long.Hi],
|
||
// 容器型负载的字节不是「按字段顺序排」,不能让 codec 去硬解(见 server/container.js)
|
||
Kind: ContainerIds.has(Long.Value) ? 'container' : 'fields',
|
||
Fields,
|
||
Unresolved,
|
||
});
|
||
}
|
||
}
|
||
}
|
||
|
||
// 同一 methodId 出现在多处(模型类重名/重复实现)→ 保留字段更多的那条,并记下分歧
|
||
const ByMethod = new Map();
|
||
const Duplicated = [];
|
||
for (const C of Commands) {
|
||
const Prev = ByMethod.get(C.MethodId);
|
||
if (!Prev) {
|
||
ByMethod.set(C.MethodId, C);
|
||
continue;
|
||
}
|
||
Duplicated.push(C.MethodId + ' ' + Prev.Module + ' ' + Prev.OwnerClass + '(' + Prev.Fields.length + ' 字段) 与 ' + C.Module + ' ' + C.OwnerClass + '(' + C.Fields.length + ' 字段)');
|
||
if (C.Fields.length > Prev.Fields.length) ByMethod.set(C.MethodId, C);
|
||
}
|
||
const Final = [...ByMethod.values()].sort((A, B) => (BigInt(A.MethodId) < BigInt(B.MethodId) ? -1 : 1));
|
||
|
||
// 覆盖统计:和 method_table.json 对一下
|
||
const MethodTable = JSON.parse(fs.readFileSync(path.join(NamesDir, 'method_table.json'), 'utf8'));
|
||
const AllMethods = (MethodTable.Methods || MethodTable.Rows || []).map((R) => R.MethodId);
|
||
const MinedSet = new Set(Final.map((C) => C.MethodId));
|
||
const Covered = AllMethods.filter((M) => MinedSet.has(M));
|
||
|
||
const AllFields = Final.flatMap((C) => C.Fields);
|
||
|
||
const Note = [
|
||
'下行(s2c)命令字段表:methodId → 有序字段。由 tools/mine_downlink.js 从 src/ 挖出,不猜。',
|
||
'字段来源两类:Slot(源码里的 codec 槽位)/ Reader(ByteBuffer 定长读取器,Type 已换算成 i8..f64)。',
|
||
'Slot 的类型按两级优先级解:① codec_fields.json 的 Codecs[*].Fields[].Slot(带 Codec / Name / TypeCodec)② codec_fields.json 的 SlotTypes["<模块>::<槽位>"](全模块扫 `<槽位> = <工厂>(接收者).n7f(new <描述符>)` 得来,带 Src = 文件:行号)。',
|
||
'两级都没解出来、或解出来的类型 lib_space_codec 没登记(any/number/throwable/xxArray/认不出的裸类名…)→ Type = "<无 codec>",并计入该方法的 Unresolved;不会拿裸类名硬当类型。',
|
||
'字段名优先用 ① 的 Name;没有时取分支里 f("_<方法名>_<字段名>Codec") 的报错串中间那段(如 _enteredUidIsBusy_advisedUidsCodec → advisedUids);都取不到就留空。',
|
||
'帧结构(src/module_61860.js:2111-2131):[nullmap][记录…],nullmap 一条记录 1 bit(bit=1 → 1 字节短命令;bit=0 → [objectId i64][methodId i64][负载])。所以记录的负载里不会出现【字段】之外的字节,帧体内多出来的字节一定是下一条记录。',
|
||
'Kind = container 的条目(LoadDependencies / LoadObjectsData / UnloadObjects / SetModelCommandAlias)负载不是「按字段顺序排」,必须走 server/container.js 的布局,不能按 Fields 硬解。',
|
||
'Unresolved > 0 表示这条方法还有字段没解出来(缺 codec 或读取器未登记),semantic.js 遇到它会退回「部分语义化」。',
|
||
].join(' ');
|
||
|
||
const Report = {
|
||
Note,
|
||
Src: SrcDir,
|
||
ReaderType,
|
||
Stats: {
|
||
Handlers: HandlerCount,
|
||
MinedCommands: Final.length,
|
||
MethodTableEntries: AllMethods.length,
|
||
CoveredByMethodTable: Covered.length,
|
||
Fields: AllFields.length,
|
||
TypeBySlotField: BySlotField,
|
||
TypeBySlotTypes: BySlotTypes,
|
||
TypeByName: ByName,
|
||
UnknownFields: AllFields.filter((F) => !F.Name && !F.Reader).length,
|
||
WithUnresolved: Final.filter((C) => C.Unresolved > 0).length,
|
||
UnresolvedFields: Final.reduce((A, C) => A + C.Unresolved, 0),
|
||
AllFieldsNamed: Final.filter((C) => C.Fields.length && C.Fields.every((F) => F.Name)).length,
|
||
SlotConflicts: SlotConflicts.length,
|
||
DuplicatedMethods: Duplicated.length,
|
||
},
|
||
Containers,
|
||
Commands: Final,
|
||
Duplicated,
|
||
SlotConflicts,
|
||
Problems,
|
||
};
|
||
fs.writeFileSync(OutPath, JSON.stringify(Report, null, 1), 'utf8');
|
||
|
||
// ---------------------------------------------------------------- 控制台摘要
|
||
console.log('源目录 : ' + SrcDir);
|
||
console.log('输出 : ' + OutPath);
|
||
console.log('');
|
||
console.log('q7i 处理器 : ' + HandlerCount);
|
||
console.log('挖出下行方法 : ' + Final.length + ' 条');
|
||
console.log(' 与 method_table 命中 : ' + Covered.length + ' / ' + AllMethods.length);
|
||
console.log(' 字段总数 : ' + AllFields.length + '(槽位查 Fields[].Slot 解出 ' + BySlotField + ' 条,查 SlotTypes 解出 ' + BySlotTypes + ' 条,靠同名字段反查 ' + ByName + ' 条)');
|
||
console.log(' 字段全有名 : ' + Report.Stats.AllFieldsNamed);
|
||
console.log(' 还有字段没解出来 : ' + Report.Stats.WithUnresolved + ' 条方法 / ' + Report.Stats.UnresolvedFields + ' 个字段');
|
||
console.log(' 同 methodId 多实现 : ' + Duplicated.length);
|
||
console.log(' slot 多义 : ' + SlotConflicts.length);
|
||
console.log(' 解析告警 : ' + Problems.length);
|
||
if (Problems.length) for (const P of Problems.slice(0, 8)) console.log(' · ' + P);
|
||
console.log('');
|
||
console.log('样例(前 8 条):');
|
||
for (const C of Final.slice(0, 8)) {
|
||
console.log(' ' + C.MethodId + ' ' + C.Module + ' ' + C.OwnerClass +
|
||
' ' + C.Fields.map((F) => (F.Name || F.Slot || F.Reader) + ':' + F.Type).join(', '));
|
||
}
|
||
|
||
// ---------------------------------------------------------------- --check:拿真实抓包帧验证
|
||
if (Check) {
|
||
const { LoadTables, MakeReader } = require('./lib_space_codec');
|
||
const Reader = MakeReader(LoadTables(NamesDir), { EnumBytes: 4 });
|
||
// 结构化记录要用语义层现场编码(见下面 Buf/Origin 的分支)
|
||
const Semantic = require('./server/semantic').Load(NamesDir);
|
||
const Cases = [
|
||
{
|
||
File: 'space-init-login-100045260.json',
|
||
MethodId: '1247179769701109529',
|
||
Why: 'RegistrationModel.EnteredUidIsBusy:advisedUids = list<string>[5 个账号名](55B),随后同帧记录 2(objectId+methodId=6180114602676583672,0 字段)→ 共 71B',
|
||
},
|
||
{
|
||
File: 'space-init-login-100045260.json',
|
||
MethodId: '237130683810296524',
|
||
Why: 'RememberLoginModel.RememberUsersHash:hash = 64 字符 token(65B),随后同帧记录 2(objectId + methodId=2025886178375810581 + 其 i64 负载 8B)→ 共 89B',
|
||
},
|
||
{ File: 'space-init-100045260.json', MethodId: '2025886178375810581', Why: '单记录:1 个 i64 = 3074457345817168740,恰好吃满 8/8' },
|
||
{ File: 'space-init-100045260.json', MethodId: '7640916300855664666', Why: '容器 pt(_loadObjectsData,448B):应识别为容器,不硬解成字段' },
|
||
];
|
||
// 字段在解出对象里的键:匿名字段(表里只有线宽)用 (匿名#序) 区分,否则会互相覆盖
|
||
const FieldKey = (Fd) => (Fd.Name || Fd.Slot || '(匿名#' + Fd.Order + ')') + ':' + Fd.Type;
|
||
const FieldList = (Spec) => (Spec && Spec.Fields.length ? Spec.Fields.map((F) => (F.Name || F.Slot || F.Reader) + ':' + F.Type).join(', ') : '(无字段)');
|
||
console.log('');
|
||
console.log('=== --check:用真实抓包帧验证字段表 ===');
|
||
for (const C of Cases) {
|
||
const P = path.join(NamesDir, C.File);
|
||
if (!fs.existsSync(P)) continue;
|
||
const T = JSON.parse(fs.readFileSync(P, 'utf8'));
|
||
for (const F of T.Frames || []) {
|
||
for (const R of F.Records || []) {
|
||
if (R.MethodId !== C.MethodId) continue;
|
||
const Spec = ByMethod.get(R.MethodId);
|
||
console.log(' ' + R.MethodId + ' ' + C.File + ' ' + C.Why);
|
||
// 容器型:负载字节不是「按字段顺序排」,报容器而不是硬解(容器布局见 server/container.js)
|
||
if (Spec && Spec.Kind === 'container') {
|
||
const L = (Containers.find((X) => X.MethodId === R.MethodId) || {}).Layouts || [];
|
||
console.log(' 字段表: [容器型 layout=' + L.join(',') + '] ' + FieldList(Spec) +
|
||
' ← 不作为普通字段解,交给 container.js');
|
||
console.log(' 解出 : [容器](Hex ' + (R.Hex ? R.Hex.length / 2 : 0) + ' 字节,容器布局不按字段顺序)');
|
||
continue;
|
||
}
|
||
// 模板升级成结构化记录之后,这里不再是"原始字节":改用 server/semantic.js /
|
||
// server/container.js 现场编码出字节,再走同一套校验。这样校验从"能不能解开真实字节"
|
||
// 变成"编码出的字节能不能被同一张表解回原值",对结构化记录仍有意义(能抓出编码/解码不自洽)。
|
||
let Buf;
|
||
let Origin;
|
||
if (R.Hex) {
|
||
Buf = Buffer.from(R.Hex, 'hex');
|
||
Origin = '原始 hex';
|
||
} else if (R.Fields) {
|
||
// 注意语义层的入参形状是「记录对象」(含 Fields),不是裸的 Fields 表
|
||
Buf = Semantic.EncodeRecord(R.MethodId, R);
|
||
Origin = '结构化 Fields → 现场编码';
|
||
} else {
|
||
console.log(' 跳过:记录既没有 Hex 也没有 Fields(Layout=' + R.Layout + ')');
|
||
continue;
|
||
}
|
||
const Out = {};
|
||
let Off = 0;
|
||
let Bad = false;
|
||
let Why2 = '';
|
||
const Chain = [];
|
||
// 记录链:模板里的 Hex 是「该帧体从本条记录负载起剩下的全部字节」(见文件头「帧结构」注释),
|
||
// 所以吃完本条字段后若还有剩余,一定是下一条记录 —— bit=0 的长形态 [objectId i64][methodId i64][负载]。
|
||
// (nullmap 的 bit 不在 Hex 里,短形态(bit=1,1 字节命令号)无法判也无法解,遇到就如实报错。)
|
||
let CurSpec = Spec;
|
||
let CurId = R.MethodId;
|
||
let ObjectId = null;
|
||
for (let Round = 0; ; Round++) {
|
||
const Val = {};
|
||
for (const Fd of (CurSpec && CurSpec.Fields) || []) {
|
||
const Res = Reader.ReadValue(Buf, Off, Fd.Type, Fd.TypeCodec, 0);
|
||
Val[FieldKey(Fd)] = Res.Value;
|
||
if (Res.Bad || Res.Next <= Off) { Bad = true; break; }
|
||
Off = Res.Next;
|
||
}
|
||
Chain.push({ Round: Round + 1, MethodId: CurId, ObjectId, Fields: CurSpec ? CurSpec.Fields : null, Value: Val });
|
||
for (const [K, V] of Object.entries(Val)) Out[(Round ? '#' + (Round + 1) + ' ' : '') + K] = V;
|
||
if (Bad) { Why2 = '本条字段解不动'; break; }
|
||
if (Off >= Buf.length) break;
|
||
if (Off + 16 > Buf.length) { Bad = true; Why2 = '剩余 ' + (Buf.length - Off) + 'B 不足一条记录头(objectId+methodId=16B)'; break; }
|
||
ObjectId = Buf.readBigInt64BE(Off).toString();
|
||
CurId = Buf.readBigInt64BE(Off + 8).toString();
|
||
Off += 16;
|
||
const Next = ByMethod.get(CurId);
|
||
if (!Next) { Bad = true; Why2 = '续记录 methodId=' + CurId + ' 不在字段表里'; break; }
|
||
if (Next.Kind === 'container') { Bad = true; Why2 = '续记录 methodId=' + CurId + ' 是容器型,容器不能出现在记录链中间'; break; }
|
||
CurSpec = Next;
|
||
}
|
||
console.log(' 字段表: ' + FieldList(Spec));
|
||
// 证据:源码描述符解出来的字段把出处一起打出来(格式 模块.js:行号 描述符)
|
||
const Ev = ((Spec && Spec.Fields) || []).filter((F) => F.Src).map((F) => (F.Name || F.Slot) + ' ← ' + F.Src);
|
||
if (Ev.length) for (const E of Ev) console.log(' 出处 : ' + E);
|
||
if (Chain.length > 1) {
|
||
console.log(' 记录链: ' + Chain.map((X) =>
|
||
'#' + X.Round + ' objectId=' + (X.ObjectId === null ? '(本条)' : X.ObjectId) + ' methodId=' + X.MethodId +
|
||
' [' + FieldList(ByMethod.get(X.MethodId)) + ']').join(' → '));
|
||
for (const X of Chain) if (X.Round > 1) console.log(' 记录' + X.Round + ' 解出: ' + JSON.stringify(X.Value));
|
||
}
|
||
console.log(' 解出 : ' + JSON.stringify(Out) + ' 吃掉 ' + Off + '/' + Buf.length + ' 字节' +
|
||
(Bad ? ' ⚠' + (Why2 || '中途失败') : Off === Buf.length ? ' ✓恰好吃满' : ' ⚠有剩余 ' + (Buf.length - Off) + 'B') +
|
||
' (负载来源:' + Origin + ')');
|
||
}
|
||
}
|
||
}
|
||
}
|