Files
TankIC/3DTank/tools/rebuild_codec_fields.js
T

313 lines
11 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
// 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 生效');
}