#!/usr/bin/env node /** * 将 build/assets/level-prefabs 拆为: * - shell: config.json + index.js(进 assets_all 首屏) * - 每关一包: assets/level-prefabs/import/.../*.json * * 输出 levels-manifest.json 供 loader 按 levelId 按需下载。 */ const crypto = require('crypto'); const fs = require('fs'); const path = require('path'); const { execSync } = require('child_process'); const { formatBytes } = require('./package-optimize'); function mkdirp(p) { fs.mkdirSync(p, { recursive: true }); } function hashFileMd5(filePath) { return crypto.createHash('md5').update(fs.readFileSync(filePath)).digest('hex'); } 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 walkJsonFiles(dir, out = []) { if (!fs.existsSync(dir)) return out; for (const ent of fs.readdirSync(dir, { withFileTypes: true })) { const p = path.join(dir, ent.name); if (ent.isDirectory()) walkJsonFiles(p, out); else if (ent.name.endsWith('.json')) out.push(p); } return out; } /** 扫描 import/*.json,从预制体名 Level{id} 建立 levelId → 相对路径 */ function indexImportFiles(levelPrefabsDir) { const importRoot = path.join(levelPrefabsDir, 'import'); const byLevelId = new Map(); for (const abs of walkJsonFiles(importRoot)) { const rel = path.relative(levelPrefabsDir, abs).split(path.sep).join('/'); const txt = fs.readFileSync(abs, 'utf8'); const m = /"Level(\d+)"/.exec(txt); if (!m) continue; const levelId = m[1]; if (byLevelId.has(levelId)) { console.warn(`>>> 警告: Level${levelId} 重复 import,保留 ${byLevelId.get(levelId)}`); continue; } byLevelId.set(levelId, rel); } return byLevelId; } /** 从 config.json 校验 path ↔ uuid */ function readConfigLevels(levelPrefabsDir) { const cfg = JSON.parse(fs.readFileSync(path.join(levelPrefabsDir, 'config.json'), 'utf8')); const out = new Map(); for (const [idx, entry] of Object.entries(cfg.paths || {})) { const m = /^level-prefabs\/Level(\d+)$/.exec(entry[0]); if (!m) continue; out.set(m[1], { path: entry[0], pathIndex: idx, uuid: (cfg.uuids || [])[+idx], }); } return out; } /** * @param {string} levelPrefabsDir Cocos 构建产物 assets/level-prefabs * @param {string} webglDir 输出 WebGL/*.bundle * @param {string} manifestOutPath 输出 levels-manifest.json */ function splitLevelBundles(levelPrefabsDir, webglDir, manifestOutPath, packTmpDir) { if (!fs.existsSync(path.join(levelPrefabsDir, 'config.json'))) { throw new Error(`缺少 level-prefabs/config.json: ${levelPrefabsDir}`); } const configLevels = readConfigLevels(levelPrefabsDir); const importByLevel = indexImportFiles(levelPrefabsDir); mkdirp(webglDir); const stageBase = packTmpDir || path.join(webglDir, '..', '.level-pack-tmp'); mkdirp(stageBase); const manifest = { version: 1, shell: ['assets/level-prefabs/config.json', 'assets/level-prefabs/index.js'], levels: {}, }; let packed = 0; let totalBytes = 0; const missing = []; for (const [levelId, meta] of configLevels) { const importRel = importByLevel.get(levelId); if (!importRel) { missing.push(levelId); continue; } const stageRoot = path.join(stageBase, levelId); const assetRoot = path.join(stageRoot, 'assets', 'level-prefabs'); mkdirp(path.dirname(path.join(assetRoot, importRel))); fs.copyFileSync( path.join(levelPrefabsDir, importRel), path.join(assetRoot, importRel), ); const zipTmp = path.join(stageBase, `${levelId}.zip`); zipDir(stageRoot, zipTmp); const hash = hashFileMd5(zipTmp); const bundleName = `defaultlocalgroup_level_${levelId}_${hash}.bundle`; fs.copyFileSync(zipTmp, path.join(webglDir, bundleName)); const size = fs.statSync(zipTmp).size; totalBytes += size; manifest.levels[levelId] = { bundle: bundleName, path: meta.path, uuid: meta.uuid, files: [`assets/level-prefabs/${importRel}`], bytes: size, }; packed += 1; fs.rmSync(stageRoot, { recursive: true, force: true }); fs.unlinkSync(zipTmp); } fs.rmSync(stageBase, { recursive: true, force: true }); fs.writeFileSync(manifestOutPath, JSON.stringify(manifest), 'utf8'); console.log(`>>> 关卡分包: ${packed} 关, 合计 ${formatBytes(totalBytes)}, 均 ${formatBytes(Math.round(totalBytes / Math.max(packed, 1)))}/关`); if (missing.length) { console.warn(`>>> 警告: ${missing.length} 关在 config 中无 import 文件 (例: ${missing.slice(0, 5).join(', ')})`); } return { manifest, packed, missing, totalBytes }; } module.exports = { splitLevelBundles, indexImportFiles, readConfigLevels, }; if (require.main === module) { const levelDir = path.resolve(process.argv[2]); const webgl = path.resolve(process.argv[3]); const manifest = path.resolve(process.argv[4] || path.join(path.dirname(webgl), 'levels-manifest.json')); splitLevelBundles(levelDir, webgl, manifest); }