/** * 统一运行时包定义 — 本地 static/unity 与 OSS unitycdndir 内容完全一致。 * * 运行时包(唯一真相): * Build/ * StreamingAssets/ * levels-db-index.json * levels-db-index.json.br * StreamingAssets/aa/levels-db/ (默认仅 .json.br) * 可选: levels-database.json(.br) — KEEP_LEGACY_DB=1 * * 不含 index.html / TemplateData(仅 standalone-player 独立调试页使用) */ const fs = require('fs'); const path = require('path'); const RUNTIME_ROOT_FILES = [ 'levels-database.json', 'levels-database.json.br', 'levels-db-index.json', 'levels-db-index.json.br', ]; const RUNTIME_DIRS = ['Build', 'StreamingAssets']; function walkFiles(root, bucket, prefix = '') { if (!fs.existsSync(root)) return; for (const ent of fs.readdirSync(root, { withFileTypes: true })) { if (ent.name === '.DS_Store') continue; const rel = prefix ? `${prefix}/${ent.name}` : ent.name; const full = path.join(root, ent.name); if (ent.isDirectory()) walkFiles(full, bucket, rel); else bucket.push({ rel, full, size: fs.statSync(full).size }); } } /** 列出运行时包内所有相对路径(用于 manifest / 校验) */ function listRuntimeFiles(packDir) { const files = []; for (const dir of RUNTIME_DIRS) { walkFiles(path.join(packDir, dir), files, dir); } for (const name of RUNTIME_ROOT_FILES) { const full = path.join(packDir, name); if (fs.existsSync(full)) { files.push({ rel: name, full, size: fs.statSync(full).size }); } } return files.sort((a, b) => a.rel.localeCompare(b.rel)); } function existsPlainOrBr(filePath) { if (fs.existsSync(filePath) && fs.statSync(filePath).size > 0) return true; const br = `${filePath}.br`; return fs.existsSync(br) && fs.statSync(br).size > 0; } function readJsonPlainOrBr(filePath) { if (fs.existsSync(filePath)) { return JSON.parse(fs.readFileSync(filePath, 'utf8')); } throw new Error(`缺少 JSON: ${filePath}`); } function bundleNamesFromCatalog(catalogPath) { if (!fs.existsSync(catalogPath)) return []; const catalog = JSON.parse(fs.readFileSync(catalogPath, 'utf8')); return [...new Set( (catalog.m_InternalIds || []) .filter((id) => String(id).includes('.bundle')) .map((id) => String(id).split('/').pop()), )].sort(); } function assertRuntimePack(packDir, opts) { opts = opts || {}; const loader = path.join(packDir, 'Build/mstest5.loader.js'); const catalog = path.join(packDir, 'StreamingAssets/aa/catalog.json'); const levelsManifest = path.join(packDir, 'StreamingAssets/aa/levels-manifest.json'); if (!fs.existsSync(loader)) { throw new Error(`缺少运行时包: ${loader}`); } if (!existsPlainOrBr(catalog)) { throw new Error(`缺少运行时包: ${catalog}(.br)`); } if (!existsPlainOrBr(path.join(packDir, 'levels-db-index.json'))) { throw new Error(`缺少运行时包: ${path.join(packDir, 'levels-db-index.json')}(.br)`); } const names = fs.existsSync(catalog) ? bundleNamesFromCatalog(catalog) : []; if (opts.requireLevelsBundle && !names.some((n) => n.includes('levels_all'))) { throw new Error('catalog 缺少 levels_all 分包'); } if (opts.requireLevelsManifest && !existsPlainOrBr(levelsManifest)) { throw new Error(`缺少运行时包: ${levelsManifest}(.br)`); } if (opts.requireLevelsManifest && fs.existsSync(levelsManifest)) { const manifest = readJsonPlainOrBr(levelsManifest); const count = Object.keys(manifest.levels || {}).length; if (count < 1) throw new Error('levels-manifest.json 无关卡条目'); } for (const name of names) { const p = path.join(packDir, 'StreamingAssets/aa/WebGL', name); if (!existsPlainOrBr(p)) { throw new Error(`bundle 无效: ${p}(.br)`); } const br = `${p}.br`; const sz = fs.existsSync(p) ? fs.statSync(p).size : fs.statSync(br).size; if (sz < 50) throw new Error(`bundle 过小: ${p}`); } } module.exports = { RUNTIME_ROOT_FILES, RUNTIME_DIRS, walkFiles, listRuntimeFiles, existsPlainOrBr, bundleNamesFromCatalog, assertRuntimePack, };