#!/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 });
