448 lines
20 KiB
JavaScript
448 lines
20 KiB
JavaScript
#!/usr/bin/env node
|
||
/**
|
||
* 将 Cocos Web 构建产物整理为与 Unity 参考包完全一致的目录/文件名:
|
||
* /Users/liuyufei/tfrh/竞赛/mstest5
|
||
*
|
||
* node tools/package-for-cdn.js <cocosBuildDir> <outDir>
|
||
*/
|
||
const crypto = require('crypto');
|
||
const fs = require('fs');
|
||
const path = require('path');
|
||
const zlib = require('zlib');
|
||
const { execSync } = require('child_process');
|
||
const {
|
||
patchPreloadSettings,
|
||
patchSplashSettings,
|
||
printPackageReport,
|
||
minifyLevelsDatabase,
|
||
brotliCompressFile,
|
||
brotliCompressWebglBundles,
|
||
formatBytes,
|
||
} = require('./package-optimize');
|
||
const { listRuntimeFiles, assertRuntimePack } = require('./runtime-pack');
|
||
const { splitLevelBundles } = require('./split-level-bundles');
|
||
const { splitLevelsDatabase } = require('./split-levels-database');
|
||
|
||
const buildDir = path.resolve(process.argv[2]);
|
||
const outDir = path.resolve(process.argv[3]);
|
||
const unityRef = path.resolve(process.argv[4] || '/Users/liuyufei/tfrh/竞赛/mstest5');
|
||
|
||
const PRODUCT = 'mstest5';
|
||
|
||
function parseCatalogBundles(catalogPath) {
|
||
const catalog = JSON.parse(fs.readFileSync(catalogPath, 'utf8'));
|
||
const names = [...new Set(
|
||
(catalog.m_InternalIds || [])
|
||
.filter((id) => String(id).includes('.bundle'))
|
||
.map((id) => String(id).split('/').pop()),
|
||
)];
|
||
const scenesAll = names.find((n) => n.includes('scenes_all'));
|
||
const assetsAll = names.find((n) => n.includes('assets_all'));
|
||
const levelsAll = names.find((n) => n.includes('levels_all'));
|
||
const shaders = names.find((n) => n.includes('unitybuiltinshaders'));
|
||
if (!scenesAll || !assetsAll) {
|
||
throw new Error(`catalog.json missing scenes/assets bundle: ${names.join(', ')}`);
|
||
}
|
||
return { shaders, scenesAll, assetsAll, levelsAll, all: names };
|
||
}
|
||
|
||
/** 复制 catalog 后剥离历史 levels_all 条目,再写入本次 bundle */
|
||
function patchCatalogAddLevelsBundle(catalogPath, bundleFileName) {
|
||
const catalog = JSON.parse(fs.readFileSync(catalogPath, 'utf8'));
|
||
const entry = `{UnityEngine.AddressableAssets.Addressables.RuntimePath}/WebGL/${bundleFileName}`;
|
||
const ids = (catalog.m_InternalIds || []).filter((id) => !String(id).includes('levels_all'));
|
||
ids.push(entry);
|
||
catalog.m_InternalIds = ids;
|
||
fs.writeFileSync(catalogPath, JSON.stringify(catalog), 'utf8');
|
||
}
|
||
|
||
/** 从 catalog 移除 levels_all 条目(仅 MERGE_LEVELS=1 合并包时使用) */
|
||
function patchCatalogRemoveLevelsBundle(catalogPath) {
|
||
const catalog = JSON.parse(fs.readFileSync(catalogPath, 'utf8'));
|
||
catalog.m_InternalIds = (catalog.m_InternalIds || []).filter((id) => !String(id).includes('levels_all'));
|
||
fs.writeFileSync(catalogPath, JSON.stringify(catalog), 'utf8');
|
||
}
|
||
|
||
function copyAssetsDirExcept(srcAssetsDir, dstAssetsDir, skipNames) {
|
||
const skip = new Set(skipNames || []);
|
||
if (!fs.existsSync(srcAssetsDir)) return;
|
||
mkdirp(dstAssetsDir);
|
||
for (const ent of fs.readdirSync(srcAssetsDir, { withFileTypes: true })) {
|
||
if (skip.has(ent.name)) continue;
|
||
const sp = path.join(srcAssetsDir, ent.name);
|
||
const dp = path.join(dstAssetsDir, ent.name);
|
||
if (ent.isDirectory()) copyDir(sp, dp);
|
||
else copyFile(sp, dp);
|
||
}
|
||
}
|
||
|
||
function hashFileMd5(filePath) {
|
||
return crypto.createHash('md5').update(fs.readFileSync(filePath)).digest('hex');
|
||
}
|
||
|
||
function attachLevelsDatabase(outDir) {
|
||
const levelsDbSrc = path.join(__dirname, '../assets/level-data/levels-database.json');
|
||
if (!fs.existsSync(levelsDbSrc)) {
|
||
console.warn('>>> 警告: 未找到 levels-database.json');
|
||
return;
|
||
}
|
||
splitLevelsDatabase(levelsDbSrc, outDir);
|
||
|
||
const levelsDbDst = path.join(outDir, 'levels-database.json');
|
||
const { before, after } = minifyLevelsDatabase(levelsDbSrc, levelsDbDst);
|
||
console.log(`>>> levels-database.json (legacy 回退): ${formatBytes(before)} → ${formatBytes(after)}`);
|
||
const { raw, br } = brotliCompressFile(levelsDbDst, path.join(outDir, 'levels-database.json.br'));
|
||
console.log(`>>> levels-database.json.br: ${formatBytes(raw)} → ${formatBytes(br)}`);
|
||
}
|
||
|
||
if (!buildDir || !outDir) {
|
||
console.error('Usage: package-for-cdn.js <cocosBuildDir> <outDir> [unityRefDir]');
|
||
process.exit(1);
|
||
}
|
||
|
||
function rmrf(p) {
|
||
if (fs.existsSync(p)) fs.rmSync(p, { recursive: true, force: true });
|
||
}
|
||
function mkdirp(p) { fs.mkdirSync(p, { recursive: true }); }
|
||
function copyFile(s, d) { mkdirp(path.dirname(d)); fs.copyFileSync(s, d); }
|
||
function copyDir(s, d) {
|
||
if (!fs.existsSync(s)) return;
|
||
mkdirp(d);
|
||
for (const ent of fs.readdirSync(s, { withFileTypes: true })) {
|
||
const sp = path.join(s, ent.name);
|
||
const dp = path.join(d, ent.name);
|
||
if (ent.isDirectory()) copyDir(sp, dp);
|
||
else copyFile(sp, dp);
|
||
}
|
||
}
|
||
|
||
function zipDir(srcDir, outFile) {
|
||
const absOut = path.resolve(outFile);
|
||
mkdirp(path.dirname(absOut));
|
||
if (fs.existsSync(absOut)) fs.unlinkSync(absOut);
|
||
execSync(`cd "${path.resolve(srcDir)}" && zip -0 -q -r "${absOut}" .`, { stdio: 'pipe' });
|
||
}
|
||
|
||
function patchText(file, fn) {
|
||
const t = fs.readFileSync(file, 'utf8');
|
||
fs.writeFileSync(file, fn(t), 'utf8');
|
||
}
|
||
|
||
function brotliStub(dst, bytes) {
|
||
const buf = zlib.brotliCompressSync(bytes || Buffer.from([0]));
|
||
fs.writeFileSync(dst, buf);
|
||
}
|
||
|
||
/**
|
||
* 分包模式下:须先 loadLevelPrefab 再 VisualAssets.preload,否则 resources 占满后 level-prefabs 加载失败。
|
||
* 对 Cocos 构建产物 assets/main/index.js 做稳定字符串 patch(无需立即重编工程)。
|
||
*/
|
||
function patchMainIndexForSplitLoad(mainIndexPath) {
|
||
if (!fs.existsSync(mainIndexPath)) {
|
||
console.warn('>>> 跳过 main/index 分包 patch(无 assets/main/index.js)');
|
||
return;
|
||
}
|
||
let s = fs.readFileSync(mainIndexPath, 'utf8');
|
||
const rules = [
|
||
{
|
||
desc: 'AppBootstrap: 首屏只 loadLevelDatabase + bounds',
|
||
old: 'yield I(),yield _(),yield E(),yield A(),L(),yield D.preload();',
|
||
neu: 'yield A(),L();',
|
||
},
|
||
{
|
||
desc: 'GameController: 进关前按需拉 assets / 关卡库分片',
|
||
old: 'if(!this.creating){this.creating=!0;var t=R(e);',
|
||
neu: 'if(!this.creating){this.creating=!0;try{if(globalThis.__tfrhEnsureAssetsCore)yield globalThis.__tfrhEnsureAssetsCore();yield I(),yield _(),yield E();if(globalThis.__tfrhEnsureLevelDbShard)yield globalThis.__tfrhEnsureLevelDbShard(e);}catch(err){console.error("[GameController] 运行时资源加载失败",err),this.creating=!1;return}var t=R(e);',
|
||
},
|
||
{
|
||
desc: 'LevelDatabase: 分片索引模式 isReady',
|
||
old: 'isLevelDatabaseReady:function(){return null!==s}',
|
||
neu: 'isLevelDatabaseReady:function(){return null!==s||("undefined"!=typeof window&&!!window.__tfrhLevelsDbIndex)}',
|
||
},
|
||
{
|
||
desc: 'LevelDatabase: 启动读 index 而非全量库',
|
||
old: 'try{if("undefined"!=typeof window&&window.__tfrhLevelsDatabaseJson)',
|
||
neu: 'try{if("undefined"!=typeof window&&window.__tfrhLevelsDbIndex){s={version:window.__tfrhLevelsDbIndex.version||2,levels:{}},console.log("[LevelDatabase] 已加载索引 "+window.__tfrhLevelsDbIndex.total+" 关");return}if("undefined"!=typeof window&&window.__tfrhLevelsDatabaseJson)',
|
||
},
|
||
{
|
||
desc: 'LevelDatabase: validate 接受 index',
|
||
old: 'function w(){var e=c.length,n=null!=(t=c[0])?t:0;if(e<100||n<a)throw new Error',
|
||
neu: 'function w(){if("undefined"!=typeof window&&window.__tfrhLevelsDbIndex){var e=window.__tfrhLevelsDbIndex;if(e.total<100||e.min<a)throw new Error("关卡库索引过旧 ("+e.total+" 关)");return}var e=c.length,n=null!=(t=c[0])?t:0;if(e<100||n<a)throw new Error',
|
||
},
|
||
{
|
||
desc: 'LevelRegistry: min/max 读 index',
|
||
old: 'function f(){return c()?d():t}function E(){return c()?I():t}',
|
||
neu: 'function f(){var e="undefined"!=typeof window&&window.__tfrhLevelsDbIndex;return e?e.min:c()?d():t}function E(){var e="undefined"!=typeof window&&window.__tfrhLevelsDbIndex;return e?e.max:c()?I():t}',
|
||
},
|
||
{
|
||
desc: 'LevelRegistry: hasLevel 读 index 范围',
|
||
old: 'hasLevel:function(e){return v(e)}',
|
||
neu: 'hasLevel:function(e){if(v(e))return!0;var n="undefined"!=typeof window&&window.__tfrhLevelsDbIndex;return!!n&&e>=n.min&&e<=n.max}',
|
||
},
|
||
{
|
||
desc: 'LevelDatabase: getLevelConfig 读 loader 分片',
|
||
old: 'getLevelConfig:function(e){var n;return null!=(n=f[e])?n:null}',
|
||
neu: 'getLevelConfig:function(e){var n;if(null!=(n=f[e]))return n;var t="undefined"!=typeof window&&window.__tfrhLevelsDatabaseJson&&window.__tfrhLevelsDatabaseJson.levels;return t&&t[String(e)]||null}',
|
||
},
|
||
{
|
||
desc: 'GameController: 使用 loader 全局 loadLevelPrefab hook (新构建 K)',
|
||
old: 'var l,r=yield K(i);',
|
||
neu: 'var l,r=yield(globalThis.__tfrhLoadLevelPrefab||K)(i);',
|
||
},
|
||
{
|
||
desc: 'GameController: 使用 loader 全局 loadLevelPrefab hook (旧构建 z)',
|
||
old: 'var l,r=yield z(i);',
|
||
neu: 'var l,r=yield(globalThis.__tfrhLoadLevelPrefab||z)(i);',
|
||
},
|
||
{
|
||
desc: 'AppBootstrap: 首关前不 preload UI 贴图',
|
||
old: 'yield h.preload(),yield O.preload()',
|
||
neu: 'yield O.preload()',
|
||
},
|
||
{
|
||
desc: 'AppBootstrap: 进关再 loadLevel(不在 bootstrap 拉首关)',
|
||
old: 'yield F.createNewLevel(F.initialLevelID),S.purgeScene(c),F.scheduleOnce((function(){return S.purgeScene(c)}),0),F.scheduleOnce((function(){return S.purgeScene(c)}),.15),F.markReady()',
|
||
neu: 'F.markReady()',
|
||
},
|
||
{
|
||
desc: 'GameController: 使用 loader 全局 loadLevelPrefab hook',
|
||
old: 'var l,r=yield z(i);yield H.preload(this.uiStyle)',
|
||
neu: 'var l,r=yield(globalThis.__tfrhLoadLevelPrefab||z)(i);yield H.preload(this.uiStyle)',
|
||
},
|
||
{
|
||
desc: 'LevelPrefabLoader: 去掉裸 LevelN 路径',
|
||
old: 'return[].concat(new Set([r,n,"level-prefabs/"+n]))',
|
||
neu: 'return[].concat(new Set([r,"level-prefabs/"+n]))',
|
||
},
|
||
{
|
||
desc: 'LevelPrefabLoader: 进关前 ensureLevelPack',
|
||
old: 'function*(e){yield a();var r=v(e);',
|
||
neu: 'function*(e){yield a();var _h=globalThis.__tfrhEnsureLevelPack,_m=/Level(\\d+)\\s*$/.exec(e.trim()),_lid=_m?parseInt(_m[1],10):void 0;if(_lid!==void 0&&typeof _h==="function")yield _h(_lid);var r=v(e);',
|
||
},
|
||
{
|
||
desc: 'LevelPrefabLoader: ensureLevelPack 后清空 bundle 缓存 (新构建 w/L/s/f)',
|
||
old: 'yield u(),yield L(e);var r=v(e),n=null;try{var l=yield c();return yield d((function(e){return p(l,e)}),r)}catch(e){n=e,console.warn("[LevelPrefabLoader] level-prefabs bundle 加载失败",e)}var t=yield y(e);if(null!=t&&t.isValid)return t;throw console.error(\'[LevelPrefabLoader] 关卡预制体未找到。请确认 bundle-level-prefabs 已标记为 Asset Bundle "level-prefabs",并运行: python3 tools/bake_cocos_level_prefabs.py\',n),n instanceof Error?n:new Error("missing prefab: "+e)',
|
||
neu: 'yield u(),yield L(e);s=null,f=null;var r=v(e);try{var l=yield c();return yield d((function(e){return p(l,e)}),r)}catch(e){throw console.error("[LevelPrefabLoader] 关卡预制体未找到",e),e||new Error("missing prefab: "+e)}',
|
||
},
|
||
{
|
||
desc: 'LevelPrefabLoader: ensureLevelPack 后清空 bundle 缓存 (旧构建 g/w/c/f)',
|
||
old: 'yield a(),yield w(e);var r=b(e);try{var n=yield p();return yield y((function(e){return d(n,e)}),r)}catch(e){throw console.error("[LevelPrefabLoader] 关卡预制体未找到",e),e||new Error("missing prefab")}',
|
||
neu: 'yield a(),yield w(e);c=null,f=null;var r=b(e);try{var n=yield p();return yield y((function(e){return d(n,e)}),r)}catch(e){throw console.error("[LevelPrefabLoader] 关卡预制体未找到",e),e||new Error("missing prefab")}',
|
||
},
|
||
{
|
||
desc: 'LevelPrefabLoader: 禁用 resources 回退',
|
||
old: '}catch(e){console.warn("[LevelPrefabLoader] level-prefabs bundle 不可用,尝试 resources",e)}var l=yield L(e);if(null!=l&&l.isValid)return l;try{return yield y(v,r)}catch(e){throw console.error(\'[LevelPrefabLoader] 关卡预制体未找到。请确认 bundle-level-prefabs 已标记为 Asset Bundle "level-prefabs",并运行: python3 tools/bake_cocos_level_prefabs.py\',e),e}',
|
||
neu: '}catch(e){throw console.error("[LevelPrefabLoader] 关卡预制体未找到",e),e||new Error("missing prefab")}',
|
||
},
|
||
];
|
||
let n = 0;
|
||
for (const { desc, old, neu } of rules) {
|
||
if (!s.includes(old)) continue;
|
||
s = s.replace(old, neu);
|
||
n += 1;
|
||
console.log(`>>> main/index.js patch: ${desc}`);
|
||
}
|
||
if (n === 0) {
|
||
console.warn('>>> 警告: main/index.js 未匹配分包 patch(请确认 Cocos 已构建或更新 patch 规则)');
|
||
}
|
||
fs.writeFileSync(mainIndexPath, s, 'utf8');
|
||
}
|
||
|
||
if (!fs.existsSync(path.join(buildDir, 'index.html'))) {
|
||
console.error('Missing Cocos build:', buildDir);
|
||
process.exit(1);
|
||
}
|
||
if (!fs.existsSync(path.join(unityRef, 'index.html'))) {
|
||
console.error('Missing Unity reference index.html:', unityRef);
|
||
console.error('需要完整 Unity 参考包(如 ~/tfrh/竞赛/mstest5),static/unity 扁平包不可用。');
|
||
process.exit(1);
|
||
}
|
||
if (!fs.existsSync(path.join(unityRef, 'StreamingAssets/aa/catalog.json'))) {
|
||
console.error('Missing Unity reference catalog:', unityRef);
|
||
process.exit(1);
|
||
}
|
||
|
||
const tmp = path.join(outDir, '..', '.pack-tmp-' + Date.now());
|
||
rmrf(outDir);
|
||
mkdirp(tmp);
|
||
|
||
console.log('>>> Unity 参考:', unityRef);
|
||
console.log('>>> Cocos 构建:', buildDir);
|
||
console.log('>>> 输出(运行时包):', outDir);
|
||
|
||
// —— 1. StreamingAssets 元数据(不含 index.html / TemplateData,与 CDN 上传一致)——
|
||
mkdirp(outDir);
|
||
|
||
const aaDir = path.join(outDir, 'StreamingAssets', 'aa');
|
||
const webglDir = path.join(aaDir, 'WebGL');
|
||
mkdirp(webglDir);
|
||
copyFile(path.join(unityRef, 'StreamingAssets/aa/settings.json'), path.join(aaDir, 'settings.json'));
|
||
copyFile(path.join(unityRef, 'StreamingAssets/aa/catalog.json'), path.join(aaDir, 'catalog.json'));
|
||
copyDir(path.join(unityRef, 'StreamingAssets/aa/AddressablesLink'), path.join(aaDir, 'AddressablesLink'));
|
||
|
||
const BUNDLE = parseCatalogBundles(path.join(aaDir, 'catalog.json'));
|
||
console.log('>>> catalog bundles:', BUNDLE.all.join(', '));
|
||
if (BUNDLE.shaders) {
|
||
copyFile(
|
||
path.join(unityRef, 'StreamingAssets/aa/WebGL', BUNDLE.shaders),
|
||
path.join(webglDir, BUNDLE.shaders),
|
||
);
|
||
}
|
||
|
||
// —— 2. 打 scenes bundle(Cocos 运行时 JS)——
|
||
const scenesStage = path.join(tmp, 'scenes');
|
||
mkdirp(scenesStage);
|
||
for (const f of ['index.js', 'application.js']) {
|
||
copyFile(path.join(buildDir, f), path.join(scenesStage, f));
|
||
}
|
||
copyDir(path.join(buildDir, 'src'), path.join(scenesStage, 'src'));
|
||
copyDir(path.join(buildDir, 'cocos-js'), path.join(scenesStage, 'cocos-js'));
|
||
copyFile(
|
||
path.join(path.dirname(__filename), '../web-template/cocos-bridge.js'),
|
||
path.join(scenesStage, 'cocos-bridge.js'),
|
||
);
|
||
|
||
patchText(path.join(scenesStage, 'application.js'), (t) =>
|
||
t.replace(/this\.settingsPath\s*=\s*'[^']*'/, "this.settingsPath = 'src/settings.json'"),
|
||
);
|
||
// 保持 assets.server 为空,由 loader fetch shim / 本地解压目录提供资源
|
||
patchText(path.join(scenesStage, 'src', 'settings.json'), (t) => {
|
||
const j = JSON.parse(t);
|
||
j.assets = j.assets || {};
|
||
j.assets.server = '';
|
||
patchPreloadSettings(j, { preloadResources: false, preloadLevelPrefabs: false });
|
||
patchSplashSettings(j, { stripSplashAssets: true });
|
||
return JSON.stringify(j);
|
||
});
|
||
|
||
zipDir(scenesStage, path.join(webglDir, BUNDLE.scenesAll));
|
||
const scenesZip = path.join(webglDir, BUNDLE.scenesAll);
|
||
const scenesBr = brotliCompressFile(scenesZip, scenesZip + '.br');
|
||
console.log(`>>> ${BUNDLE.scenesAll}.br: ${formatBytes(scenesBr.raw)} → ${formatBytes(scenesBr.br)}`);
|
||
|
||
// —— 3a. assets_all:核心资源(不含 level-prefabs)——
|
||
// 默认分包;MERGE_LEVELS=1 时合并 level-prefabs 进 assets_all(不推荐)
|
||
const MERGE_LEVELS = process.env.MERGE_LEVELS === '1';
|
||
const assetsCoreStage = path.join(tmp, 'assets-core');
|
||
mkdirp(assetsCoreStage);
|
||
if (MERGE_LEVELS) {
|
||
copyDir(path.join(buildDir, 'assets'), path.join(assetsCoreStage, 'assets'));
|
||
patchCatalogRemoveLevelsBundle(path.join(aaDir, 'catalog.json'));
|
||
console.log('>>> MERGE_LEVELS=1: level-prefabs 已并入 assets_all');
|
||
} else {
|
||
copyAssetsDirExcept(
|
||
path.join(buildDir, 'assets'),
|
||
path.join(assetsCoreStage, 'assets'),
|
||
['level-prefabs'],
|
||
);
|
||
// level-prefabs 壳(config + index)进首屏,各关 import 单独按需包
|
||
const lpShellSrc = path.join(buildDir, 'assets', 'level-prefabs');
|
||
const lpShellDst = path.join(assetsCoreStage, 'assets', 'level-prefabs');
|
||
mkdirp(lpShellDst);
|
||
for (const f of ['config.json', 'index.js']) {
|
||
const sp = path.join(lpShellSrc, f);
|
||
if (fs.existsSync(sp)) copyFile(sp, path.join(lpShellDst, f));
|
||
}
|
||
patchMainIndexForSplitLoad(path.join(assetsCoreStage, 'assets', 'main', 'index.js'));
|
||
console.log('>>> 分包模式: assets_all 含 level-prefabs 壳,不含关卡 import');
|
||
}
|
||
const nativeWasm = path.join(buildDir, 'cocos-js', 'assets');
|
||
if (fs.existsSync(nativeWasm)) {
|
||
copyDir(nativeWasm, path.join(assetsCoreStage, 'assets'));
|
||
}
|
||
copyFile(path.join(scenesStage, 'src', 'settings.json'), path.join(assetsCoreStage, 'src', 'settings.json'));
|
||
if (fs.existsSync(path.join(scenesStage, 'src', 'effect.bin'))) {
|
||
copyFile(path.join(scenesStage, 'src', 'effect.bin'), path.join(assetsCoreStage, 'src', 'effect.bin'));
|
||
}
|
||
|
||
const assetsCoreZip = path.join(webglDir, BUNDLE.assetsAll);
|
||
zipDir(assetsCoreStage, assetsCoreZip);
|
||
console.log(`>>> assets_core (assets_all): ${formatBytes(fs.statSync(assetsCoreZip).size)}`);
|
||
const assetsBr = brotliCompressFile(assetsCoreZip, assetsCoreZip + '.br');
|
||
console.log(`>>> ${BUNDLE.assetsAll}.br: ${formatBytes(assetsBr.raw)} → ${formatBytes(assetsBr.br)}`);
|
||
|
||
// —— 3b. 每关独立 bundle + levels-manifest.json ——
|
||
const levelPrefabsSrc = path.join(buildDir, 'assets', 'level-prefabs');
|
||
let levelPackStats = null;
|
||
if (!MERGE_LEVELS) {
|
||
if (!fs.existsSync(levelPrefabsSrc)) {
|
||
console.error('>>> 错误: 分包模式需要 build/assets/level-prefabs');
|
||
console.error('>>> 请确认 bundle-level-prefabs 已配置并在 Cocos 中重新构建 Web Desktop');
|
||
process.exit(1);
|
||
}
|
||
patchCatalogRemoveLevelsBundle(path.join(aaDir, 'catalog.json'));
|
||
const manifestPath = path.join(aaDir, 'levels-manifest.json');
|
||
levelPackStats = splitLevelBundles(levelPrefabsSrc, webglDir, manifestPath, tmp);
|
||
const brStats = brotliCompressWebglBundles(webglDir);
|
||
console.log(`>>> bundle Brotli: ${brStats.count} 个, 节省 ${formatBytes(brStats.saved)}`);
|
||
}
|
||
|
||
attachLevelsDatabase(outDir);
|
||
|
||
// —— 4. Build/ 仅 4 个文件(与 Unity 同名)——
|
||
const buildOut = path.join(outDir, 'Build');
|
||
mkdirp(buildOut);
|
||
|
||
const loaderTpl = fs.readFileSync(
|
||
path.join(path.dirname(__filename), '../web-template/mstest5-cocos-loader.js'),
|
||
'utf8',
|
||
);
|
||
fs.writeFileSync(
|
||
path.join(buildOut, `${PRODUCT}.loader.js`),
|
||
loaderTpl.replace(/__PRODUCT_NAME__/g, PRODUCT),
|
||
'utf8',
|
||
);
|
||
|
||
// .br 占位(index.html config 需要文件存在;实际由自定义 loader 启动 Cocos)
|
||
brotliStub(path.join(buildOut, `${PRODUCT}.data.br`));
|
||
brotliStub(path.join(buildOut, `${PRODUCT}.framework.js.br`));
|
||
brotliStub(path.join(buildOut, `${PRODUCT}.wasm.br`));
|
||
|
||
rmrf(tmp);
|
||
|
||
// —— 5. 独立调试页(可选,不进 static/unity 也不上传 OSS)——
|
||
const standaloneDir = path.join(path.dirname(outDir), 'standalone-player');
|
||
rmrf(standaloneDir);
|
||
mkdirp(standaloneDir);
|
||
copyFile(path.join(unityRef, 'index.html'), path.join(standaloneDir, 'index.html'));
|
||
copyDir(path.join(unityRef, 'TemplateData'), path.join(standaloneDir, 'TemplateData'));
|
||
for (const item of [
|
||
'Build',
|
||
'StreamingAssets',
|
||
'levels-database.json',
|
||
'levels-database.json.br',
|
||
'levels-db-index.json',
|
||
'levels-db-index.json.br',
|
||
]) {
|
||
const src = path.join(outDir, item);
|
||
const dst = path.join(standaloneDir, item);
|
||
if (!fs.existsSync(src)) continue;
|
||
if (fs.statSync(src).isDirectory()) copyDir(src, dst);
|
||
else copyFile(src, dst);
|
||
}
|
||
console.log('>>> 独立调试页:', standaloneDir);
|
||
|
||
assertRuntimePack(outDir, { requireLevelsBundle: false, requireLevelsManifest: !MERGE_LEVELS });
|
||
|
||
const runtimeFiles = listRuntimeFiles(outDir);
|
||
console.log('\n>>> 运行时包文件(本地 static/unity = OSS unitycdndir):');
|
||
for (const { rel } of runtimeFiles) console.log(' ', rel);
|
||
|
||
const requiredBundles = [
|
||
path.join(webglDir, BUNDLE.scenesAll),
|
||
path.join(webglDir, BUNDLE.assetsAll),
|
||
];
|
||
if (BUNDLE.shaders) requiredBundles.push(path.join(webglDir, BUNDLE.shaders));
|
||
for (const p of requiredBundles) {
|
||
if (!fs.existsSync(p) || fs.statSync(p).size < 100) {
|
||
console.error('>>> bundle 无效:', p);
|
||
process.exit(1);
|
||
}
|
||
}
|
||
|
||
const preloadNote = MERGE_LEVELS
|
||
? 'scenes ∥ assets_all 合并包 (MERGE_LEVELS=1)'
|
||
: `scenes ∥ assets_core 首屏(引擎启动必需);关卡库分片 + 关卡 bundle 进关按需 (${levelPackStats ? levelPackStats.packed : '?'} 关)`;
|
||
console.log('\n完成。运行时包 → 本地 import-to-unity.sh / OSS unitycdndir(同一目录)');
|
||
printPackageReport(outDir, { preloadNote });
|