Files
cocos_zhuzhan/tools/split-level-bundles.js

181 lines
6.0 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.
#!/usr/bin/env node
/**
* 将 build/assets/level-prefabs 拆为:
* - shell: config.json + index.js进 assets_all 首屏)
* - 每 15 关一包: assets/level-prefabs/import/.../*.json
*
* 输出 levels-manifest.json 供 loader 按 levelId 按需下载(同包关卡共享 bundle
* LEVELS_PER_PACK 可覆盖每包关卡数,默认 15。
*/
const crypto = require('crypto');
const fs = require('fs');
const path = require('path');
const { execSync } = require('child_process');
const { formatBytes } = require('./package-optimize');
const DEFAULT_LEVELS_PER_PACK = 15;
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;
}
function levelsPerPack() {
const n = parseInt(process.env.LEVELS_PER_PACK || String(DEFAULT_LEVELS_PER_PACK), 10);
return Number.isFinite(n) && n > 0 ? n : DEFAULT_LEVELS_PER_PACK;
}
/** 扫描 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 packSize = levelsPerPack();
const configLevels = readConfigLevels(levelPrefabsDir);
const importByLevel = indexImportFiles(levelPrefabsDir);
mkdirp(webglDir);
const stageBase = packTmpDir || path.join(webglDir, '..', '.level-pack-tmp');
mkdirp(stageBase);
const ready = [];
const missing = [];
for (const [levelId, meta] of configLevels) {
const importRel = importByLevel.get(levelId);
if (!importRel) {
missing.push(levelId);
continue;
}
ready.push({ levelId, meta, importRel });
}
ready.sort((a, b) => Number(a.levelId) - Number(b.levelId));
const manifest = {
version: 2,
packSize,
shell: ['assets/level-prefabs/config.json', 'assets/level-prefabs/index.js'],
levels: {},
};
let packed = 0;
let packs = 0;
let totalBytes = 0;
for (let i = 0; i < ready.length; i += packSize) {
const group = ready.slice(i, i + packSize);
const firstId = group[0].levelId;
const lastId = group[group.length - 1].levelId;
const stageRoot = path.join(stageBase, `pack-${firstId}-${lastId}`);
const assetRoot = path.join(stageRoot, 'assets', 'level-prefabs');
for (const item of group) {
mkdirp(path.dirname(path.join(assetRoot, item.importRel)));
fs.copyFileSync(
path.join(levelPrefabsDir, item.importRel),
path.join(assetRoot, item.importRel),
);
}
const zipTmp = path.join(stageBase, `pack-${firstId}-${lastId}.zip`);
zipDir(stageRoot, zipTmp);
const hash = hashFileMd5(zipTmp);
const bundleName = `defaultlocalgroup_levels_${firstId}_${lastId}_${hash}.bundle`;
fs.copyFileSync(zipTmp, path.join(webglDir, bundleName));
const size = fs.statSync(zipTmp).size;
totalBytes += size;
packs += 1;
for (const item of group) {
manifest.levels[item.levelId] = {
bundle: bundleName,
uuid: item.meta.uuid,
};
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} 关 / ${packs} 包 (${packSize} 关/包), 合计 ${formatBytes(totalBytes)}, 均 ${formatBytes(Math.round(totalBytes / Math.max(packs, 1)))}/包`);
if (missing.length) {
console.warn(`>>> 警告: ${missing.length} 关在 config 中无 import 文件 (例: ${missing.slice(0, 5).join(', ')})`);
}
return { manifest, packed, packs, packSize, missing, totalBytes };
}
module.exports = {
DEFAULT_LEVELS_PER_PACK,
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);
}