Files
TankIC/3DTank/tools/extract_codec_fields.js
T
TermiNexus 2ef424929b fix(protocol): 适配 e98d0216 版编解码 API 并重生成协议表
- 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 上行命令全量有类型
2026-10-21 00:03:07 +08:00

1360 lines
65 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
// 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);
}
}