- wss_server:识别 c2s 空间命令([nullmap][objectId i64][methodId i64][负载])并逐字段解码 - 应答签名改用 S:<methodId>(pair_flow.js 同步),fake 模式能按具体上行命令触发对应下行回放 - extract_codec_fields:新增「协议核心类 → 线上基础类型」挖掘(WireClasses,xr/R → i64) - lib_space_codec:按模型真名/核心类换算兜底(ji → i64、xr → i64),抽成上下行共享库 - decode_space_frames:记录容器(_t/lt)已核实结构与未闭合环节 - README:补「协议逆向与离线模拟」章节(帧格式/字段编码/命令表/fake 回放/容器现状) 实测:297 条上行记录 100% 完整解出;fake 模式按 G.q1wd、ls.xbl 等命令精确触发应答
899 lines
38 KiB
JavaScript
899 lines
38 KiB
JavaScript
// extract_codec_fields.js - 从空间层 codec 模块里抽出「字段名 + 类型 + 线上顺序」
|
||
// 用法:
|
||
// node extract_codec_fields.js --scan // 扫 src/ 全部模块(推荐)
|
||
// node extract_codec_fields.js --module ../src/module_54199.js
|
||
// node extract_codec_fields.js --scan --show st,ft // 打印指定 codec 的字段表
|
||
// node extract_codec_fields.js --scan --json // 额外打印 JSON 片段
|
||
//
|
||
// 原理(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
|
||
// 产物: names/codec_fields.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 Scan = Argv.indexOf('--scan') >= 0;
|
||
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(Arg('module', path.join(SrcDir, 'module_54199.js')))];
|
||
|
||
// ---------------------------------------------------------------- 通用工具
|
||
// 配平扫描:从 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;
|
||
}
|
||
|
||
// ---------------------------------------------------------------- 类型解析
|
||
// 基础类型注册表的访问器 → 可读类型名
|
||
// 来源: 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 的读取器)
|
||
const ReaderType = {
|
||
xy: 'i8',
|
||
n2u: 'i8',
|
||
s2t: 'i16',
|
||
o2u: 'i16',
|
||
t2t: 'i32',
|
||
p2u: 'i32',
|
||
u2t: 'i64',
|
||
q2u: 'i64',
|
||
v2t: 'f32',
|
||
r2u: 'f32',
|
||
w2t: 'f64',
|
||
s2u: 'f64',
|
||
};
|
||
|
||
// 类型描述符表达式 → {Type, Raw}
|
||
// 描述符类只在别处定义,各模块用局部别名引用,因此先按「角色」识别别名(见 FindRoles)。
|
||
// 角色 List → ListCodecInfo(element, optional) → list<...>
|
||
// 角色 Enum → EnumCodecInfo(type, optional) → enum<...>
|
||
// 角色 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 {
|
||
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) 枚举
|
||
// 基础类型注册表与 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',
|
||
[KOLIB_MODULE + '::w4']: 'Prim',
|
||
[KOLIB_MODULE + '::mf']: '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 = {};
|
||
const Re =
|
||
/function ([A-Za-z_$][\w$]*)\(\) \{\s*return \(\(([A-Za-z_$][\w$]*) = [A-Za-z_$][\w$]*\(d\(([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 = [];
|
||
const Re = /this\.([a-z0-9]+_\d+) = t\.n7f\(/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 = [];
|
||
const Re = /([A-Za-z_$][\w$]*)\(this\)\.p7g\(t\)|t\.a7g_1\.([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'])
|
||
);
|
||
|
||
// 返回 { 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 的模型真名;拿不到就退回 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: '' };
|
||
}
|
||
|
||
// 类型串 → { Type, Codecs };把里面的裸类名逐个替换掉
|
||
function ResolveType(Type, ModId, ModuleInfo, ClassToCodec) {
|
||
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);
|
||
// 协议核心类没有 codec 时按「线上基础类型」换算(如实体引用 xr → i64),否则保留类名
|
||
else if (WireClasses[R.Name]) return WireClasses[R.Name];
|
||
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 = {};
|
||
for (const File of ModuleFiles) {
|
||
let Src;
|
||
try {
|
||
Src = fs.readFileSync(File, 'utf8');
|
||
} catch (Err) {
|
||
continue;
|
||
}
|
||
const Roles = (ModuleInfo[ModIdOf(File)] || {}).Roles || {};
|
||
const ByCodec = new Map(FindBlocks(Src, 'o7g').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 && !WireClasses[M[2]]) WireClasses[M[2]] = Type;
|
||
}
|
||
}
|
||
|
||
// ---------------------------------------------------------------- 第二遍:抽 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, 'q7g');
|
||
const OrderBlocks = FindBlocks(Src, 'p7g');
|
||
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 里的 instanceof(直接是数据类),其次看 p7g 用的工厂函数(可能是 Ut → Ft)
|
||
const O7g = FindBlocks(Src, 'o7g').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++;
|
||
}
|
||
}
|
||
|
||
// ---------------------------------------------------------------- 第四遍:上行命令表
|
||
// 上行命令在源码里的形态(见 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 = /\.n7f\(/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+)\.o7g\(/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,
|
||
});
|
||
}
|
||
}
|
||
|
||
// ---------------------------------------------------------------- 写出与统计
|
||
const RegPath = path.resolve(Arg('reg', path.join(PROJECT, 'names', 'regdump.json')));
|
||
const Join = JoinRegistries(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',
|
||
Scan,
|
||
Modules: ModuleFiles.length,
|
||
ModulesWithCodec: ModulesUsed,
|
||
CodecCount: Object.keys(Codecs).length,
|
||
FieldCount: AllFields.length,
|
||
NamedCount: Named.length,
|
||
AnonymousCount: Unnamed.length,
|
||
WireClasses,
|
||
Join,
|
||
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('已写出 ' + OutPath);
|
||
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 + ')');
|
||
|
||
// 自检:字段的原始描述符里若还残留未识别形状(既不是基础类型也不是 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];
|
||
return Array.isArray(V) ? V.includes(Id) : V === 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);
|
||
}
|
||
}
|