Adds level prefabs, theme assets, audio, extensions, and deployment scripts for the Unity WebGL migration. Co-authored-by: Cursor <cursoragent@cursor.com>
156 lines
5.1 KiB
JavaScript
156 lines
5.1 KiB
JavaScript
#!/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);
|
||
}
|