// rebuild_codec_fields.js - 用源码里的 u7h/t7h 重建 codec 字段表(含「没被注册成顶层模型」的嵌套类型) // // 为什么需要它: // names/regdump_model.json 只能拿到「运行时注册过 modelId 的 codec」的 t7h; // 像 ParticleEffectItem / ParticleEffectsEntity / LightingSFXEntity 这类只作为别的模型字段出现的 // 嵌套类型,运行时 dump 里没有,字段表只能靠离线抽取。抽取一旦把工厂角色(List/Type/Enum/Map) // 或 opt 标志认错,解码就会静默错位。 // // 本工具的做法(全部以源码为准,不猜): // 1. 定位 `(<类名>).u7h = function (t) {...}` 与 `(<类名>).t7h = function (t) {...}`; // 2. u7h 里逐条 `this.<槽位> = t.r7g(new <工厂>(...))` 拿到「槽位 → 类型描述符表达式」; // 3. t7h 里按出现顺序收集两类读操作: // t.e7h_1.<读取器>() → 基础类型(见 ReaderType) // <访问器>(this).t7h(t) → 槽位(访问器定义形如 function X(t){var n=t.<槽位>;...}) // 4. 用模块内的别名角色(24635::b=Enum、c=List、d=Map、e=Type、37036::nf=KClass、w4=Prim) // 把槽位的描述符表达式翻成 list<>/enum<>/map<>/opt<>; // 5. 与 names/codec_fields.json 里的 Type 逐字段比对,打印差异;加 --apply 才写回。 // // 用法: // node tools/rebuild_codec_fields.js # 只报告差异 // node tools/rebuild_codec_fields.js --apply # 把可确定的类型写回 codec_fields.json // node tools/rebuild_codec_fields.js --only 85183 # 只看某个模块 'use strict'; const fs = require('fs'); const path = require('path'); const PROJECT = path.resolve(__dirname, '..'); const NAMES = path.join(PROJECT, 'names'); const SRC = path.join(PROJECT, 'src'); 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 Apply = Argv.indexOf('--apply') >= 0; const Only = Arg('only', ''); // ---------------------------------------------------------------- 角色与基础类型表 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', [KOLIB_MODULE + '::mf']: 'KClass', [KOLIB_MODULE + '::nf']: 'KClass', }; 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; const ReaderType = { xy: 'i8', p2u: 'i8', q2u: 'i16', r2u: 'i32', s2u: 'i64', t2u: 'f32', u2u: 'f64', }; // ---------------------------------------------------------------- 源码小工具 function MatchBrace(Src, Start) { let Depth = 0; for (let I = Start; I < Src.length; I++) { if (Src[I] === '{') Depth++; else if (Src[I] === '}') { Depth--; if (Depth === 0) return I; } } return -1; } function MatchParen(Src, Start) { let Depth = 0; for (let I = Start; I < Src.length; I++) { if (Src[I] === '(') Depth++; else if (Src[I] === ')') { Depth--; if (Depth === 0) return I; } } return -1; } // 顶层逗号切分(忽略括号/引号内的逗号) function SplitArgs(Text) { const Out = []; let Depth = 0; let Cur = ''; for (let I = 0; I < Text.length; I++) { const C = Text[I]; if (C === '(' || C === '[') Depth++; else if (C === ')' || C === ']') Depth--; if (C === ',' && Depth === 0) { Out.push(Cur.trim()); Cur = ''; continue; } Cur += C; } if (Cur.trim()) Out.push(Cur.trim()); return Out; } function FindDeps(Src) { const Head = /\(\s*function\s*\(([^)]*)\)\s*\{/.exec(Src); const Tails = Src.match(/\}\)\(([^;]*)\);\s*$/); if (!Head || !Tails) return {}; const Params = SplitArgs(Head[1]); const Args = SplitArgs(Tails[1]); const Out = {}; for (let I = 0; I < Params.length && I < Args.length; I++) { const M = /\(\s*(\d+)\s*\)/.exec(Args[I]); if (M) Out[Params[I]] = String(Number(M[1])); } return Out; } function FindAliases(Src, Deps) { const Out = {}; const Re = /(?; if (null != n) return n; ... }`)→ 槽位 function FindAccessors(Src) { const Out = {}; const Re = /function ([A-Za-z_$][\w$]*)\(t\) \{\s*var [A-Za-z_$][\w$]* = t\.([A-Za-z0-9_$]+);\s*if \(null != [A-Za-z_$][\w$]*\) return/g; let M; while ((M = Re.exec(Src))) if (!Out[M[1]]) Out[M[1]] = M[2]; return Out; } // `(<类>).u7h = function (...) {...}` 的函数体 function ClassMethod(Src, Cls, Kind) { const Head = new RegExp('\\(\\s*' + Cls.replace(/\$/g, '\\$') + '\\s*\\)\\.' + Kind + '\\s*=\\s*function\\s*\\([^)]*\\)\\s*\\{'); const M = Head.exec(Src); if (!M) return null; const Start = M.index + M[0].length - 1; const End = MatchBrace(Src, Start); if (End < 0) return null; return Src.slice(Start, End + 1); } // u7h:槽位 → 描述符表达式(t.r7g(...) 的实参) function ParseSlots(Body) { const Out = new Map(); const Re = /this\.([A-Za-z0-9_$]+)\s*=\s*t\.r7g\(/g; let M; while ((M = Re.exec(Body))) { const Open = Body.indexOf('(', M.index + M[0].length - 1); const Close = MatchParen(Body, Open); if (Close < 0) continue; Out.set(M[1], Body.slice(Open + 1, Close).trim()); } return Out; } // t7h:按出现顺序的读步骤 function ParseReads(Body, Accessors) { const Out = []; const Re = /t\.e7h_1\.([A-Za-z0-9_$]+)\(\)|([A-Za-z_$][\w$]*)\(this\)\.t7h\(t\)/g; let M; while ((M = Re.exec(Body))) { if (M[1]) { const T = ReaderType[M[1]]; Out.push({ Kind: 'prim', Reader: M[1], Type: T || null }); } else { Out.push({ Kind: 'slot', Accessor: M[2], Slot: Accessors[M[2]] || null }); } } return Out; } // 描述符表达式 → 类型串(认不出就返回 null,保持原样不动) function Describe(Expr, Roles, Depth) { if (!Expr || Depth > 6) return null; const Raw = Expr.trim().replace(/^new\s+/, ''); const Prim = /^([A-Za-z_$][\w$]*)\(\)\.([A-Za-z0-9_$]+)\(\)$/.exec(Raw); if (Prim) return PrimName(Prim[2]); const M = /^([A-Za-z_$][\w$]*)\(([\s\S]*)\)$/.exec(Raw); if (!M) return null; // 裸类名:交给调用方保留原值 const Role = Roles[M[1]]; const Args = SplitArgs(M[2]); const Optional = Args.some((A) => A === '!0'); const Inner = Args[0] ? Describe(Args[0], Roles, Depth + 1) : null; let Base = null; if (Role === 'List') Base = 'list<' + (Inner || '?') + '>'; else if (Role === 'Map') { const V = Args[1] ? Describe(Args[1], Roles, Depth + 1) : null; Base = 'map<' + (Inner || '?') + ', ' + (V || '?') + '>'; } else if (Role === 'Enum') Base = 'enum<' + (Inner || '?') + '>'; else if (Role === 'Type' || Role === 'KClass') Base = Inner; else if (Role === 'Prim') return null; else return null; if (Base === null) return null; return Optional ? 'opt<' + Base + '>' : Base; } // ---------------------------------------------------------------- 主流程 const Files = fs.readdirSync(SRC).filter((F) => /^module_\d+\.js$/.test(F)); const ModCache = new Map(); function LoadMod(Mod) { if (ModCache.has(Mod)) return ModCache.get(Mod); const File = path.join(SRC, Mod + '.js'); if (!fs.existsSync(File)) return null; const Src = fs.readFileSync(File, 'utf8'); const Deps = FindDeps(Src); const Ctx = { Src, Roles: FindRoles(Src, Deps), Accessors: FindAccessors(Src) }; ModCache.set(Mod, Ctx); return Ctx; } const FieldsPath = path.join(NAMES, 'codec_fields.json'); const CodecFields = JSON.parse(fs.readFileSync(FieldsPath, 'utf8')); const Codecs = CodecFields.Codecs || {}; const Diffs = []; const Changed = []; let Checked = 0; let NoSource = 0; for (const [Key, C] of Object.entries(Codecs)) { const Mod = C.Module; if (Only && !String(Mod).includes(Only)) continue; if (!Mod || !C.Codec) continue; const Ctx = LoadMod(Mod); if (!Ctx) { NoSource++; continue; } const U7h = ClassMethod(Ctx.Src, C.Codec, 'u7h'); const T7h = ClassMethod(Ctx.Src, C.Codec, 't7h'); if (!T7h) { NoSource++; continue; } const Slots = U7h ? ParseSlots(U7h) : new Map(); const Reads = ParseReads(T7h, Ctx.Accessors); const Fields = C.Fields || []; Checked++; // 读步骤必须和字段表一一对应;对不上说明字段表结构本身就不对,单独报出来 if (Reads.length !== Fields.length) { Diffs.push({ Key, Kind: '结构', Detail: 't7h 读 ' + Reads.length + ' 次,字段表 ' + Fields.length + ' 个:' + Reads.map((R) => R.Kind === 'prim' ? (R.Reader + (R.Type ? '=' + R.Type : '')) : ('slot:' + (R.Slot || R.Accessor))).join(', '), }); continue; } let Dirty = false; const NewFields = Fields.map((F, I) => { const R = Reads[I]; let Type = null; if (R.Kind === 'prim') { Type = R.Type; } else { const Slot = R.Slot || F.Slot; const Expr = Slot ? Slots.get(Slot) : null; if (Expr) Type = Describe(Expr, Ctx.Roles, 0); } if (!Type) return F; if (Type !== F.Type) { Dirty = true; Diffs.push({ Key, Kind: '类型', Detail: '第 ' + (I + 1) + ' 个(' + (F.Name || '匿名') + '):' + F.Type + ' → ' + Type }); return Object.assign({}, F, { Type }); } return F; }); if (Dirty) { Changed.push(Key); if (Apply) { C.Fields = NewFields; C.RebuiltFrom = 'u7h/t7h 源码(rebuild_codec_fields.js)'; } } } console.log('字段表重建核对(源码 u7h/t7h)'); console.log(' codec ' + Object.keys(Codecs).length + ',核对 ' + Checked + ',无源码/无 t7h ' + NoSource + ',有差异 ' + Diffs.length + '(涉及 ' + Changed.length + ' 个 codec)'); const ByKind = {}; for (const D of Diffs) ByKind[D.Kind] = (ByKind[D.Kind] || 0) + 1; console.log(' 差异类型:' + JSON.stringify(ByKind)); for (const D of Diffs.slice(0, 40)) console.log(' ' + D.Key + ' [' + D.Kind + '] ' + D.Detail); if (Diffs.length > 40) console.log(' …还有 ' + (Diffs.length - 40) + ' 条'); if (Apply && Changed.length) { fs.writeFileSync(FieldsPath, JSON.stringify(CodecFields, null, 1), 'utf8'); console.log(' 已写回 ' + FieldsPath + '(' + Changed.length + ' 个 codec)'); } else if (Changed.length) { console.log(' 未写回;加 --apply 生效'); }