Files
cocos/tools/split-level-bundles.js
刘宇飞 d393302388 Complete Cocos Creator port with level bundles, themes, and tooling.
Adds level prefabs, theme assets, audio, extensions, and deployment scripts for the Unity WebGL migration.

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-06-16 15:30:58 +08:00

156 lines
5.1 KiB
JavaScript
Raw Permalink 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.
#!/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);
}