- 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 上行命令全量有类型
1360 lines
65 KiB
JavaScript
1360 lines
65 KiB
JavaScript
// extract_codec_fields.js - 从空间层 codec 模块里抽出「字段名 + 类型 + 线上顺序」
|
||
// 用法:
|
||
// node extract_codec_fields.js // 扫 src/ 全部模块(默认)
|
||
// node extract_codec_fields.js --scan // 同上(保留这个写法)
|
||
// node extract_codec_fields.js --module ../src/module_54199.js // 只扫一个模块(调试用)
|
||
// node extract_codec_fields.js --show st,ft // 打印指定 codec 的字段表
|
||
// node extract_codec_fields.js --find-method <id> // 反查 methodId 的字段表
|
||
//
|
||
// 原理(Kotlin/JS 协议 codec 的三段式结构;codec 变量名只在本模块内唯一,故键 = 模块::变量名):
|
||
// ① 字段名 getter: function G(t) { var n = t.<slot>; if (null != n) return n; v("codec_<字段名>"); }
|
||
// —— 报错字符串就是字段名(Kotlin 惰性属性名的兜底提示)
|
||
// ② (X(Codec).q7g = function (t) { this.<slot> = t.n7f(new <描述符>(...)); ... })
|
||
// —— slot → 类型描述符。首参以 "new " 开头 → 该头是「可选」包装;否则是「必填」包装;
|
||
// b(X) → 列表<X>;w().pb() → bool;w().xb() → string
|
||
// ③ (X(Codec).p7g = function (t) { ... G(this).p7g(t) ... t.a7g_1.<x>u() ... })
|
||
// —— 调用次序 = 线上字段次序;匿名 t.a7g_1.<x>u() 是无名字段的裸读
|
||
// 位宽: xy=i8 s2t=i16 t2t=i32 u2t=i64 v2t=f32 w2t=f64(见 module_72834 的读取器定义)
|
||
// ④ p7g 里给数据类赋值的属性名(如 i.xi0_1 = ...)是该模型数据类的有序字段名,
|
||
// 可与 names/regdump.json 里的模型字段列表 join 出 modelId
|
||
// ⑤ 顶层 SlotTypes:全库扫 `<接收者>.<槽位> = <工厂>(<接收者>).n7f(new <描述符>)`(q7g 之外的局部 codec 实例),
|
||
// 供下行字段表定位「q7i 分支里读的那个槽位是什么类型」
|
||
// 产物: names/codec_fields.json(+ names/uplink_table.json)
|
||
const fs = require('fs');
|
||
const path = require('path');
|
||
|
||
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 PROJECT = path.resolve(__dirname, '..');
|
||
const OutPath = path.resolve(Arg('out', path.join(PROJECT, 'names', 'codec_fields.json')));
|
||
const ModuleArg = Arg('module', '');
|
||
// 默认就是整库扫:本脚本的产物 names/codec_fields.json 永远是「全库表」,
|
||
// 只扫一个模块会把它覆盖成残表(下游 mine_downlink / lib_space_codec 会直接失效),
|
||
// 所以「只扫单模块」必须显式给 --module(或 --scan 与 --module 并存时以 --scan 为准)。
|
||
const Scan = Argv.indexOf('--scan') >= 0 || !ModuleArg;
|
||
const SrcDir = path.join(PROJECT, 'src');
|
||
const ModuleFiles = Scan
|
||
? fs
|
||
.readdirSync(SrcDir)
|
||
.filter((F) => /^module_\d+\.js$/.test(F))
|
||
.map((F) => path.join(SrcDir, F))
|
||
: [path.resolve(ModuleArg)];
|
||
|
||
// ---------------------------------------------------------------- 通用工具
|
||
// 配平扫描:从 Open 处的开括号找到对应闭括号(跳过字符串字面量)
|
||
function MatchBracket(Text, Open, OpenCh, CloseCh) {
|
||
let Depth = 0;
|
||
for (let I = Open; I < Text.length; I++) {
|
||
const C = Text[I];
|
||
if (C === '"' || C === "'" || C === '`') {
|
||
const Q = C;
|
||
I++;
|
||
while (I < Text.length && Text[I] !== Q) {
|
||
if (Text[I] === '\\') I++;
|
||
I++;
|
||
}
|
||
} else if (C === OpenCh) Depth++;
|
||
else if (C === CloseCh) {
|
||
Depth--;
|
||
if (!Depth) return I;
|
||
}
|
||
}
|
||
return -1;
|
||
}
|
||
|
||
// 按顶层逗号切分实参
|
||
function SplitArgs(Text) {
|
||
const Out = [];
|
||
let Depth = 0;
|
||
let Start = 0;
|
||
for (let I = 0; I < Text.length; I++) {
|
||
const C = Text[I];
|
||
if (C === '"' || C === "'" || C === '`') {
|
||
const Q = C;
|
||
I++;
|
||
while (I < Text.length && Text[I] !== Q) {
|
||
if (Text[I] === '\\') I++;
|
||
I++;
|
||
}
|
||
} else if (C === '(' || C === '[' || C === '{') Depth++;
|
||
else if (C === ')' || C === ']' || C === '}') Depth--;
|
||
else if (C === ',' && Depth === 0) {
|
||
Out.push(Text.slice(Start, I).trim());
|
||
Start = I + 1;
|
||
}
|
||
}
|
||
Out.push(Text.slice(Start).trim());
|
||
return Out;
|
||
}
|
||
|
||
// 偏移量 → 行号:SlotTypes 的 Desc 必须能指回源码(如 module_10283.js:2343),否则没法核证据
|
||
function MakeLineIndex(Text) {
|
||
const Starts = [0];
|
||
for (let I = 0; I < Text.length; I++) if (Text[I] === '\n') Starts.push(I + 1);
|
||
return (Off) => {
|
||
let Lo = 0;
|
||
let Hi = Starts.length - 1;
|
||
while (Lo < Hi) {
|
||
const Mid = (Lo + Hi + 1) >> 1;
|
||
if (Starts[Mid] <= Off) Lo = Mid;
|
||
else Hi = Mid - 1;
|
||
}
|
||
return Lo + 1;
|
||
};
|
||
}
|
||
|
||
// ---------------------------------------------------------------- 类型解析
|
||
// 基础类型注册表的访问器 → 可读类型名
|
||
// 来源: module_37036 的 `Wr($_).<accessor> = function () { return this.<xxx>Class; }`
|
||
// (旧版 tools/ 里把它误当成 codec 的 name/sid 读取器,这里以源码为准)
|
||
const PrimTable = {
|
||
mb: 'any',
|
||
nb: 'number',
|
||
ob: 'nothing',
|
||
pb: 'bool',
|
||
qb: 'i8',
|
||
rb: 'i16',
|
||
sb: 'i32',
|
||
tb: 'i64',
|
||
ub: 'f32',
|
||
vb: 'f64',
|
||
wb: 'array',
|
||
xb: 'string',
|
||
yb: 'throwable',
|
||
zb: 'boolArray',
|
||
ac: 'charArray',
|
||
bc: 'i8Array',
|
||
cc: 'i16Array',
|
||
dc: 'i32Array',
|
||
ec: 'f32Array',
|
||
fc: 'f64Array',
|
||
};
|
||
const PrimName = (K) => PrimTable[K] || 'prim:' + K;
|
||
|
||
// 读取器方法名 → 线级数值宽度(module_72834 的读取器)
|
||
// 注意:这是「读」方法。e98d0216 版里 p2u..u2u 是读、n2u/o2u 是写,旧版口径整体右移了,
|
||
// 所以这里以当前源码为准(h(F).p2u→xy、q2u→u2t、r2u→v2t、s2u→w2t、t2u→x2t、u2u→y2t)。
|
||
const ReaderType = {
|
||
xy: 'i8',
|
||
p2u: 'i8',
|
||
q2u: 'i16',
|
||
r2u: 'i32',
|
||
s2u: 'i64',
|
||
t2u: 'f32',
|
||
u2u: 'f64',
|
||
};
|
||
|
||
// 类型描述符表达式 → {Type, Raw}
|
||
// 描述符类只在别处定义,各模块用局部别名引用,因此先按「角色」识别别名(见 FindRoles)。
|
||
// 角色 List → ListCodecInfo(element, optional) → list<...>
|
||
// 角色 Enum → EnumCodecInfo(type, optional) → enum<...>
|
||
// 角色 Map → MapCodecInfo(key, value, optional) → map<K, V>
|
||
// 角色 Type → TypeCodecInfo(type, optional) → ...
|
||
// 角色 KClass→ kClass(X) → X
|
||
// 角色 Prim → KClass 注册表实例,X().pb() 取基础类型类 → bool/i32/string/...
|
||
// optional 标志是构造器第 2 参:!0 = optional = true
|
||
function TypeOfDescriptor(Expr, Roles, Depth) {
|
||
const Raw = Expr.trim();
|
||
if (Depth > 6) return { Type: '...', Raw };
|
||
const E = Raw.replace(/^new\s+/, '');
|
||
// 基础类型:X().pb()
|
||
const Whole = /^([A-Za-z_$][\w$]*)\(\)\.([A-Za-z0-9_$]+)\(\)$/.exec(E);
|
||
if (Whole) return { Type: PrimName(Whole[2]), Raw };
|
||
const M = /^([A-Za-z_$][\w$]*)\(([\s\S]*)\)$/.exec(E);
|
||
if (!M) return { Type: E || '?', Raw };
|
||
const Role = Roles[M[1]] || '';
|
||
const Args = SplitArgs(M[2]);
|
||
const First = Args[0] || '';
|
||
const Optional = Args.some((A) => A === '!0');
|
||
let Base;
|
||
if (Role === 'List') {
|
||
Base = 'list<' + TypeOfDescriptor(First, Roles, Depth + 1).Type + '>';
|
||
} else if (Role === 'Map') {
|
||
// map 描述符:MapCodecInfo(keyCodec, valueCodec, optional)(src/module_24635.js:262-264 的 pn 构造函数:
|
||
// this.t7h_1 = key、this.u7h_1 = value,第 3 参 optional 走 cn.call(this, i))。
|
||
// 真正上线的是 src/module_24635.js:489-512 的 Jt:o7g/p7g 先写/读一个变长条数(w.s7g/w.t7g,
|
||
// 与 list/string 同一个长度编解码,见 src/module_24635.js:589-611),再逐条写/读 key、value。
|
||
// 所以线上形状 = map<K, V>,K/V 各自按自己的描述符编码。
|
||
const K = TypeOfDescriptor(Args[0] || '', Roles, Depth + 1).Type;
|
||
const V = TypeOfDescriptor(Args[1] || '', Roles, Depth + 1).Type;
|
||
Base = 'map<' + (K || '?') + ', ' + (V || '?') + '>';
|
||
} else {
|
||
const Inner = TypeOfDescriptor(First, Roles, Depth + 1).Type;
|
||
if (Role === 'Enum') Base = 'enum<' + Inner + '>';
|
||
else if (Role === 'KClass') Base = Inner;
|
||
else Base = Inner || '?';
|
||
}
|
||
return { Type: Optional ? 'opt<' + Base + '>' : Base, Raw };
|
||
}
|
||
|
||
// JS 级类型线索(来自 p7g 里对数据类赋值的判定)→ 可读类型名
|
||
function JsTypeOf(Seg) {
|
||
if (/"boolean" == typeof/.test(Seg)) return 'bool';
|
||
if (/"string" == typeof/.test(Seg)) return 'string';
|
||
const Inst = /instanceof ([A-Za-z_$][\w$]*)/.exec(Seg);
|
||
if (Inst) return Inst[1];
|
||
if (/[A-Za-z_$][\w$]*\([A-Za-z_$][\w$]*, [A-Za-z_$][\w$]*\)/.test(Seg)) return 'list';
|
||
return '';
|
||
}
|
||
|
||
// ---------------------------------------------------------------- 别名角色识别
|
||
// 模块结构(切割后):
|
||
// ;(window.__modules__ = window.__modules__ || {})[54199] = (t, n, i) => {
|
||
// !(function (t, n, i, r, e, u) { "use strict"; ... })
|
||
// (t.exports, i(37036), i(24635), i(88693), i(52943), i(61860));
|
||
// };
|
||
// 所以「IIFE 形参名 → 依赖模块号」可从句尾的调用实参表读出来。
|
||
// 描述符三兄弟定义在 module_24635:
|
||
// $_$.e = gn = TypeCodecInfo(type, optional) 单值
|
||
// $_$.c = wn = ListCodecInfo(element, optional) 列表
|
||
// $_$.b = vn = EnumCodecInfo(type, optional) 枚举
|
||
// $_$.d = pn = MapCodecInfo(key, value, optional) 映射(README 里说的「三兄弟」其实有四兄弟,
|
||
// d 这一支以前没被认出来,于是 map 字段被当成 enum 之类按 4 字节读 → 负载错位)
|
||
// 基础类型注册表与 kClass 助手在 module_37036:$_$.w4 = j_()、$_$.mf = x_(cls)
|
||
const CODEC_INFO_MODULE = '24635';
|
||
const KOLIB_MODULE = '37036';
|
||
const ROLE_KEYS = {
|
||
[CODEC_INFO_MODULE + '::e']: 'Type',
|
||
[CODEC_INFO_MODULE + '::c']: 'List',
|
||
[CODEC_INFO_MODULE + '::b']: 'Enum',
|
||
[CODEC_INFO_MODULE + '::d']: 'Map',
|
||
[KOLIB_MODULE + '::w4']: 'Prim',
|
||
// kClass 助手:旧包导出键 mf,新包(e98d0216)为 nf;两个都收,换版时不必再改
|
||
[KOLIB_MODULE + '::mf']: 'KClass',
|
||
[KOLIB_MODULE + '::nf']: 'KClass',
|
||
};
|
||
|
||
// module_03569.js → '3569'
|
||
const ModIdOf = (File) => String(Number((/module_(\d+)\.js$/.exec(File) || [, '0'])[1]));
|
||
|
||
// 形参名 → 依赖模块号
|
||
function FindDeps(Src) {
|
||
const ParamToMod = {};
|
||
const At = Src.lastIndexOf('})(');
|
||
if (At < 0) return ParamToMod;
|
||
let Close = MatchBracket(Src, At + 2, '(', ')');
|
||
if (Close < 0) Close = Src.length;
|
||
const Args = SplitArgs(Src.slice(At + 3, Close));
|
||
const Head = /\(\s*function\s*\(([^)]*)\)\s*\{/.exec(Src);
|
||
if (!Head) return ParamToMod;
|
||
const Params = SplitArgs(Head[1]);
|
||
for (let I = 0; I < Params.length && I < Args.length; I++) {
|
||
const M = /\(\s*(\d+)\s*\)/.exec(Args[I]);
|
||
if (M) ParamToMod[Params[I]] = M[1];
|
||
}
|
||
return ParamToMod;
|
||
}
|
||
|
||
// 本模块导出的类:$_$.<key> = <类名>
|
||
function FindExports(Src) {
|
||
const Out = {};
|
||
const Re = /\(\s*t\.\$_\$\.([A-Za-z0-9_$]+) = ([A-Za-z_$][\w$]*)\s*[,);]/g;
|
||
let M;
|
||
while ((M = Re.exec(Src))) if (!Out[M[1]]) Out[M[1]] = M[2];
|
||
return Out;
|
||
}
|
||
|
||
// 局部别名 → 引用的是哪个模块的哪个导出:<别名> = <形参>.$_$.<key>
|
||
// 注意:别名可以是 `$`,所以不能用 \b($ 不是单词字符),改用否定后顾
|
||
function FindAliases(Src, ParamToMod) {
|
||
const Out = {};
|
||
const Re = /(?<![A-Za-z0-9_$.])([A-Za-z_$][\w$]*) = ([A-Za-z_$][\w$]*)\.\$_\$\.([A-Za-z0-9_$]+)[,;]/g;
|
||
let M;
|
||
while ((M = Re.exec(Src))) {
|
||
const Dep = ParamToMod[M[2]];
|
||
if (Dep && !Out[M[1]]) Out[M[1]] = { Mod: Dep, Key: M[3] };
|
||
}
|
||
return Out;
|
||
}
|
||
|
||
// 局部别名 → 角色(Type/List/Enum/Prim/KClass)
|
||
function FindRoles(Src, ParamToMod) {
|
||
const Roles = {};
|
||
const Aliases = FindAliases(Src, ParamToMod);
|
||
for (const [Name, A] of Object.entries(Aliases)) {
|
||
const Role = ROLE_KEYS[A.Mod + '::' + A.Key];
|
||
if (Role) Roles[Name] = Role;
|
||
}
|
||
return Roles;
|
||
}
|
||
|
||
// 数据类真名:Kotlin 生成的 toString 里就写着原始类名
|
||
// (d(Ft).toString = function () { var t = "ChannelSidAndName ["; ...
|
||
function FindClassNames(Src) {
|
||
const Out = {};
|
||
const Re = /\(\s*[A-Za-z_$][\w$]*\(([A-Za-z_$][\w$]*)\)\.toString = function \(\) \{\s*(?:var [A-Za-z_$][\w$]* = )?"([^"]{1,90})/g;
|
||
let M;
|
||
while ((M = Re.exec(Src))) {
|
||
const N = /^(?:class\s+)?([A-Za-z_][\w$]*)/.exec(M[2]);
|
||
if (N && !Out[M[1]]) Out[M[1]] = N[1];
|
||
}
|
||
return Out;
|
||
}
|
||
|
||
// 数据类工厂:function Ut() { return ((t = M(d(Ft))), Ft.call(t), t); } → Ut 其实是造 Ft
|
||
function FindFactories(Src) {
|
||
const Out = {};
|
||
// 旧包:return ((X = Y(d(Z))), Z.call(X), X);
|
||
// 新包:return ((t = L(w(Mt))), Mt.call(t), t); ← 多包了一层工厂,故放宽为「两层单参调用」
|
||
const Re =
|
||
/function ([A-Za-z_$][\w$]*)\(\) \{\s*return \(\(([A-Za-z_$][\w$]*) = [A-Za-z_$][\w$]*\([A-Za-z_$][\w$]*\(([A-Za-z_$][\w$]*)\)\)\), \3\.call\(\2\), \2\);/g;
|
||
let M;
|
||
while ((M = Re.exec(Src))) if (!Out[M[1]]) Out[M[1]] = M[3];
|
||
return Out;
|
||
}
|
||
|
||
// ---------------------------------------------------------------- 各段落解析
|
||
// 找 `A(Codec).q7g = function (t) {` 形式的块体(o7g 是 (t, n) 两个参数,所以形参表放宽)
|
||
function FindBlocks(Src, Prop) {
|
||
const Out = [];
|
||
const Re = new RegExp('\\b([A-Za-z_$][\\w$]*)\\(([A-Za-z_$][\\w$]*)\\)\\.' + Prop + ' = function \\([^)]*\\)\\s*\\{', 'g');
|
||
let M;
|
||
while ((M = Re.exec(Src))) {
|
||
const Open = Src.indexOf('{', M.index + M[0].length - 1);
|
||
const Close = MatchBracket(Src, Open, '{', '}');
|
||
if (Close < 0) break;
|
||
Out.push({ Codec: M[2], Body: Src.slice(Open + 1, Close) });
|
||
Re.lastIndex = Close + 1;
|
||
}
|
||
return Out;
|
||
}
|
||
|
||
// 字段名 getter: slot → 字段名(模块内唯一)
|
||
function FindGetters(Src) {
|
||
const SlotToName = {};
|
||
const GetterToSlot = {};
|
||
const Re =
|
||
/function ([A-Za-z_$][\w$]*)\(t\) \{\s*var ([A-Za-z_$][\w$]*) = t\.([a-z0-9]+_\d+);\s*if \(null != \2\) return \2;\s*([A-Za-z_$][\w$]*)\(\s*"(?:codec_)?([A-Za-z][\w]*)"\s*\)/g;
|
||
let M;
|
||
while ((M = Re.exec(Src))) {
|
||
SlotToName[M[3]] = M[5];
|
||
GetterToSlot[M[1]] = M[3];
|
||
}
|
||
return { SlotToName, GetterToSlot };
|
||
}
|
||
|
||
// q7g 块 → [{Slot, Desc, Type, Raw}]
|
||
function ParseSlots(Body, Roles) {
|
||
const Out = [];
|
||
// 字段工厂:旧包 n7f,新包 r7g
|
||
const Re = /this\.([a-z0-9]+_\d+) = t\.r7g\(/g;
|
||
let M;
|
||
while ((M = Re.exec(Body))) {
|
||
const Open = Body.indexOf('(', M.index + M[0].length - 1);
|
||
const Close = MatchBracket(Body, Open, '(', ')');
|
||
if (Close < 0) break;
|
||
const T = TypeOfDescriptor(Body.slice(Open + 1, Close), Roles, 0);
|
||
Out.push({ Slot: M[1], Desc: T.Raw, Type: T.Type });
|
||
Re.lastIndex = Close + 1;
|
||
}
|
||
return Out;
|
||
}
|
||
|
||
// p7g 块 → [{Slot|Reader, Prop, Js, Wire}],次序即线上次序
|
||
function ParseOrder(Body) {
|
||
const Hits = [];
|
||
// 读序:旧包 p7g,新包 t7h;基础类型走 <缓冲槽位>.<reader>()(旧包固定 a7g_1,新包槽位名会变,故泛化)
|
||
const Re = /([A-Za-z_$][\w$]*)\(this\)\.t7h\(t\)|t\.[a-z0-9]+_\d+\.([A-Za-z0-9_$]+)\(\)/g;
|
||
let M;
|
||
while ((M = Re.exec(Body))) Hits.push({ At: M.index, End: Re.lastIndex, Getter: M[1] || '', Reader: M[2] || '' });
|
||
const Out = [];
|
||
for (let I = 0; I < Hits.length; I++) {
|
||
const H = Hits[I];
|
||
const From = H.End;
|
||
const To = I + 1 < Hits.length ? Hits[I + 1].At : Math.min(Body.length, H.End + 300);
|
||
const Seg = Body.slice(From, To);
|
||
let Prop = '';
|
||
let Js = '';
|
||
if (H.Getter) {
|
||
// 有名字段:赋值紧跟 getter 调用之后
|
||
const A = /\b([A-Za-z_$][\w$]*)\.([a-z0-9]+_\d+) =/.exec(Seg);
|
||
if (A) Prop = A[2];
|
||
Js = JsTypeOf(Seg);
|
||
Out.push({ Slot: '', Name: '', Prop, Js, Wire: '', Getter: H.Getter, Order: I + 1 });
|
||
} else {
|
||
// 匿名字段:赋值紧贴在读取调用之前
|
||
const Before = Body.slice(Math.max(0, H.At - 120), H.At);
|
||
const A = /([A-Za-z_$][\w$]*)\.([a-z0-9]+_\d+) = $/.exec(Before);
|
||
if (A) Prop = A[2];
|
||
Out.push({ Slot: '', Name: '', Prop, Js: '', Wire: ReaderType[H.Reader] || H.Reader, Getter: '', Order: I + 1 });
|
||
}
|
||
}
|
||
return Out;
|
||
}
|
||
|
||
// ---------------------------------------------------------------- 与注册表 join
|
||
// names/regdump.json(由 tools/client_registry_dump.js 从运行中的客户端导出)里 ModelReg 的表(语义已核实):
|
||
// u7n_1: modelId → 模型类(ctor = 类名;keys 是枚举到的实例属性,不保证等于 codec 槽位)
|
||
// w7n_1: modelId → codec(ctor = codec 名;keys = 该 codec 的槽位名,只有被实例化过才有值)
|
||
// v7n_1: methodId → 命令类(ctor = 命令类名;text = 该命令负载所属的 modelId)
|
||
// 于是:
|
||
// ① 用「槽位名有序列表」在 w7n_1 里查 → 拿到 modelId(同时校验 codec 构造器名)
|
||
// ② 若该 codec 没被实例化(keys 为空)则退化为「codec 构造器名唯一匹配」
|
||
// ③ 再用 modelId 去 v7n_1 反查 methodId 列表(下行方法表)
|
||
function JoinRegistries(Codecs, RegPath) {
|
||
if (!fs.existsSync(RegPath)) return { Ok: false, Reason: '找不到 ' + RegPath };
|
||
const Reg = JSON.parse(fs.readFileSync(RegPath, 'utf8'));
|
||
const Maps = Reg.maps || Reg;
|
||
const Entries = (Name) => (Maps[Name] && Maps[Name].entries) || [];
|
||
|
||
const CodecByModel = new Map(); // modelId → codec 构造器名
|
||
const ModelBySig = new Map(); // 槽位签名 → modelId
|
||
for (const E of Entries('ModelReg.w7n_1')) {
|
||
CodecByModel.set(String(E.key), E.ctor || '');
|
||
if (E.keys && E.keys.length) ModelBySig.set(E.keys.join(','), String(E.key));
|
||
}
|
||
const ModelClass = new Map(); // modelId → 模型类名
|
||
for (const E of Entries('ModelReg.u7n_1')) ModelClass.set(String(E.key), E.ctor || '');
|
||
const MethodByModel = new Map(); // modelId → [methodId]
|
||
const CommandClass = new Map(); // methodId → 命令类名
|
||
for (const E of Entries('ModelReg.v7n_1')) {
|
||
const Id = String(E.key);
|
||
CommandClass.set(Id, E.ctor || '');
|
||
if (!E.text) continue;
|
||
const M = String(E.text);
|
||
if (!MethodByModel.has(M)) MethodByModel.set(M, []);
|
||
MethodByModel.get(M).push(Id);
|
||
}
|
||
// codec 构造器名 → [modelId](仅当全局唯一时才敢用于兜底)
|
||
const ModelsByCtor = new Map();
|
||
for (const [Id, Ctor] of CodecByModel) {
|
||
if (!Ctor) continue;
|
||
if (!ModelsByCtor.has(Ctor)) ModelsByCtor.set(Ctor, []);
|
||
ModelsByCtor.get(Ctor).push(Id);
|
||
}
|
||
// 本次扫到的 codec 里,构造器名也必须唯一(压缩后跨模块重名极常见,如 st/ht/V)
|
||
const CtorCount = {};
|
||
for (const C of Object.values(Codecs)) CtorCount[C.Codec] = (CtorCount[C.Codec] || 0) + 1;
|
||
|
||
const Conflicts = [];
|
||
let BySigHit = 0;
|
||
let ByCtorHit = 0;
|
||
for (const [Key, C] of Object.entries(Codecs)) {
|
||
const Sig = (C.Slots || []).map((S) => S.Slot).join(',');
|
||
let ModelId = Sig ? ModelBySig.get(Sig) : undefined;
|
||
if (ModelId) {
|
||
BySigHit++;
|
||
const Expect = CodecByModel.get(ModelId);
|
||
if (Expect && Expect !== C.Codec) Conflicts.push(Key + ' 签名命中 ' + ModelId + ' 但 codec 名不符(期望 ' + Expect + ')');
|
||
} else if (CtorCount[C.Codec] === 1) {
|
||
// 兜底:codec 构造器名在注册表与本轮扫描里都唯一
|
||
const Cand = ModelsByCtor.get(C.Codec);
|
||
if (Cand && Cand.length === 1) {
|
||
ModelId = Cand[0];
|
||
ByCtorHit++;
|
||
}
|
||
}
|
||
if (!ModelId) continue;
|
||
C.ModelId = ModelId;
|
||
C.ModelClass = ModelClass.get(ModelId) || '';
|
||
C.MethodId = MethodByModel.get(ModelId) || [];
|
||
}
|
||
|
||
const Count = { ModelId: 0, MethodId: 0 };
|
||
for (const C of Object.values(Codecs)) {
|
||
if (C.ModelId) Count.ModelId++;
|
||
if (C.MethodId && C.MethodId.length) Count.MethodId++;
|
||
}
|
||
return {
|
||
Ok: true,
|
||
CodecEntries: CodecByModel.size,
|
||
MethodEntries: CommandClass.size,
|
||
BySigHit,
|
||
ByCtorHit,
|
||
Count,
|
||
Conflicts,
|
||
};
|
||
}
|
||
|
||
// ---------------------------------------------------------------- 类名还原
|
||
// 字段类型里出现的裸局部类名(Ft / en / Wn / ni / $ …)需要还原成 Kotlin 原名:
|
||
// ① 本模块内定义的数据类 → 看 (d(X).toString = ...) 里的字符串
|
||
// ② 从别的模块 import 的类 → 先查别名(X = 形参.$_$.key)定位到「模块+导出键」,
|
||
// 再取那个模块的导出表拿到它本地的类名,最后同样查它自己的 toString
|
||
// ③ 没有 toString 的类(非 data class)→ 用「模型类 → codec」反查,
|
||
// 这样即使名字还原不了,也至少能指向解码该嵌套模型用的 codec
|
||
const PRIM_WORDS = new Set(
|
||
Object.values(PrimTable).concat(['i8', 'i16', 'i32', 'i64', 'f32', 'f64', 'bytes', 'opt', 'list', 'enum', 'map'])
|
||
);
|
||
|
||
// 返回 { Name, Via, Mod, Class };Via 为空表示没还原出原名
|
||
function ResolveClass(Name, ModId, ModuleInfo, Seen) {
|
||
const Info = ModuleInfo[ModId];
|
||
const Self = { Name, Via: '', Mod: ModId, Class: Name };
|
||
if (!Info) return Self;
|
||
if (Info.Classes[Name]) return { Name: Info.Classes[Name], Via: 'local', Mod: ModId, Class: Name };
|
||
const A = Info.Aliases[Name];
|
||
if (!A) return Self;
|
||
const Dep = ModuleInfo[A.Mod];
|
||
const Cls = Dep && Dep.Exports[A.Key];
|
||
if (!Cls) return Self;
|
||
if (Dep.Classes[Cls]) return { Name: Dep.Classes[Cls], Via: 'import', Mod: A.Mod, Class: Cls };
|
||
// 导出的类自己也可能只是别处的再导出 → 顺着别名链再走一层
|
||
if (!Seen.includes(A.Mod)) {
|
||
const R = ResolveClass(Cls, A.Mod, ModuleInfo, Seen.concat(A.Mod));
|
||
if (R.Via) return R;
|
||
}
|
||
return { Name: Cls, Via: 'import-raw', Mod: A.Mod, Class: Cls };
|
||
}
|
||
|
||
// 单个别名 → { Name, Codec }(Codec = 该类的 codec 键,供递归解码用)
|
||
// 协议核心类 / 数学类在线上没有独立 codec,但会话双方对它们的字节形状是固定的。
|
||
// 键 = 模块::导出键(唯一身份,避免重名),值 = 线上形状:
|
||
// 61860::r = xr(协议实体引用)→ i64
|
||
// 37036::sj = Kotlin Long → i64
|
||
// 32088::j = Vector3 → 3 × f32(由生成器展开)
|
||
const CoreIdentities = {
|
||
'61860::r': 'i64',
|
||
'37036::sj': 'i64',
|
||
'32088::j': 'vector3',
|
||
};
|
||
|
||
// 资源类:模块::导出键;线上只出现 i64 引用(由下面的 y84 注册块填充)
|
||
const ResourceIdentities = new Set();
|
||
|
||
// 类名(模块内短名)→ 稳定身份(模块::导出键)。优先别名,其次导出反查,最后退回本地类名。
|
||
function IdentityOf(Name, ModId, ModuleInfo) {
|
||
const Info = ModuleInfo[ModId] || {};
|
||
const A = (Info.Aliases || {})[Name];
|
||
if (A) return A.Mod + '::' + A.Key;
|
||
const ExportKey = Object.keys(Info.Exports || {}).find((K) => Info.Exports[K] === Name);
|
||
return ModId + '::' + (ExportKey || Name);
|
||
}
|
||
|
||
// 源码里「共享 codec」的登记:q7g(kClass(A), codec.t7g(kClass(B))) → A 与 B 同线形。
|
||
// 实例:module_71105.js:12590 的 Jf 复用 qv 的 codec(两者都是资源引用 i64)。
|
||
function AddSharedCodecIdentities(ModuleFiles, ModuleInfo) {
|
||
for (const File of ModuleFiles) {
|
||
let Src;
|
||
try {
|
||
Src = fs.readFileSync(File, 'utf8');
|
||
} catch (Err) {
|
||
continue;
|
||
}
|
||
const ModId = ModIdOf(File);
|
||
const Roles = (ModuleInfo[ModId] || {}).Roles || {};
|
||
const Re =
|
||
/\.q7g\(\s*([A-Za-z_$][\w$]*)\(\s*([A-Za-z_$][\w$]*)\s*\)\s*,\s*[A-Za-z_$][\w$]*\.t7g\(\s*([A-Za-z_$][\w$]*)\(\s*([A-Za-z_$][\w$]*)\s*\)\s*\)/g;
|
||
let M;
|
||
while ((M = Re.exec(Src))) {
|
||
if ((Roles[M[1]] || '') !== 'KClass' || (Roles[M[3]] || '') !== 'KClass') continue;
|
||
const A = IdentityOf(M[2], ModId, ModuleInfo);
|
||
const B = IdentityOf(M[4], ModId, ModuleInfo);
|
||
if (ResourceIdentities.has(B)) ResourceIdentities.add(A);
|
||
}
|
||
}
|
||
}
|
||
|
||
// 源码侧没有 codec、但有实测标定出处的裸类名(详见 3DTank/tools/server/container.js 的注释)
|
||
const ManualClassWire = { Pc: 'i64' };
|
||
|
||
// 名字优先用 codec 的模型真名;拿不到就退回 toString 得到的名字
|
||
function ResolveOne(Name, ModId, ModuleInfo, ClassToCodec) {
|
||
const R = ResolveClass(Name, ModId, ModuleInfo, []);
|
||
const CodecKey = ClassToCodec[R.Mod + '::' + R.Class] || '';
|
||
if (CodecKey) return { Name: Codecs[CodecKey].ModelName || R.Name, Codec: CodecKey };
|
||
return { Name: R.Name, Codec: '' };
|
||
}
|
||
|
||
// 名字在本模块里的「引用链」:每个跳点写成 模块::导出键(= PrimClassWire 的键)。
|
||
// 为什么要这条链而不是还原后的类名:ResolveClass 把跨模块引用还原成**那个模块里的本地类名**,
|
||
// 而这类本地名是混淆器按模块发的短名(如 15 个模块各有一个 `ji`),拿它查表会撞名;
|
||
// 模块::导出键才是唯一身份(module_37036.js:11088 的 qj 只有一个 ji)。
|
||
function AliasChain(Name, ModId, ModuleInfo) {
|
||
const Hops = [];
|
||
let Cur = Name;
|
||
let Mod = ModId;
|
||
for (let I = 0; I < 6; I++) {
|
||
const Info = ModuleInfo[Mod];
|
||
const A = Info && Info.Aliases[Cur];
|
||
if (!A) break;
|
||
Hops.push(A.Mod + '::' + A.Key);
|
||
const Dep = ModuleInfo[A.Mod];
|
||
const Cls = (Dep && Dep.Exports[A.Key]) || '';
|
||
if (!Cls) break;
|
||
Cur = Cls;
|
||
Mod = A.Mod;
|
||
}
|
||
return Hops;
|
||
}
|
||
|
||
// 类型串 → { Type, Codecs };把里面的裸类名逐个替换掉
|
||
// Trace(可选,调用方给就填):把「既没有 codec、也不在 WireClasses / PrimClassWire 里」的裸类名收进 Leftover。
|
||
// SlotTypes 段靠它判「类型到底推没推出来」——残留裸名意味着线上没有解码器,只能如实留空。
|
||
function ResolveType(Type, ModId, ModuleInfo, ClassToCodec, Trace) {
|
||
const Refs = [];
|
||
const Out = String(Type || '').replace(/[A-Za-z_$][\w$]*/g, (W) => {
|
||
if (PRIM_WORDS.has(W)) return W;
|
||
const R = ResolveOne(W, ModId, ModuleInfo, ClassToCodec);
|
||
if (R.Codec) Refs.push(R.Codec);
|
||
else {
|
||
// Kolib 原始类型类(kClass(跨模块别名的原始类型类),如 module_47243.js:181 的 xi = 37036::qj
|
||
// = Long):按模块::导出键查表换成线上类型(见 PrimClassWire 的注释)。
|
||
const Hop = AliasChain(W, ModId, ModuleInfo).find((H) => PrimClassWire[H]);
|
||
// 协议核心类没有 codec 时按「线上基础类型」换算(如实体引用 xr → i64),否则保留类名
|
||
if (Hop) return PrimClassWire[Hop];
|
||
// 核心类型 / 资源类:按 模块::导出键 的稳定身份换算
|
||
const Chain = AliasChain(W, ModId, ModuleInfo);
|
||
const Core = Chain.find((H) => CoreIdentities[H]);
|
||
if (Core) return CoreIdentities[Core];
|
||
if (Chain.some((H) => ResourceIdentities.has(H))) return 'i64';
|
||
if (ManualClassWire[R.Name]) return ManualClassWire[R.Name];
|
||
if (WireClasses[R.Name]) return WireClasses[R.Name];
|
||
// 两条都没命中:这个裸类名在线上没有解码器,记进 Leftover 让调用方判「类型没推出来」
|
||
else if (Trace) Trace.Leftover.push(W);
|
||
}
|
||
return R.Name;
|
||
});
|
||
return { Type: Out, Codecs: Refs };
|
||
}
|
||
|
||
// ---------------------------------------------------------------- 第一遍:建全模块符号表
|
||
const ModuleInfo = {};
|
||
for (const File of ModuleFiles) {
|
||
let Src;
|
||
try {
|
||
Src = fs.readFileSync(File, 'utf8');
|
||
} catch (Err) {
|
||
continue;
|
||
}
|
||
const ModId = ModIdOf(File);
|
||
const Deps = FindDeps(Src);
|
||
ModuleInfo[ModId] = {
|
||
File,
|
||
Aliases: FindAliases(Src, Deps),
|
||
Roles: FindRoles(Src, Deps),
|
||
Exports: FindExports(Src),
|
||
Classes: FindClassNames(Src),
|
||
Factories: FindFactories(Src),
|
||
};
|
||
}
|
||
|
||
// --who <模块号>:<导出键>:反查哪些模块以什么别名引用了该符号,以及 `new 别名(` 的出现次数
|
||
// 例:node extract_codec_fields.js --who 61860:l → 谁在用 SpaceCommand
|
||
const Who = Arg('who', '');
|
||
if (Who) {
|
||
const [WMod, WKey] = Who.split(':');
|
||
let Total = 0;
|
||
for (const [ModId, Info] of Object.entries(ModuleInfo)) {
|
||
for (const [Name, A] of Object.entries(Info.Aliases)) {
|
||
if (A.Mod !== WMod || A.Key !== WKey) continue;
|
||
const Src2 = fs.readFileSync(Info.File, 'utf8');
|
||
const Esc = Name.replace(/\$/g, '\\$');
|
||
const News = (Src2.match(new RegExp('new ' + Esc + '\\(', 'g')) || []).length;
|
||
const Calls = (Src2.match(new RegExp('(?<![\\w$.])' + Esc + '\\(', 'g')) || []).length;
|
||
Total += News;
|
||
console.log('module_' + ModId + ' 别名 ' + Name + ' new ' + News + ' 次,调用 ' + Calls + ' 次');
|
||
}
|
||
}
|
||
console.log('合计 new ' + Total + ' 次');
|
||
}
|
||
|
||
// ---------------------------------------------------------------- 协议核心类 → 线上基础类型
|
||
// module_61860 用 `vi(t).m7f(G(类), new Codec())` 给协议核心类注册 codec。这类 codec 的 o7g 往往只调一次
|
||
// 写入器,于是「类 → 线上宽度」能直接从 o7g 的写入方法读出:
|
||
// 实体引用 xr 的 codec cr → t.a7g_1.f2u(...) → i64
|
||
// 只认「恰好一次写入」的标量 codec;向量(fi 写 3 次 l2u)、嵌套命令(or/fr)会被自动跳过。
|
||
const WRITER_TYPE = {
|
||
x2t: 'i8', i2u: 'i8',
|
||
c2u: 'i16', j2u: 'i16',
|
||
d2u: 'i32', k2u: 'i32',
|
||
f2u: 'i64', p1s: 'i64',
|
||
g2u: 'f32', l2u: 'f32',
|
||
h2u: 'f64', m2u: 'f64',
|
||
};
|
||
const WireClasses = {};
|
||
// 「Kolib 原始类型类 → 线上类型」,键 = **模块::导出键**(不是裸类名)。
|
||
// 为什么必须带模块:这类类的名字是混淆器按模块发的短名,全库有 15 个模块各有一个 `ji`
|
||
// (src/module_37036.js:661 才是 Kolib 的 Long,其余同名者与协议无关),裸类名当键必然串味。
|
||
// 证据链(登记点在下面循环里,消费点在 ResolveType):
|
||
// src/module_37036.js:661 function ji(t, n) ← Kolib 的 Long(:651-657 的 qi() 用
|
||
// new ji(0, -2147483648) 造 MIN_VALUE)
|
||
// src/module_37036.js:11088 t.$_$.qj = ji ← 导出键 qj
|
||
// src/module_24635.js:36 R = i.$_$.qj ← 引入方(module_24635)的别名
|
||
// src/module_24635.js:731-733 case "Long": i = L(R) ← an.n7f 的 primitive 分派:字符串 "Long"
|
||
// 就映射到这个类(L = kClass 助手)
|
||
// src/module_24635.js:247 this.m7f(L(R), new Xt()) ← 给它注册 codec Xt
|
||
// src/module_24635.js:482-487 Xt.o7g 恰好一次写入 t.a7g_1.f2u(...)、Xt.p7g 读 t.a7g_1.u2t()
|
||
// ⇒ 线上 8 字节 = i64(WRITER_TYPE.f2u)
|
||
// src/module_47243.js:181 xi = n.$_$.qj ← 使用方别名
|
||
// src/module_47243.js:15869 this.nft_1 = Mm(this).n7f(new Ei(new yi(bi(xi), !1), !1))
|
||
// → ListCodecInfo(TypeCodecInfo(kClass(ji))) ⇒ 线上 list<i64>
|
||
// 所以 kClass(<原始类型类>) 推不出来的根因不是角色表(bi 已是 KClass),而是「KClass 的实参是
|
||
// 一个跨模块别名的原始类型类」时,只登记裸别名/裸类名会漏。这里按模块::导出键补齐。
|
||
const PrimClassWire = {};
|
||
for (const File of ModuleFiles) {
|
||
let Src;
|
||
try {
|
||
Src = fs.readFileSync(File, 'utf8');
|
||
} catch (Err) {
|
||
continue;
|
||
}
|
||
const ModId = ModIdOf(File);
|
||
const Roles = (ModuleInfo[ModId] || {}).Roles || {};
|
||
const ByCodec = new Map(FindBlocks(Src, 's7h').map((B) => [B.Codec, B.Body]));
|
||
if (!ByCodec.size) continue;
|
||
const Re = /\.m7f\(\s*([A-Za-z_$][\w$]*)\(([A-Za-z_$][\w$]*)\)\s*,\s*new ([A-Za-z_$][\w$]*)\(/g;
|
||
let M;
|
||
while ((M = Re.exec(Src))) {
|
||
if ((Roles[M[1]] || '') !== 'KClass') continue; // 第一参必须是 kClass(类)
|
||
const Body = ByCodec.get(M[3]);
|
||
if (!Body) continue;
|
||
// 只认「整个 o7g 里恰好调一次写入器」的标量 codec:
|
||
// 向量(fi 写 3 次 l2u)、嵌套命令(fr 写 2 次 f2u)都因次数不为 1 被跳过
|
||
const Calls = [];
|
||
for (const W of Body.matchAll(/(?<![\w$.])[A-Za-z_$][\w$]*\.[A-Za-z0-9_$]+\.([A-Za-z0-9_$]+)\(/g))
|
||
if (WRITER_TYPE[W[1]]) Calls.push(W[1]);
|
||
if (Calls.length !== 1) continue;
|
||
const Type = WRITER_TYPE[Calls[0]];
|
||
if (!Type) continue;
|
||
// 键必须用「还原后的类名」,因为消费方 ResolveType 查的就是它(见上面 ResolveType 的 WireClasses[R.Name])。
|
||
// 只登记源码里的裸别名是不够的:别名只在本模块内有效,跨模块引用同一类时两边名字不同,
|
||
// 本模块定义、没人再引用它的类(如 module_61860.js:439 的 `function xr() {}`)才会碰巧对上。
|
||
// 两种键都登记:裸别名保持既有行为(如 'xr'),原始类型类另走 PrimClassWire(模块::导出键)。
|
||
if (!WireClasses[M[2]]) WireClasses[M[2]] = Type;
|
||
// kClass 的实参是本模块的「跨模块别名」(如 module_24635.js:247 的 R = i.$_$.qj)⇒ 记下它指向的
|
||
// 导出身份,供 ResolveType 在**任何**模块里引用同一类时认得出来(见 PrimClassWire 的注释)。
|
||
// 只认别名、不认本地类名:本地类名是「本模块自己定义的类」,不是原始类型类。
|
||
const Alias = (ModuleInfo[ModId] || {}).Aliases[M[2]];
|
||
if (Alias && !PrimClassWire[Alias.Mod + '::' + Alias.Key]) PrimClassWire[Alias.Mod + '::' + Alias.Key] = Type;
|
||
}
|
||
}
|
||
|
||
// ---------------------------------------------------------------- 资源类 → 线上 i64
|
||
// 出处:src/module_71105.js:12439-12518 的资源类型注册块,形状 `r.u83(<typeId>, kClass(<类>), <工厂>)`
|
||
// (si = module_37036 的 $_$.mf = kClass,见 src/module_71105.js:156)。
|
||
// 被注册的类都是「资源」:资源实例在空间协议里只以 i64 key 出现(查表/构造见 src/module_45686.js:209-228 的
|
||
// qt() → resourceRegistry.x83 与 L() 的 i64/i16/i16/i64/bool 读序)。
|
||
// 为什么从源码导出而不是手写:容器层原来手写了 10 个(container.js 的 RESOURCE_CLASS_WIRE),
|
||
// 漏了 typeId 13 的 no 与 zv 两兄弟——实测 space-100072911/s2c_0001 的 DonationAlertCC.imageWithEmail
|
||
// (类型 = module_71105 的导出 d = 类 no)就是因此解不动。全库 `.u83(` 恰好只出现 12 次、全在 module_71105,
|
||
// 不会被别处同名方法污染,所以这里按注册块全量导出,不做任何选择。
|
||
const ResourceClasses = {};
|
||
for (const File of ModuleFiles) {
|
||
let Src;
|
||
try {
|
||
Src = fs.readFileSync(File, 'utf8');
|
||
} catch (Err) {
|
||
continue;
|
||
}
|
||
const ModId = ModIdOf(File);
|
||
const Roles = (ModuleInfo[ModId] || {}).Roles || {};
|
||
const LineOf = MakeLineIndex(Src);
|
||
// 当前版注册方法:r.y84(<typeId>, oi(<类>), <工厂>);旧版是 .u83(
|
||
const Re = /\.y84\(\s*([^,()]+),\s*([A-Za-z_$][\w$]*)\(\s*([A-Za-z_$][\w$]*)\s*\)/g;
|
||
let M;
|
||
while ((M = Re.exec(Src))) {
|
||
if ((Roles[M[2]] || '') !== 'KClass') continue; // 第 2 参必须是 kClass(类)
|
||
const Name = ResolveClass(M[3], ModId, ModuleInfo, []).Name;
|
||
// 记录稳定身份:注册处传入的可能是本模块别名,也可能是本模块本地类名;
|
||
// 后者要反查导出键(类名只是模块内短名,导出键才是跨模块身份)
|
||
const Info = ModuleInfo[ModId] || {};
|
||
const A = (Info.Aliases || {})[M[3]];
|
||
let Identity = '';
|
||
if (A) Identity = A.Mod + '::' + A.Key;
|
||
else {
|
||
const ExportKey = Object.keys(Info.Exports || {}).find((K) => Info.Exports[K] === M[3]);
|
||
Identity = ModId + '::' + (ExportKey || M[3]);
|
||
}
|
||
ResourceIdentities.add(Identity);
|
||
if (!ResourceClasses[Name])
|
||
ResourceClasses[Name] = { Wire: 'i64', TypeId: M[1].trim(), From: path.basename(File) + ':' + LineOf(M.index) };
|
||
}
|
||
}
|
||
|
||
// 资源型 codec 共享(如 Jf 复用 qv)——必须在 codec 抽取之前填好身份集
|
||
AddSharedCodecIdentities(ModuleFiles, ModuleInfo);
|
||
|
||
// ---------------------------------------------------------------- 第二遍:抽 codec
|
||
const Codecs = {};
|
||
let ModulesUsed = 0;
|
||
let TypeResolved = 0;
|
||
let TypeUnresolved = 0;
|
||
|
||
for (const File of ModuleFiles) {
|
||
let Src;
|
||
try {
|
||
Src = fs.readFileSync(File, 'utf8');
|
||
} catch (Err) {
|
||
continue;
|
||
}
|
||
const Mod = path.basename(File, '.js');
|
||
const ModId = ModIdOf(File);
|
||
const Roles = (ModuleInfo[ModId] || {}).Roles || {};
|
||
const Factories = (ModuleInfo[ModId] || {}).Factories || {};
|
||
const { SlotToName, GetterToSlot } = FindGetters(Src);
|
||
const SlotBlocks = FindBlocks(Src, 'u7h');
|
||
const OrderBlocks = FindBlocks(Src, 't7h');
|
||
if (!SlotBlocks.length && !OrderBlocks.length) continue;
|
||
ModulesUsed++;
|
||
|
||
const OrderByCodec = new Map(OrderBlocks.map((B) => [B.Codec, B.Body]));
|
||
const Names = new Set([...SlotBlocks.map((B) => B.Codec), ...OrderBlocks.map((B) => B.Codec)]);
|
||
for (const CodecName of Names) {
|
||
const SBs = SlotBlocks.filter((B) => B.Codec === CodecName);
|
||
const Slots = [];
|
||
for (const B of SBs) for (const S of ParseSlots(B.Body, Roles)) Slots.push(S);
|
||
const BySlot = new Map(Slots.map((S) => [S.Slot, S]));
|
||
const OrderBody = OrderByCodec.get(CodecName) || '';
|
||
const Order = ParseOrder(OrderBody);
|
||
const Fields = Order.map((O) => {
|
||
let Name = '(匿名)';
|
||
let Type = O.Wire || '?';
|
||
let Desc = '';
|
||
let Slot = '';
|
||
if (O.Getter) {
|
||
Slot = GetterToSlot[O.Getter] || '';
|
||
Name = SlotToName[Slot] || '?';
|
||
const S = BySlot.get(Slot);
|
||
if (S) {
|
||
Desc = S.Desc;
|
||
Type = S.Type;
|
||
} else if (O.Js) Type = O.Js;
|
||
}
|
||
return { Order: O.Order, Name, Type, Js: O.Js, Slot, Desc, Prop: O.Prop, Wire: O.Wire };
|
||
});
|
||
// 模型类:优先看写方法(旧 o7g / 新 s7h)里的 instanceof,其次看读方法用的工厂函数
|
||
const O7g = FindBlocks(Src, 's7h').find((B) => B.Codec === CodecName);
|
||
const Inst = O7g && /instanceof ([A-Za-z_$][\w$]*)/.exec(O7g.Body);
|
||
const Made = /var [A-Za-z_$][\w$]* = ([A-Za-z_$][\w$]*)\(\)/.exec(OrderBody);
|
||
const DataClass = (Inst && Inst[1]) || (Made ? Made[1] : '');
|
||
const Key = Mod + '::' + CodecName;
|
||
Codecs[Key] = {
|
||
Module: Mod,
|
||
ModuleId: ModId,
|
||
Codec: CodecName,
|
||
ModelClassLocal: DataClass,
|
||
ModelClassReal: DataClass ? Factories[DataClass] || DataClass : '',
|
||
ModelName: '',
|
||
Slots,
|
||
Fields,
|
||
ModelKeys: Fields.map((F) => F.Prop).filter(Boolean),
|
||
};
|
||
}
|
||
}
|
||
|
||
// ---------------------------------------------------------------- 第三遍:还原类名与嵌套 codec 链接
|
||
// 「模块::类名 → codec」——同一模块内类名唯一,足以定位解码该嵌套模型要用哪个 codec
|
||
const ClassToCodec = {};
|
||
for (const [Key, C] of Object.entries(Codecs)) {
|
||
if (C.ModelClassLocal) ClassToCodec[C.ModuleId + '::' + C.ModelClassLocal] = Key;
|
||
if (C.ModelClassReal) ClassToCodec[C.ModuleId + '::' + C.ModelClassReal] = Key;
|
||
}
|
||
|
||
for (const C of Object.values(Codecs)) {
|
||
const ModId = C.ModuleId;
|
||
C.ModelName = C.ModelClassReal ? ResolveClass(C.ModelClassReal, ModId, ModuleInfo, []).Name : '';
|
||
for (const F of C.Fields) {
|
||
const Raw = F.Type;
|
||
const R = ResolveType(Raw, ModId, ModuleInfo, ClassToCodec);
|
||
F.TypeRaw = Raw;
|
||
F.Type = R.Type;
|
||
if (R.Codecs.length) F.TypeCodec = R.Codecs[0];
|
||
if (F.Type !== Raw) TypeResolved++;
|
||
else if (!PRIM_WORDS.has(Raw) && !/^prim:/.test(Raw)) TypeUnresolved++;
|
||
}
|
||
}
|
||
|
||
// ---------------------------------------------------------------- 顶层段:SlotTypes(全模块「槽位 → 类型」)
|
||
// 为什么需要它:上面的 Codecs 段只收「被抽成 codec 条目的类」的字段。可下行方法(模型类的 q7i 分支)
|
||
// 读的是模型类里那些**局部 codec 实例**(chc_1 / jh4_1 / b1wd_1 …),它们由 taw() 之类的一次性初始化
|
||
// 赋成 `<槽位> = <工厂>(<接收者>).n7f(new <描述符>)`,往往压根没进 Codecs 段(那个模型没注册 feature、
|
||
// 或 codec 条目没被抽到)。于是「这个槽位是什么类型」在全库查不到,下行字段就只能报未解。
|
||
// 这里对**全部模块**扫这个形状,复用与上面完全相同的 TypeOfDescriptor + ResolveType 推类型:
|
||
// 源码证据(src/module_10283.js:2343) this.hh4_1 = Un(this).n7f(new w(v().xb(), !1))
|
||
// 源码证据(src/module_10283.js:2727) this.chc_1 = mi(this).n7f(new $(new w(v().xb(), !1), !1))
|
||
// 源码证据(src/module_29554.js:242) this.b1wd_1 = B(this).n7f(new _(o(W), !1))
|
||
// 键 = 模块::槽位:槽位名本来是「模块内唯一」,但槽位一旦被别处引用(如 q7i 分支、上行发送点)
|
||
// 就必须能定位到「哪个模块的哪个槽位」,所以把模块号写进键里,避免将来把同名槽位认错。
|
||
// (实测本包 1073 个槽位名恰好全局唯一,但那是混淆器给的下标,不该依赖。)
|
||
function ParseSlotAssignments(Src, Roles, LineOf) {
|
||
const Out = new Map();
|
||
const Conflicts = [];
|
||
const Re = /\.r7g\(/g;
|
||
let M;
|
||
while ((M = Re.exec(Src))) {
|
||
const Open = M.index + M[0].length - 1;
|
||
const Close = MatchBracket(Src, Open, '(', ')');
|
||
if (Close < 0) break;
|
||
// prettier 会把实参换行,窗口放宽到 200 字符;工厂实参允许换行但不容嵌套括号
|
||
const Win = Src.slice(Math.max(0, M.index - 200), M.index);
|
||
const Head = /(?:([A-Za-z_$][\w$]*)\.)?([a-z0-9]+_\d+)\s*=\s*([A-Za-z_$][\w$]*)\s*\([^()]{0,60}\)\s*$/.exec(Win);
|
||
if (Head) {
|
||
const Slot = Head[2];
|
||
const T = TypeOfDescriptor(Src.slice(Open + 1, Close), Roles, 0);
|
||
const Prev = Out.get(Slot);
|
||
// 同一槽位名在本模块被赋了不同描述符 → 记冲突、保留第一条(宁可少解,不可解错)
|
||
if (Prev && Prev.Desc !== T.Raw) Conflicts.push(Slot + ' → ' + Prev.Desc + ' / ' + T.Raw);
|
||
else if (!Prev) Out.set(Slot, { Slot, Desc: T.Raw, Raw: T.Type, Line: LineOf(M.index) });
|
||
}
|
||
Re.lastIndex = Close + 1;
|
||
}
|
||
return { Map: Out, Conflicts };
|
||
}
|
||
|
||
const SlotTypes = {};
|
||
const SlotTypeConflicts = [];
|
||
let SlotTypeOk = 0;
|
||
let SlotTypeBlank = 0;
|
||
const SlotTypeBySlotName = new Map(); // 槽位名 → [{Mod, Type}],只用来统计「同名槽位类型不一致」
|
||
for (const File of ModuleFiles) {
|
||
let Src;
|
||
try {
|
||
Src = fs.readFileSync(File, 'utf8');
|
||
} catch (Err) {
|
||
continue;
|
||
}
|
||
const Mod = path.basename(File, '.js');
|
||
const ModId = ModIdOf(File);
|
||
// 键里的模块名统一用「去前导零」的 module_<数字>(与 Codecs[].ModuleId 同一套写法):
|
||
// 文件名是 module_01510 而模块号是 1510,两套写法混用会让消费方(mine_downlink)查不到。
|
||
const ModKey = 'module_' + ModId;
|
||
const Roles = (ModuleInfo[ModId] || {}).Roles || {};
|
||
const Parsed = ParseSlotAssignments(Src, Roles, MakeLineIndex(Src));
|
||
for (const C of Parsed.Conflicts) SlotTypeConflicts.push(Mod + ' ' + C);
|
||
for (const [Slot, Rec] of Parsed.Map) {
|
||
const Trace = { Leftover: [] };
|
||
const R = ResolveType(Rec.Raw, ModId, ModuleInfo, ClassToCodec, Trace);
|
||
// 「推不出来就留空」:类型串里还残留没有 codec 的裸类名(Trace.Leftover)或压根没解析出形状
|
||
// ('?' / 'prim:xx')时,线上没有对应的解码器,如实留空——不要拿裸类名硬当类型。
|
||
const Ok = !!R.Type && R.Type !== '?' && R.Type !== '...' && !/^prim:/.test(R.Type) && !Trace.Leftover.length;
|
||
if (Ok) SlotTypeOk++;
|
||
else SlotTypeBlank++;
|
||
SlotTypes[ModKey + '::' + Slot] = {
|
||
Module: ModKey,
|
||
Slot,
|
||
Type: Ok ? R.Type : '',
|
||
TypeCodec: Ok ? R.Codecs[0] || '' : '',
|
||
Desc: Rec.Desc,
|
||
Line: Rec.Line,
|
||
};
|
||
if (!SlotTypeBySlotName.has(Slot)) SlotTypeBySlotName.set(Slot, []);
|
||
SlotTypeBySlotName.get(Slot).push(SlotTypes[ModKey + '::' + Slot]);
|
||
}
|
||
}
|
||
// 同名槽位在不同模块被推成不同类型 → 只按槽位名查表必然解错,这里把数量报出来当护栏
|
||
let SlotNameMismatch = 0;
|
||
for (const List of SlotTypeBySlotName.values()) {
|
||
if (List.length < 2) continue;
|
||
const First = List[0].Type;
|
||
if (List.some((X) => X.Type !== First)) SlotNameMismatch++;
|
||
}
|
||
|
||
// ---------------------------------------------------------------- 第四遍:上行命令表
|
||
// 上行命令在源码里的形态(见 module_85183 / 10283 / 47243 / 85667 …):
|
||
// ① 命令类构造函数里放着「methodId 长整型字面量」和「负载 codec」:
|
||
// function mq() {
|
||
// ((this.zl3_1 = new vi(Mn(fi), null)), (this.bl3_1 = new Xn(1724953279, 829170552)));
|
||
// } ↑ hi*2^32 + lo(无符号) = methodId
|
||
// ② 每个发送方法按顺序把负载字段写进共享缓冲,再包成 SpaceCommand 发出去:
|
||
// (p(xn).sh1 = function (t, n) {
|
||
// (this.kh1_1.o7g(this.uh0_1, t), this.lh1_1.o7g(this.uh0_1, n)); ← 负载字段(有序)
|
||
// var i = new U(spaceId, this.jh1_1, this.uh0_1), … ← SpaceCommand(objectId, methodId, buffer)
|
||
// ③ 负载字段的类型声明与下行同源:(n.kh1_1 = jn(n).n7f(new w(v().pb(), !1)))
|
||
const LongOf = (Lo, Hi) => (BigInt(Hi) * 4294967296n + BigInt(Lo >>> 0)).toString();
|
||
|
||
// 模块内「字段 → {Long, Desc}」:Long 来自两整数构造的字面量,Desc 来自 .n7f(new …)
|
||
function FindFieldInfo(Src, Roles) {
|
||
const Long = {};
|
||
const ReLong = /this\.([a-z0-9]+_\d+) = new ([A-Za-z_$][\w$]*)\((-?\d+), (-?\d+)\)/g;
|
||
let M;
|
||
while ((M = ReLong.exec(Src)))
|
||
Long[M[1]] = { Cls: M[2], Lo: Number(M[3]), Hi: Number(M[4]), Value: LongOf(Number(M[3]), Number(M[4])) };
|
||
|
||
const Desc = {};
|
||
const Re = /\.r7g\(/g;
|
||
while ((M = Re.exec(Src))) {
|
||
const Open = M.index + M[0].length - 1;
|
||
const Close = MatchBracket(Src, Open, '(', ')');
|
||
if (Close < 0) break;
|
||
const Before = Src.slice(Src.lastIndexOf('\n', M.index) + 1, M.index);
|
||
const A = /([a-z0-9]+_\d+) = [^\n]{0,60}?$/.exec(Before);
|
||
if (A && !Desc[A[1]]) Desc[A[1]] = TypeOfDescriptor(Src.slice(Open + 1, Close), Roles, 0);
|
||
Re.lastIndex = Close + 1;
|
||
}
|
||
return { Long, Desc };
|
||
}
|
||
|
||
// 找 `X(Cls).Fn = function (…) { … }` 的方法块(用于定位发送点的宿主方法与宿主类)
|
||
function FindMethods(Src) {
|
||
const Out = [];
|
||
const Re = /\(?([A-Za-z_$][\w$]*)\(([A-Za-z_$][\w$]*)\)\.([A-Za-z_$][\w$]*) = function \([^)]*\)\s*\{/g;
|
||
let M;
|
||
while ((M = Re.exec(Src))) {
|
||
const Open = Src.indexOf('{', M.index + M[0].length - 1);
|
||
const Close = MatchBracket(Src, Open, '{', '}');
|
||
if (Close < 0) break;
|
||
Out.push({ Owner: M[2], Name: M[3], BodyStart: Open + 1, End: Close });
|
||
Re.lastIndex = Close + 1;
|
||
}
|
||
return Out;
|
||
}
|
||
|
||
// 命令类构造函数里登记的 codec/model:new <Codec>(<kClass>(<Model>), null)
|
||
function OwnerModels(Src, Owner, ModId) {
|
||
if (!Owner) return [];
|
||
const At = Src.search(new RegExp('\\bfunction ' + Owner.replace(/\$/g, '\\$') + '\\('));
|
||
if (At < 0) return [];
|
||
const Open = Src.indexOf('{', At);
|
||
const Close = MatchBracket(Src, Open, '{', '}');
|
||
if (Close < 0) return [];
|
||
const Body = Src.slice(Open + 1, Close);
|
||
const Out = [];
|
||
for (const G of Body.matchAll(/\(([A-Za-z_$][\w$]*)\(([A-Za-z_$][\w$]*)\)/g)) {
|
||
if (!Out.includes(G[2])) Out.push(G[2]);
|
||
}
|
||
return Out.map((N) => ({ Local: N, Name: ResolveClass(N, ModId, ModuleInfo, []).Name }));
|
||
}
|
||
|
||
const UplinkCommands = [];
|
||
const UpSeen = new Set();
|
||
const UpSendRe = /new ([A-Za-z_$][\w$]*)\([^;]*?this\.([a-z0-9]+_\d+), this\.([a-z0-9]+_\d+)\)/g;
|
||
|
||
for (const File of ModuleFiles) {
|
||
let Src;
|
||
try {
|
||
Src = fs.readFileSync(File, 'utf8');
|
||
} catch (Err) {
|
||
continue;
|
||
}
|
||
const ModId = ModIdOf(File);
|
||
const Mod = path.basename(File, '.js');
|
||
const Roles = (ModuleInfo[ModId] || {}).Roles || {};
|
||
const Classes = (ModuleInfo[ModId] || {}).Classes || {};
|
||
const { Long, Desc } = FindFieldInfo(Src, Roles);
|
||
if (!Object.keys(Long).length) continue;
|
||
const Methods = FindMethods(Src);
|
||
|
||
let M;
|
||
UpSendRe.lastIndex = 0;
|
||
while ((M = UpSendRe.exec(Src))) {
|
||
const Inf = Long[M[2]];
|
||
if (!Inf) continue; // 第二个实参必须是本模块里的长整型字面量字段,否则不是 SpaceCommand
|
||
const At = M.index;
|
||
const Host = Methods.find((X) => At > X.BodyStart && At < X.End);
|
||
// 负载字段:宿主方法里、发送点之前的所有 this.<codec>.o7g( 接收者,按出现顺序
|
||
const Fields = [];
|
||
if (Host) {
|
||
const Head = Src.slice(Host.BodyStart, At);
|
||
for (const G of Head.matchAll(/this\.([a-z0-9]+_\d+)\.s7h\(/g)) {
|
||
const D = Desc[G[1]];
|
||
const T = D ? ResolveType(D.Type, ModId, ModuleInfo, ClassToCodec) : { Type: '?', Codecs: [] };
|
||
Fields.push({ Field: G[1], Type: T.Type, TypeCodec: T.Codecs[0] || '', Desc: D ? D.Raw : '' });
|
||
}
|
||
}
|
||
const Key = Inf.Value + '|' + Mod + '|' + (Host ? Host.Owner + '.' + Host.Name : '');
|
||
if (UpSeen.has(Key)) continue;
|
||
UpSeen.add(Key);
|
||
UplinkCommands.push({
|
||
MethodId: Inf.Value,
|
||
LongLiteral: [Inf.Lo, Inf.Hi],
|
||
Module: Mod,
|
||
OwnerClass: Host ? Host.Owner : '',
|
||
OwnerName: Host ? Classes[Host.Owner] || '' : '',
|
||
SendFn: Host ? Host.Name : '',
|
||
BufferField: M[3],
|
||
Models: OwnerModels(Src, Host ? Host.Owner : '', ModId),
|
||
Fields,
|
||
});
|
||
}
|
||
}
|
||
|
||
// ---------------------------------------------------------------- 模型注册点 → modelId ↔ codec(源码第三证据)
|
||
// 客户端把「modelId → codec」登记在 RPC 分发器的模型注册表里:src/module_61860.js:1664-1671 的
|
||
// j7n(t, n) { this.w7n_1.g3(t, n) } —— 就是 names/regdump.json 的 maps['ModelReg.w7n_1']
|
||
// 源码里的登记点形状(都是 taw() 里,三处互相印证):
|
||
// src/module_99527.js:122 this.ide_1 = new C(-841295065, 916213531) → modelId 3935107155251354407
|
||
// src/module_99527.js:357 Y(this).j7n(this.ide_1, K(this).n7f(new v(k(Z), !1))) → codec 取的是类 Z 的 codec(module_99527::N)
|
||
// src/module_85196.js:1198 this.icp_1 = new Me(-769625095, 727121853) → modelId 3122964582367261689
|
||
// src/module_85196.js:6835 na(this).j7n(this.icp_1, ta(this).n7f(new ye(ze(Q_), !1))) → codec 取类 Q_ 的 codec(module_85196::vu)
|
||
// src/module_25377.js:274 this.ldi_1 = new Q(-2144376380, 1490830385) → modelId 6403067749608679876
|
||
// src/module_25377.js:1413 sn(this).j7n(this.ldi_1, un(this).n7f(new L(B(en), !1))) → codec 取类 en 的 codec(module_25377::ft)
|
||
// 判断口径:第 1 个别名必须是 TypeCodecInfo(module_24635::e = gn)、第 2 个必须是 kClass(module_37036::mf),
|
||
// 也就是 an.n7f 那条「给某个模型类取 codec」的分支(src/module_24635.js:746-784)。长整型字面量 → modelId 用 LongOf。
|
||
function FindModelRegistrations(Src, Roles) {
|
||
const IdOf = {};
|
||
const ReId = /this\.([a-z0-9]+_\d+) = new ([A-Za-z_$][\w$]*)\((-?\d+), (-?\d+)\)/g;
|
||
let M;
|
||
while ((M = ReId.exec(Src))) IdOf[M[1]] = LongOf(Number(M[3]), Number(M[4]));
|
||
const Out = [];
|
||
if (!Object.keys(IdOf).length) return Out;
|
||
// 旧包登记函数 j7n,新包 n7o;字段工厂旧 n7f / 新 r7g
|
||
const Re = /n7o\(\s*this\.([a-z0-9]+_\d+)\s*,[\s\S]{0,90}?\.r7g\(\s*new ([A-Za-z_$][\w$]*)\(([A-Za-z_$][\w$]*)\(([A-Za-z_$][\w$]*)\)/g;
|
||
while ((M = Re.exec(Src))) {
|
||
const Id = IdOf[M[1]];
|
||
if (!Id) continue;
|
||
if ((Roles[M[2]] || '') !== 'Type' || (Roles[M[3]] || '') !== 'KClass') continue;
|
||
Out.push({ ModelId: Id, Class: M[4] });
|
||
}
|
||
return Out;
|
||
}
|
||
|
||
const SrcModelCodec = new Map(); // modelId → { CodecKey, Mod, Class }
|
||
const SrcRegConflicts = [];
|
||
for (const File of ModuleFiles) {
|
||
let Src;
|
||
try {
|
||
Src = fs.readFileSync(File, 'utf8');
|
||
} catch (Err) {
|
||
continue;
|
||
}
|
||
const ModId = ModIdOf(File);
|
||
const Roles = (ModuleInfo[ModId] || {}).Roles || {};
|
||
for (const R of FindModelRegistrations(Src, Roles)) {
|
||
const One = ResolveOne(R.Class, ModId, ModuleInfo, ClassToCodec);
|
||
if (!One.Codec) continue;
|
||
const Prev = SrcModelCodec.get(R.ModelId);
|
||
if (Prev && Prev.CodecKey !== One.Codec) SrcRegConflicts.push(R.ModelId + ' 在两个模块登记了不同 codec:' + Prev.CodecKey + ' / ' + One.Codec);
|
||
else if (!Prev) SrcModelCodec.set(R.ModelId, { CodecKey: One.Codec, Mod: ModId, Class: R.Class });
|
||
}
|
||
}
|
||
|
||
// regdump.json 的 maps['ModelReg.w7n_1'] 是同一个表的运行时 dump:modelId → codec 实例,
|
||
// 每条带 ctor(codec 类短名)与 keys(该 codec 实例自己的槽位名)。
|
||
// 指纹 = 「codec 类短名 + 槽位名集合」,拿它去本轮抽出的 Codecs 里找唯一匹配。
|
||
// 为什么需要这一步:压缩后短名会跨模块重名(如 vu/N/ft 各有多个),单看名字会绑错;
|
||
// 而槽位名(缩器按下标生成的 x_1 形式)在同一 codec 内是确定的,两者合起来才能消歧。
|
||
// 仍然多解时用「源码登记点」(FindModelRegistrations)定音——但**只允许在候选集合里挑**,
|
||
// 源码登记点落在候选之外就如实留空并列出,绝不硬绑。
|
||
function BindModelIds(Codecs, RegPath) {
|
||
const Stats = { W7nEntries: 0, ByFingerprint: 0, BySrcReg: 0, ZeroCandidate: 0, Ambiguous: 0, Bound: 0, ConflictWithJoin: 0 };
|
||
const Unresolved = [];
|
||
const Conflicts = [];
|
||
if (!fs.existsSync(RegPath)) {
|
||
Stats.Reason = '找不到 ' + RegPath;
|
||
return { Stats, Unresolved };
|
||
}
|
||
const Reg = JSON.parse(fs.readFileSync(RegPath, 'utf8'));
|
||
const Maps = Reg.maps || Reg;
|
||
const W = (Maps['ModelReg.w7n_1'] && Maps['ModelReg.w7n_1'].entries) || [];
|
||
const ByFingerprint = new Map(); // 短名|排序后的槽位名 → [codec 键]
|
||
for (const [Key, C] of Object.entries(Codecs)) {
|
||
const Sig = C.Codec + '|' + (C.Slots || []).map((S) => S.Slot).slice().sort().join(',');
|
||
if (!ByFingerprint.has(Sig)) ByFingerprint.set(Sig, []);
|
||
ByFingerprint.get(Sig).push(Key);
|
||
}
|
||
for (const E of W) {
|
||
const ModelId = String(E.key);
|
||
Stats.W7nEntries++;
|
||
const Sig = String(E.ctor || '') + '|' + (E.keys || []).slice().sort().join(',');
|
||
const Cand = ByFingerprint.get(Sig) || [];
|
||
const Src = SrcModelCodec.get(ModelId);
|
||
let Key = '';
|
||
let How = '';
|
||
if (Cand.length === 1) {
|
||
Key = Cand[0];
|
||
How = '唯一指纹';
|
||
} else if (Cand.length > 1) {
|
||
if (Src && Cand.includes(Src.CodecKey)) {
|
||
Key = Src.CodecKey;
|
||
How = '指纹命中 ' + Cand.length + ' 个,源码登记点(' + Src.Mod + '::' + Src.Class + ')定音';
|
||
} else {
|
||
Stats.Ambiguous++;
|
||
Unresolved.push(ModelId + ':指纹 ' + Sig + ' 命中 ' + Cand.length + ' 个 [' + Cand.join(', ') + ']' +
|
||
(Src ? ',源码登记点 ' + Src.CodecKey + ' 不在候选里' : ',源码里没有登记点'));
|
||
continue;
|
||
}
|
||
} else {
|
||
if (Src) {
|
||
Key = Src.CodecKey;
|
||
How = '指纹 0 命中,源码登记点(' + Src.Mod + '::' + Src.Class + ')';
|
||
} else {
|
||
Stats.ZeroCandidate++;
|
||
Unresolved.push(ModelId + ':指纹 ' + Sig + ' 在 Codecs 里 0 命中,源码里也没有登记点');
|
||
continue;
|
||
}
|
||
}
|
||
if (How === '唯一指纹') Stats.ByFingerprint++;
|
||
else Stats.BySrcReg++;
|
||
const C = Codecs[Key];
|
||
if (!C.ModelIds) C.ModelIds = [];
|
||
if (!C.ModelIds.includes(ModelId)) C.ModelIds.push(ModelId);
|
||
if (!C.ModelHow) C.ModelHow = How;
|
||
if (C.ModelId && C.ModelId !== ModelId && !C.ModelIds.includes(C.ModelId)) {
|
||
// 旧 join(槽位签名 / 构造器名兜底)给的是另一个 modelId:不是错,但要如实报出来
|
||
Stats.ConflictWithJoin++;
|
||
Conflicts.push(Key + ' 旧 ModelId=' + C.ModelId + ',新绑定=' + ModelId + '(两个都会进 CodecByModelId)');
|
||
}
|
||
Stats.Bound++;
|
||
}
|
||
return { Stats, Unresolved, Conflicts };
|
||
}
|
||
|
||
// ---------------------------------------------------------------- 写出与统计
|
||
const RegPath = path.resolve(Arg('reg', path.join(PROJECT, 'names', 'regdump.json')));
|
||
const Join = JoinRegistries(Codecs, RegPath);
|
||
const ModelBind = BindModelIds(Codecs, RegPath);
|
||
|
||
const AllFields = Object.values(Codecs).reduce((A, C) => A.concat(C.Fields), []);
|
||
const Named = AllFields.filter((F) => F.Name !== '(匿名)' && F.Name !== '?');
|
||
const Unnamed = AllFields.filter((F) => F.Name === '(匿名)');
|
||
const Unknown = Named.filter((F) => F.Name === '?');
|
||
const TypeCount = {};
|
||
for (const F of AllFields) TypeCount[F.Type] = (TypeCount[F.Type] || 0) + 1;
|
||
|
||
const Out = {
|
||
Note: '空间层 codec 字段表(键 = 模块::codec变量名)。字段名来自 v("codec_名字");类型来自 q7g 描述符(已把裸局部类名还原成 Kotlin 原名,TypeRaw 保留原名);Js= p7g 里的 JS 级线索;Prop= 数据类属性名;TypeCodec= 该嵌套模型对应的 codec 键;ModelId/MethodId 来自 regdump.json join;ModelIds= 同一个 codec 被多个 modelId 共用时的完整列表(regdump w7n_1 指纹绑定,见 ModelIdBinding)',
|
||
SlotTypesNote:
|
||
'SlotTypes:全模块「槽位 → 类型」表,键 = 模块::槽位(如 module_10283::jh4_1)。来源是 `<接收者>.<槽位> = <工厂>(<接收者>).n7f(new <描述符>)` 的构造赋值(q7g 之外的局部 codec 实例),' +
|
||
'供 mine_downlink.js 解 q7i 分支里读到的槽位;Type 推不出来时留空字符串(不编),TypeCodec 为空表示该类型是基础类型/线上基础类型,不需要嵌套 codec',
|
||
Scan,
|
||
Modules: ModuleFiles.length,
|
||
ModulesWithCodec: ModulesUsed,
|
||
CodecCount: Object.keys(Codecs).length,
|
||
FieldCount: AllFields.length,
|
||
NamedCount: Named.length,
|
||
AnonymousCount: Unnamed.length,
|
||
WireClasses,
|
||
PrimClassWire,
|
||
ResourceClasses,
|
||
SlotTypes,
|
||
SlotTypeStats: {
|
||
Count: Object.keys(SlotTypes).length,
|
||
Typed: SlotTypeOk,
|
||
Blank: SlotTypeBlank,
|
||
DescConflicts: SlotTypeConflicts.length,
|
||
NameMismatch: SlotNameMismatch,
|
||
},
|
||
Join,
|
||
ModelIdBinding: {
|
||
Note:
|
||
'ModelId → codec 的权威绑定。主证据 = names/regdump.json 的 maps["ModelReg.w7n_1"](modelId → codec 实例:' +
|
||
'ctor = codec 类短名、keys = 该 codec 实例的槽位名),指纹 = 「短名 + 槽位名集合」,在本表 Codecs 里唯一命中即绑定;' +
|
||
'若指纹多解,用源码登记点(`j7n(<id 槽位>, X(this).n7f(new TypeCodecInfo(kClass(类), !1)))`,见 extract_codec_fields.js ' +
|
||
'的 FindModelRegistrations)在候选集合内定音;多解且定不了 / 零命中且源码也没有登记点 → 如实留空并列在 Unresolved,绝不硬绑。' +
|
||
'ByFingerprint = 靠唯一指纹绑上的条数,BySrcReg = 靠源码登记点绑上(含指纹 0 命中)的条数。',
|
||
SrcRegistrations: SrcModelCodec.size,
|
||
SrcRegConflicts,
|
||
...ModelBind.Stats,
|
||
Unresolved: ModelBind.Unresolved,
|
||
ConflictsWithJoin: ModelBind.Conflicts || [],
|
||
},
|
||
Codecs,
|
||
};
|
||
fs.writeFileSync(OutPath, JSON.stringify(Out, null, 1), 'utf8');
|
||
|
||
// 上行命令表单独落盘(names/uplink_table.json)
|
||
const UpOutPath = path.resolve(Arg('up-out', path.join(PROJECT, 'names', 'uplink_table.json')));
|
||
const UpOut = {
|
||
Note:
|
||
'上行命令表(methodId → 命令类 / 发送方法 / 负载字段)。methodId = hi*2^32 + (lo>>>0),由源码里的 Long 字面量还原。' +
|
||
'字段只有 Kotlin 局部 slot 名(如 bcq_1)+ 类型:命令类不像模型 codec 那样保留 codec_<名字> getter,' +
|
||
'555 个字段全都没有属性访问点,压缩后已无原可读名可挖(要可读名只能人工标注或靠调用点语义推断)',
|
||
Count: UplinkCommands.length,
|
||
Commands: UplinkCommands.sort((A, B) => (BigInt(A.MethodId) < BigInt(B.MethodId) ? -1 : 1)),
|
||
};
|
||
fs.writeFileSync(UpOutPath, JSON.stringify(UpOut, null, 1), 'utf8');
|
||
|
||
console.log('扫描模块 ' + ModuleFiles.length + ' 个(含 codec ' + ModulesUsed + ' 个)');
|
||
console.log('codec ' + Object.keys(Codecs).length + ' 个;字段 ' + AllFields.length + ' 条(有名 ' + Named.length + ',匿名 ' + Unnamed.length + ',未解 ' + Unknown.length + ')');
|
||
console.log(
|
||
'slot 类型表: ' + Out.SlotTypeStats.Count + ' 个(推出 ' + SlotTypeOk + ',留空 ' + SlotTypeBlank +
|
||
');同槽位描述符冲突 ' + SlotTypeConflicts.length + ',同名槽位类型不一致 ' + SlotNameMismatch + ' 处(故本表按 模块::槽位 索引)'
|
||
);
|
||
if (SlotTypeConflicts.length) for (const X of SlotTypeConflicts.slice(0, 3)) console.log(' ⚠ ' + X);
|
||
console.log('已写出 ' + OutPath);
|
||
console.log('协议核心类(线上基础类型): ' + JSON.stringify(WireClasses));
|
||
console.log('Kolib 原始类型类(模块::导出键 → 线上类型): ' + JSON.stringify(PrimClassWire));
|
||
console.log('资源类(u83 注册块导出 → 线上 i64): ' + Object.keys(ResourceClasses).length + ' 个 — ' +
|
||
Object.entries(ResourceClasses).map(([N, R]) => N + '(typeId ' + R.TypeId + ')').join(' '));
|
||
console.log('类型分布: ' + Object.entries(TypeCount).sort((A, B) => B[1] - A[1]).slice(0, 18).map(([K, V]) => K + '=' + V).join(' '));
|
||
if (Join.Ok)
|
||
console.log(
|
||
'注册表 join: codec 表 ' + Join.CodecEntries + ' 条 / 方法表 ' + Join.MethodEntries + ' 条;对上 modelId ' +
|
||
Join.Count.ModelId + ' 个 codec(槽位签名 ' + Join.BySigHit + ' + 构造器名兜底 ' + Join.ByCtorHit +
|
||
'),其中能反查到 methodId 的 ' + Join.Count.MethodId + ' 个' +
|
||
(Join.Conflicts.length ? ' ⚠ 冲突 ' + Join.Conflicts.length + ' 处: ' + Join.Conflicts.slice(0, 3).join('; ') : ' ✓ 无冲突')
|
||
);
|
||
else console.log('注册表 join: 跳过(' + Join.Reason + ')');
|
||
|
||
// modelId → codec 绑定(regdump w7n_1 指纹 + 源码登记点定音)报告
|
||
const MB = Out.ModelIdBinding;
|
||
if (MB.Reason) console.log('modelId 绑定: 跳过(' + MB.Reason + ')');
|
||
else {
|
||
console.log(
|
||
'modelId → codec 绑定: w7n_1 ' + MB.W7nEntries + ' 条;绑上 ' + MB.Bound + ' 条(唯一指纹 ' + MB.ByFingerprint +
|
||
' + 源码登记点定音 ' + MB.BySrcReg + ');仍绑不上 ' + (MB.Ambiguous + MB.ZeroCandidate) +
|
||
'(指纹多解 ' + MB.Ambiguous + ',零命中 ' + MB.ZeroCandidate + ')'
|
||
);
|
||
console.log(' 源码登记点(taw 里的 j7n(id, codec))共 ' + MB.SrcRegistrations + ' 个 modelId' +
|
||
(MB.SrcRegConflicts.length ? ';⚠ 同 id 两处登记不同 codec ' + MB.SrcRegConflicts.length + ' 处' : ';无冲突') +
|
||
(MB.ConflictWithJoin ? ';与旧 join 给的 ModelId 不一致 ' + MB.ConflictWithJoin + ' 处' : ''));
|
||
for (const U of MB.Unresolved.slice(0, 8)) console.log(' ⚠ 未绑定 ' + U);
|
||
}
|
||
|
||
// 自检:字段的原始描述符里若还残留未识别形状(既不是基础类型也不是 X(Y) 也不是 new X(...)),列出来看
|
||
const Suspicious = new Set();
|
||
for (const K of Object.keys(Codecs)) {
|
||
for (const F of Codecs[K].Fields) {
|
||
const D = (F.Desc || '').replace(/^new\s+/, '');
|
||
if (!D) continue;
|
||
if (/^[A-Za-z_$][\w$]*\(\)\.[A-Za-z0-9_$]+\(\)$/.test(D)) continue; // X().pb()
|
||
if (/^[A-Za-z_$][\w$]*\([\s\S]*\)$/.test(D)) continue; // X(...)
|
||
Suspicious.add(F.Desc);
|
||
}
|
||
}
|
||
console.log('未识别描述符形状: ' + (Suspicious.size ? [...Suspicious].slice(0, 8).join(' | ') : '无'));
|
||
console.log('类型裸类名还原: 成功 ' + TypeResolved + ' 条,仍为裸名 ' + TypeUnresolved + ' 条(多半是没有 toString 的非 data class 资源类)');
|
||
const NamedCodecs = Object.values(Codecs).filter((C) => C.ModelName).length;
|
||
const Refed = AllFields.filter((F) => F.TypeCodec).length;
|
||
console.log('codec 的模型类真名: ' + NamedCodecs + ' / ' + Object.keys(Codecs).length + ' 个;字段里带嵌套 codec 链接的 ' + Refed + ' 条');
|
||
const UpWithFields = UplinkCommands.filter((C) => C.Fields.length).length;
|
||
console.log('上行命令表: ' + UplinkCommands.length + ' 条(带负载字段的 ' + UpWithFields + ' 条)→ ' + UpOutPath);
|
||
|
||
// --find-up <methodId>:查一条上行命令的负载字段
|
||
const FindUp = Arg('find-up', '');
|
||
if (FindUp) {
|
||
const Hit = UplinkCommands.filter((C) => C.MethodId === FindUp);
|
||
if (!Hit.length) console.log('\n上行 methodId ' + FindUp + ' 未在源码里找到');
|
||
for (const C of Hit) {
|
||
console.log('\n上行 ' + C.MethodId + ' [' + C.LongLiteral.join(', ') + '] ' + C.Module + ' ' + C.OwnerClass + '(' + (C.OwnerName || '?') + ').' + C.SendFn);
|
||
if (C.Models.length) console.log(' 命令类登记的模型: ' + C.Models.map((X) => X.Local + (X.Name && X.Name !== X.Local ? '(' + X.Name + ')' : '')).join(', '));
|
||
for (const [I, F] of C.Fields.entries()) console.log(' ' + (I + 1) + '. ' + F.Type.padEnd(28) + ' ' + F.Field + ' ' + F.Desc);
|
||
if (!C.Fields.length) console.log(' (负载为空)');
|
||
}
|
||
}
|
||
|
||
const Show = Arg('show', 'st,ft,ht,dt,V').split(',');
|
||
for (const C of Show) {
|
||
const Hit = Object.keys(Codecs).filter((K) => K === C || K.endsWith('::' + C) || K.includes('::' + C));
|
||
for (const K of Hit.slice(0, 4)) {
|
||
const Cd = Codecs[K];
|
||
const Ids = [
|
||
Cd.ModelName ? '模型=' + Cd.ModelName : '',
|
||
Cd.ModelId ? 'modelId=' + Cd.ModelId : '',
|
||
Cd.ModelClass ? 'modelClass=' + Cd.ModelClass : '',
|
||
Cd.MethodId && Cd.MethodId.length ? 'methodId=' + Cd.MethodId.join(',') : '',
|
||
].filter(Boolean).join(' ');
|
||
console.log('\n=== codec ' + K + '(' + Cd.Fields.length + ' 字段)' + (Ids ? ' ' + Ids : '') + ' ===');
|
||
for (const F of Cd.Fields) {
|
||
console.log(
|
||
' ' + String(F.Order).padStart(2) + '. ' + String(F.Name).padEnd(32) + ' ' + String(F.Type).padEnd(16) +
|
||
' js=' + String(F.Js || '-').padEnd(10) + ' ' + F.Slot + ' ' + (F.Desc || '')
|
||
);
|
||
}
|
||
}
|
||
}
|
||
|
||
// --find-model <modelId> / --find-method <methodId>:反查 codec 并打印字段表
|
||
for (const [Opt, Field, Label] of [
|
||
['find-model', 'ModelId', 'modelId'],
|
||
['find-method', 'MethodId', 'methodId'],
|
||
]) {
|
||
const Id = Arg(Opt, '');
|
||
if (!Id) continue;
|
||
const Hit = Object.entries(Codecs).filter(([, C]) => {
|
||
const V = C[Field];
|
||
if (Array.isArray(V) ? V.includes(Id) : V === Id) return true;
|
||
// modelId 可能只出现在新绑定(ModelIds)里:一个 codec 被多个 modelId 共用时 ModelId 只留了一个
|
||
return Field === 'ModelId' && Array.isArray(C.ModelIds) && C.ModelIds.includes(Id);
|
||
});
|
||
console.log('\n' + Label + ' = ' + Id + ' → ' + (Hit.length ? Hit.map(([K, C]) => K + '(' + C.Codec + ', ' + C.Fields.length + ' 字段)').join(' ') : '无匹配'));
|
||
for (const [K, C] of Hit.slice(0, 2)) {
|
||
for (const F of C.Fields) console.log(' ' + String(F.Order).padStart(2) + '. ' + String(F.Name).padEnd(30) + ' ' + F.Type);
|
||
}
|
||
}
|