From beb1d3f917f23d97cf621316e86839de9fb89bcc Mon Sep 17 00:00:00 2001 From: TermiNexus Date: Wed, 21 Oct 2026 02:46:28 +0800 Subject: [PATCH] =?UTF-8?q?feat(tools):=20=E7=94=A8=E6=BA=90=E7=A0=81=20u7?= =?UTF-8?q?h/t7h=20=E6=A0=B8=E5=AF=B9=E5=85=A8=E9=83=A8=20codec=20?= =?UTF-8?q?=E5=AD=97=E6=AE=B5=E8=A1=A8?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- 3DTank/tools/rebuild_codec_fields.js | 312 +++++++++++++++++++++++++++ 1 file changed, 312 insertions(+) create mode 100644 3DTank/tools/rebuild_codec_fields.js diff --git a/3DTank/tools/rebuild_codec_fields.js b/3DTank/tools/rebuild_codec_fields.js new file mode 100644 index 0000000..8cbcfca --- /dev/null +++ b/3DTank/tools/rebuild_codec_fields.js @@ -0,0 +1,312 @@ +// 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 生效'); +}