feat(tools): 用源码 u7h/t7h 核对全部 codec 字段表

This commit is contained in:
TermiNexus committed 2026-10-21 02:46:28 +08:00
1 parent b079d04653
commit beb1d3f917
1 file changed
+312
+312
View File
@@ -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 = /(?<![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 = Deps[M[2]];
if (Dep && !Out[M[1]]) Out[M[1]] = { Mod: Dep, Key: M[3] };
}
return Out;
}
function FindRoles(Src, Deps) {
const Roles = {};
for (const [Name, A] of Object.entries(FindAliases(Src, Deps))) {
const Role = ROLE_KEYS[A.Mod + '::' + A.Key];
if (Role) Roles[Name] = Role;
}
return Roles;
}
// 访问器(`function X(t) { var n = t.<槽位>; 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 生效');
}