// 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 // 反查 methodId 的字段表 // // 原理(Kotlin/JS 协议 codec 的三段式结构;codec 变量名只在本模块内唯一,故键 = 模块::变量名): // ① 字段名 getter: function G(t) { var n = t.; if (null != n) return n; v("codec_<字段名>"); } // —— 报错字符串就是字段名(Kotlin 惰性属性名的兜底提示) // ② (X(Codec).q7g = function (t) { this. = t.n7f(new <描述符>(...)); ... }) // —— slot → 类型描述符。首参以 "new " 开头 → 该头是「可选」包装;否则是「必填」包装; // b(X) → 列表;w().pb() → bool;w().xb() → string // ③ (X(Codec).p7g = function (t) { ... G(this).p7g(t) ... t.a7g_1.u() ... }) // —— 调用次序 = 线上字段次序;匿名 t.a7g_1.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($_). = function () { return this.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 // 角色 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 各自按自己的描述符编码。 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; } // 本模块导出的类:$_$. = <类名> 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; } // 局部别名 → 引用的是哪个模块的哪个导出:<别名> = <形参>.$_$. // 注意:别名可以是 `$`,所以不能用 \b($ 不是单词字符),改用否定后顾 function FindAliases(Src, ParamToMod) { const Out = {}; const Re = /(?.()(旧包固定 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('(? // 所以 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(/(?, 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(, 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 ((), 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..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(, 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 :查一条上行命令的负载字段 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 / --find-method :反查 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); } }