Files
TankIC/tools/apply_symbol_map.js
T
TermiNexus dcf5dfce5c fix(tools): 工具链符号对齐新版 bundle(修复 HORNET 功能大面积失效)
- 根因:新版 main.7cfa4058 重拆后游戏符号整体平移,工具里写死的字段名(pyt_1/q100_1/r11l/oyz/jyw/cyw_1/dyw_1…)全部对不上 → 找不到组件/相机/场景

- 新增 tools/apply_symbol_map.js:按 map_symbols 映射批量替换,4 道安全规则:① 长名或含下划线 ② 排除 JS 关键字/内置名黑名单 ③ 游戏符号命名特征(含数字或 _数字 结尾) ④ 双向校验(旧名在新 bundle 仍存在→视为未变跳过;新名不存在→映射错误丢弃)

- 单遍替换,避免链式污染(映射表新旧同名复用时会被二次替换)

- 应用到 12 个 hornet_*.js 共 231 处(跳过 25764 个未变符号)

- 重新生成 hornet_bundle.js(10 模块 / 249263 字节)并同步 unpacked
2026-10-16 14:55:39 +08:00

101 lines
5.5 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.
// 临时脚本:按 tmp_symbol_map.json 的映射,批量替换 tools/hornet_*.js 里的符号名
// 用法:node tools/tmp_apply_symbol_map.js
// 安全规则(上一版全量替换误伤了 function/return/window/length 等 JS 关键字与内置名):
// ① 旧名必须是"长名或含下划线"(>=4 字符 或 含 '_')—— 游戏符号基本都长这样;
// ② 旧名/新名都不能落在 JS 关键字/内置名黑名单里;
// ③ 只替换独立标识符(带词边界)。
const fs = require('fs');
const path = require('path');
const MapPath = path.resolve(__dirname, '..', '3DTank', 'tmp_symbol_map.json');
const Map = (require(MapPath)._merged) || {};
// JS 关键字 + 内置名 + 常见通用词:绝不能参与替换
const BLACK = new Set([
'function', 'return', 'var', 'let', 'const', 'null', 'undefined', 'true', 'false', 'this',
'typeof', 'instanceof', 'new', 'delete', 'void', 'in', 'of', 'if', 'else', 'for', 'while',
'do', 'switch', 'case', 'break', 'continue', 'throw', 'try', 'catch', 'finally', 'class',
'extends', 'super', 'yield', 'await', 'async', 'default', 'export', 'import',
'window', 'document', 'console', 'Math', 'Object', 'Array', 'String', 'Number', 'Boolean',
'JSON', 'Date', 'Error', 'Promise', 'Map', 'Set', 'Symbol', 'Proxy', 'Reflect',
'length', 'name', 'value', 'text', 'call', 'apply', 'bind', 'prototype', 'constructor',
'toString', 'valueOf', 'equals', 'hashCode', 'push', 'pop', 'slice', 'splice', 'indexOf',
'forEach', 'filter', 'reduce', 'join', 'split', 'replace', 'match', 'test', 'keys', 'values',
'entries', 'has', 'get', 'set', 'add', 'clear', 'size', 'run', 'self', 'globalThis',
'requestAnimationFrame', 'setTimeout', 'setInterval', 'parseInt', 'parseFloat', 'isNaN',
'NaN', 'Infinity', 'arguments', 'prototype_', 'undefined_',
]);
// ===== 双向校验用的"新 bundle 符号表" =====
// 背景:map_symbols 的投票在"通用字段名"上会算错(例如把未变的 f20_1 投成 w9p_1)。
// 这里直接拿新 bundle 原文做事实校验:
// ① 旧名在新 bundle 里**仍然存在** → 说明它根本没变,跳过(保守,避免误改);
// ② 新名在新 bundle 里**不存在** → 说明这条映射是错的,丢弃。
const BUNDLE_PATH = path.resolve(__dirname, '..', 'main.7cfa4058.js');
const BUNDLE = fs.existsSync(BUNDLE_PATH) ? fs.readFileSync(BUNDLE_PATH, 'utf8') : '';
const CountCache = {};
function CountInBundle(Name) {
if (!BUNDLE) return 1; // 拿不到 bundle 就不做校验(退回旧行为)
if (CountCache[Name] !== undefined) return CountCache[Name];
const Re = new RegExp('(?<![A-Za-z0-9_$])' + Name.replace(/\$/g, '\\$') + '(?![A-Za-z0-9_$])', 'g');
const M = BUNDLE.match(Re);
CountCache[Name] = M ? M.length : 0;
return CountCache[Name];
}
// 收集要替换的条目
const Entries = [];
const Skipped = { keeps: [], bad: [] };
for (const K of Object.keys(Map)) {
const V = Map[K];
if (typeof V !== 'string' || !V || V === K) continue;
if (!/^[A-Za-z_$][A-Za-z0-9_$]*$/.test(K)) continue;
if (!/^[A-Za-z_$][A-Za-z0-9_$]*$/.test(V)) continue;
if (BLACK.has(K) || BLACK.has(V)) continue;
if (K.length < 4 && K.indexOf('_') < 0) continue; // 规则①:长名或含下划线
// 规则④:游戏符号的命名特征 —— 必须含数字,或以 _数字 结尾(如 pyt_1 / q100_1 / r11l / cyw_1)。
// 这样能排除工具自身的普通英文单词变量(placeholder / mode / length 之类)。
if (!/[0-9]/.test(K) && !/_[0-9]+$/.test(K)) continue;
// 规则⑤/⑥:双向校验
if (CountInBundle(K) > 0) { Skipped.keeps.push(K); continue; }
if (CountInBundle(V) === 0) { Skipped.bad.push(K + '→' + V); continue; }
Entries.push([K, V]);
}
if (Skipped.keeps.length) console.log('跳过(旧名在新版仍存在,视为未变) ' + Skipped.keeps.length + ' 个: ' + Skipped.keeps.slice(0, 20).join(' ') + (Skipped.keeps.length > 20 ? ' …' : ''));
if (Skipped.bad.length) console.log('丢弃(新名在新版不存在,映射有误) ' + Skipped.bad.length + ' 条: ' + Skipped.bad.slice(0, 20).join(' ') + (Skipped.bad.length > 20 ? ' …' : ''));
// 长符号优先,避免短符号先吃掉长符号的一部分
Entries.sort((A, B) => B[0].length - A[0].length);
const Dir = path.resolve(__dirname);
const Files = fs.readdirSync(Dir).filter((F) => /^hornet_.*\.js$/.test(F));
// 单遍替换:一个正则同时匹配所有旧名,回调里查映射。
// 这样能避免"上一轮的替换结果又被下一轮规则二次替换"(映射表里新旧符号可能同名复用,会造成链式污染)。
const Alt = new RegExp(
'(?<![A-Za-z0-9_$])(' + Entries.map((E) => E[0].replace(/\$/g, '\\$')).join('|') + ')(?![A-Za-z0-9_$])',
'g'
);
let HitsOf = {}; // 注意:别用 new Map()——本文件里 Map 已被映射表变量占用
let TotalHits = 0;
for (const F of Files) {
const P = path.join(Dir, F);
const Before = fs.readFileSync(P, 'utf8');
HitsOf = {};
const S = Before.replace(Alt, (M) => {
const V = Map[M];
if (!V || V === M) return M;
HitsOf[M + '→' + V] = (HitsOf[M + '→' + V] || 0) + 1;
return V;
});
if (S !== Before) {
fs.writeFileSync(P, S, 'utf8');
let Hits = 0;
Object.keys(HitsOf).forEach((K) => (Hits += HitsOf[K]));
TotalHits += Hits;
const Detail = Object.keys(HitsOf).map((K) => K + '(' + HitsOf[K] + ')');
console.log(F + ':' + Hits + ' 处 ' + Detail.slice(0, 12).join(' ') + (Detail.length > 12 ? ' …共' + Detail.length + '种' : ''));
}
}
console.log('— 合计 ' + TotalHits + ' 处替换 —');