313 lines
11 KiB
JavaScript
313 lines
11 KiB
JavaScript
// 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 生效');
|
||
}
|