no message

This commit is contained in:
2026-06-18 14:07:38 +08:00
parent d393302388
commit 18990deb2d
12 changed files with 910 additions and 116 deletions

View File

@@ -12,13 +12,16 @@ const zlib = require('zlib');
const { execSync } = require('child_process');
const {
patchPreloadSettings,
patchSplashSettings,
printPackageReport,
minifyLevelsDatabase,
brotliCompressFile,
brotliCompressWebglBundles,
formatBytes,
} = require('./package-optimize');
const { listRuntimeFiles, assertRuntimePack } = require('./runtime-pack');
const { splitLevelBundles } = require('./split-level-bundles');
const { splitLevelsDatabase } = require('./split-levels-database');
const buildDir = path.resolve(process.argv[2]);
const outDir = path.resolve(process.argv[3]);
@@ -83,9 +86,11 @@ function attachLevelsDatabase(outDir) {
console.warn('>>> 警告: 未找到 levels-database.json');
return;
}
splitLevelsDatabase(levelsDbSrc, outDir);
const levelsDbDst = path.join(outDir, 'levels-database.json');
const { before, after } = minifyLevelsDatabase(levelsDbSrc, levelsDbDst);
console.log(`>>> levels-database.json 压缩: ${formatBytes(before)}${formatBytes(after)}`);
console.log(`>>> levels-database.json (legacy 回退): ${formatBytes(before)}${formatBytes(after)}`);
const { raw, br } = brotliCompressFile(levelsDbDst, path.join(outDir, 'levels-database.json.br'));
console.log(`>>> levels-database.json.br: ${formatBytes(raw)}${formatBytes(br)}`);
}
@@ -139,6 +144,56 @@ function patchMainIndexForSplitLoad(mainIndexPath) {
}
let s = fs.readFileSync(mainIndexPath, 'utf8');
const rules = [
{
desc: 'AppBootstrap: 首屏只 loadLevelDatabase + bounds',
old: 'yield I(),yield _(),yield E(),yield A(),L(),yield D.preload();',
neu: 'yield A(),L();',
},
{
desc: 'GameController: 进关前按需拉 assets / 关卡库分片',
old: 'if(!this.creating){this.creating=!0;var t=R(e);',
neu: 'if(!this.creating){this.creating=!0;try{if(globalThis.__tfrhEnsureAssetsCore)yield globalThis.__tfrhEnsureAssetsCore();yield I(),yield _(),yield E();if(globalThis.__tfrhEnsureLevelDbShard)yield globalThis.__tfrhEnsureLevelDbShard(e);}catch(err){console.error("[GameController] 运行时资源加载失败",err),this.creating=!1;return}var t=R(e);',
},
{
desc: 'LevelDatabase: 分片索引模式 isReady',
old: 'isLevelDatabaseReady:function(){return null!==s}',
neu: 'isLevelDatabaseReady:function(){return null!==s||("undefined"!=typeof window&&!!window.__tfrhLevelsDbIndex)}',
},
{
desc: 'LevelDatabase: 启动读 index 而非全量库',
old: 'try{if("undefined"!=typeof window&&window.__tfrhLevelsDatabaseJson)',
neu: 'try{if("undefined"!=typeof window&&window.__tfrhLevelsDbIndex){s={version:window.__tfrhLevelsDbIndex.version||2,levels:{}},console.log("[LevelDatabase] 已加载索引 "+window.__tfrhLevelsDbIndex.total+" 关");return}if("undefined"!=typeof window&&window.__tfrhLevelsDatabaseJson)',
},
{
desc: 'LevelDatabase: validate 接受 index',
old: 'function w(){var e=c.length,n=null!=(t=c[0])?t:0;if(e<100||n<a)throw new Error',
neu: 'function w(){if("undefined"!=typeof window&&window.__tfrhLevelsDbIndex){var e=window.__tfrhLevelsDbIndex;if(e.total<100||e.min<a)throw new Error("关卡库索引过旧 ("+e.total+" 关)");return}var e=c.length,n=null!=(t=c[0])?t:0;if(e<100||n<a)throw new Error',
},
{
desc: 'LevelRegistry: min/max 读 index',
old: 'function f(){return c()?d():t}function E(){return c()?I():t}',
neu: 'function f(){var e="undefined"!=typeof window&&window.__tfrhLevelsDbIndex;return e?e.min:c()?d():t}function E(){var e="undefined"!=typeof window&&window.__tfrhLevelsDbIndex;return e?e.max:c()?I():t}',
},
{
desc: 'LevelRegistry: hasLevel 读 index 范围',
old: 'hasLevel:function(e){return v(e)}',
neu: 'hasLevel:function(e){if(v(e))return!0;var n="undefined"!=typeof window&&window.__tfrhLevelsDbIndex;return!!n&&e>=n.min&&e<=n.max}',
},
{
desc: 'LevelDatabase: getLevelConfig 读 loader 分片',
old: 'getLevelConfig:function(e){var n;return null!=(n=f[e])?n:null}',
neu: 'getLevelConfig:function(e){var n;if(null!=(n=f[e]))return n;var t="undefined"!=typeof window&&window.__tfrhLevelsDatabaseJson&&window.__tfrhLevelsDatabaseJson.levels;return t&&t[String(e)]||null}',
},
{
desc: 'GameController: 使用 loader 全局 loadLevelPrefab hook (新构建 K)',
old: 'var l,r=yield K(i);',
neu: 'var l,r=yield(globalThis.__tfrhLoadLevelPrefab||K)(i);',
},
{
desc: 'GameController: 使用 loader 全局 loadLevelPrefab hook (旧构建 z)',
old: 'var l,r=yield z(i);',
neu: 'var l,r=yield(globalThis.__tfrhLoadLevelPrefab||z)(i);',
},
{
desc: 'AppBootstrap: 首关前不 preload UI 贴图',
old: 'yield h.preload(),yield O.preload()',
@@ -256,10 +311,14 @@ patchText(path.join(scenesStage, 'src', 'settings.json'), (t) => {
j.assets = j.assets || {};
j.assets.server = '';
patchPreloadSettings(j, { preloadResources: false, preloadLevelPrefabs: false });
patchSplashSettings(j, { stripSplashAssets: true });
return JSON.stringify(j);
});
zipDir(scenesStage, path.join(webglDir, BUNDLE.scenesAll));
const scenesZip = path.join(webglDir, BUNDLE.scenesAll);
const scenesBr = brotliCompressFile(scenesZip, scenesZip + '.br');
console.log(`>>> ${BUNDLE.scenesAll}.br: ${formatBytes(scenesBr.raw)}${formatBytes(scenesBr.br)}`);
// —— 3a. assets_all核心资源不含 level-prefabs——
// 默认分包MERGE_LEVELS=1 时合并 level-prefabs 进 assets_all不推荐
@@ -299,6 +358,8 @@ if (fs.existsSync(path.join(scenesStage, 'src', 'effect.bin'))) {
const assetsCoreZip = path.join(webglDir, BUNDLE.assetsAll);
zipDir(assetsCoreStage, assetsCoreZip);
console.log(`>>> assets_core (assets_all): ${formatBytes(fs.statSync(assetsCoreZip).size)}`);
const assetsBr = brotliCompressFile(assetsCoreZip, assetsCoreZip + '.br');
console.log(`>>> ${BUNDLE.assetsAll}.br: ${formatBytes(assetsBr.raw)}${formatBytes(assetsBr.br)}`);
// —— 3b. 每关独立 bundle + levels-manifest.json ——
const levelPrefabsSrc = path.join(buildDir, 'assets', 'level-prefabs');
@@ -312,6 +373,8 @@ if (!MERGE_LEVELS) {
patchCatalogRemoveLevelsBundle(path.join(aaDir, 'catalog.json'));
const manifestPath = path.join(aaDir, 'levels-manifest.json');
levelPackStats = splitLevelBundles(levelPrefabsSrc, webglDir, manifestPath, tmp);
const brStats = brotliCompressWebglBundles(webglDir);
console.log(`>>> bundle Brotli: ${brStats.count} 个, 节省 ${formatBytes(brStats.saved)}`);
}
attachLevelsDatabase(outDir);
@@ -343,7 +406,14 @@ rmrf(standaloneDir);
mkdirp(standaloneDir);
copyFile(path.join(unityRef, 'index.html'), path.join(standaloneDir, 'index.html'));
copyDir(path.join(unityRef, 'TemplateData'), path.join(standaloneDir, 'TemplateData'));
for (const item of ['Build', 'StreamingAssets', ...['levels-database.json', 'levels-database.json.br']]) {
for (const item of [
'Build',
'StreamingAssets',
'levels-database.json',
'levels-database.json.br',
'levels-db-index.json',
'levels-db-index.json.br',
]) {
const src = path.join(outDir, item);
const dst = path.join(standaloneDir, item);
if (!fs.existsSync(src)) continue;
@@ -372,6 +442,6 @@ for (const p of requiredBundles) {
const preloadNote = MERGE_LEVELS
? 'scenes ∥ assets_all 合并包 (MERGE_LEVELS=1)'
: `scenes ∥ assets_core 首屏;关卡包进关按需 (${levelPackStats ? levelPackStats.packed : '?'} 关)`;
: `scenes ∥ assets_core 首屏(引擎启动必需);关卡库分片 + 关卡 bundle 进关按需 (${levelPackStats ? levelPackStats.packed : '?'} 关)`;
console.log('\n完成。运行时包 → 本地 import-to-unity.sh / OSS unitycdndir同一目录');
printPackageReport(outDir, { preloadNote });

View File

@@ -25,6 +25,7 @@ const {
minifyLevelsDatabase,
brotliCompressFile,
patchPreloadSettings,
patchSplashSettings,
printPackageReport,
formatBytes,
} = require('./package-optimize');
@@ -201,6 +202,7 @@ function patchSettingsForFrontend(settingsFile) {
preloadResources: opts.preloadResources,
preloadLevelPrefabs: opts.preloadLevelPrefabs,
});
patchSplashSettings(j, { stripSplashAssets: true });
if (j.rendering) {
j.rendering.effectSettingsPath = `${UNITY_BASE}src/effect.bin`;
}

View File

@@ -32,10 +32,11 @@ usage() {
echo " --skip-manifest 不生成 deploy/ 清单" >&2
echo " --zip 额外生成 build/mstest5-runtime.zip" >&2
echo "" >&2
echo "默认分包: assets_all 首屏含 level-prefabs 壳;每关独立 bundle 进关按需" >&2
echo "默认分包: scenes 首屏;assets_all / 关卡库分片 / 每关 bundle 进关按需" >&2
echo " MERGE_LEVELS=1 合并 level-prefabs 进 assets_all不推荐" >&2
echo "运行时包结构(本地 static/unity = OSS unitycdndir:" >&2
echo " Build/ StreamingAssets/ levels-database.json(.br)" >&2
echo " Build/ StreamingAssets/ levels-db-index.json(.br) levels-database.json(.br)" >&2
echo " 首屏: scenes_all + assets_all进关: 关卡库分片 + 关卡 bundle" >&2
echo "" >&2
echo "步骤 2: scratch-gui/static/unity/import-to-unity.sh" >&2
}

View File

@@ -3,8 +3,10 @@
* 供 package-for-project.js / package-for-cdn.js 共用。
*/
const fs = require('fs');
const os = require('os');
const path = require('path');
const zlib = require('zlib');
const { execSync } = require('child_process');
function formatBytes(n) {
if (n >= 1024 * 1024) return `${(n / 1024 / 1024).toFixed(1)} MB`;
@@ -46,6 +48,23 @@ function brotliCompressFile(srcPath, dstPath, quality = 9) {
return { raw: input.length, br: out.length };
}
/** 为 WebGL 目录下所有 .bundle 生成 .bundle.br */
function brotliCompressWebglBundles(webglDir, opts = {}) {
if (!fs.existsSync(webglDir)) return { count: 0, saved: 0 };
let count = 0;
let saved = 0;
for (const name of fs.readdirSync(webglDir)) {
if (!name.endsWith('.bundle') || name.endsWith('.bundle.br')) continue;
const src = path.join(webglDir, name);
if (!fs.statSync(src).isFile()) continue;
const { raw, br } = brotliCompressFile(src, src + '.br', opts.quality);
count += 1;
saved += raw - br;
console.log(`>>> ${name}.br: ${formatBytes(raw)}${formatBytes(br)}`);
}
return { count, saved };
}
/**
* 调整预加载 bundle默认只预加载 mainresources / level-prefabs 按需加载。
* @param {object} opts
@@ -68,6 +87,50 @@ function patchPreloadSettings(settingsObj, opts = {}) {
return settingsObj;
}
/**
* 关闭 Cocos 启动闪屏("Created with Cocos")。
* totalTime <= 0 时引擎跳过闪屏等待,首屏可快约 2s不影响关卡切换与网络下载。
* @param {object} opts
* @param {boolean} [opts.disableSplash=true]
* @param {boolean} [opts.stripSplashAssets=false] 去掉内嵌 logo/background略减 scenes 包体积
*/
function patchSplashSettings(settingsObj, opts = {}) {
const disableSplash = opts.disableSplash !== false;
if (!disableSplash) return settingsObj;
settingsObj.splashScreen = settingsObj.splashScreen || {};
settingsObj.splashScreen.totalTime = 0;
if (opts.stripSplashAssets) {
delete settingsObj.splashScreen.logo;
delete settingsObj.splashScreen.background;
}
return settingsObj;
}
/**
* 就地修改 zip bundle 内的 settings.json必须用 zip -0 storeloader 不支持 deflate
*/
function patchSplashInZipBundle(zipPath, opts = {}) {
const absZip = path.resolve(zipPath);
const tmp = fs.mkdtempSync(path.join(os.tmpdir(), 'cocos-splash-'));
try {
execSync(`unzip -q -o "${absZip}" -d "${tmp}"`, { stdio: 'pipe' });
const settingsPath = path.join(tmp, 'src', 'settings.json');
if (!fs.existsSync(settingsPath)) return false;
const j = JSON.parse(fs.readFileSync(settingsPath, 'utf8'));
patchSplashSettings(j, opts);
fs.writeFileSync(settingsPath, JSON.stringify(j));
const newZip = absZip + '.new';
if (fs.existsSync(newZip)) fs.unlinkSync(newZip);
execSync(`cd "${tmp}" && zip -0 -q -r "${newZip}" .`, { stdio: 'pipe' });
fs.renameSync(newZip, absZip);
return true;
} finally {
fs.rmSync(tmp, { recursive: true, force: true });
}
}
function printPackageReport(outDir, opts = {}) {
const lines = ['\n>>> 包体积报告:'];
const entries = [
@@ -101,6 +164,9 @@ module.exports = {
fileSize,
minifyLevelsDatabase,
brotliCompressFile,
brotliCompressWebglBundles,
patchPreloadSettings,
patchSplashSettings,
patchSplashInZipBundle,
printPackageReport,
};

View File

@@ -6,13 +6,21 @@
* StreamingAssets/
* levels-database.json
* levels-database.json.br
* levels-db-index.json
* levels-db-index.json.br
* StreamingAssets/aa/levels-db/
*
* 不含 index.html / TemplateData仅 standalone-player 独立调试页使用)
*/
const fs = require('fs');
const path = require('path');
const RUNTIME_ROOT_FILES = ['levels-database.json', 'levels-database.json.br'];
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 = '') {
@@ -62,6 +70,9 @@ function assertRuntimePack(packDir, opts) {
if (!fs.existsSync(catalog)) {
throw new Error(`缺少运行时包: ${catalog}`);
}
if (!fs.existsSync(path.join(packDir, 'levels-db-index.json'))) {
throw new Error(`缺少运行时包: ${path.join(packDir, 'levels-db-index.json')}`);
}
if (!fs.existsSync(path.join(packDir, 'levels-database.json'))) {
throw new Error(`缺少运行时包: ${path.join(packDir, 'levels-database.json')}`);
}

View File

@@ -0,0 +1,94 @@
#!/usr/bin/env node
/**
* 将 levels-database.json 拆为 index + 分片,首屏只下 index~几 KB
*
* 输出:
* <outDir>/levels-db-index.json (+ .br)
* <outDir>/StreamingAssets/aa/levels-db/<min>-<max>.json (+ .br)
*/
const fs = require('fs');
const path = require('path');
const { brotliCompressFile, formatBytes } = require('./package-optimize');
const DEFAULT_SHARD_SIZE = 100;
function mkdirp(p) { fs.mkdirSync(p, { recursive: true }); }
/**
* @param {string} srcPath assets/level-data/levels-database.json
* @param {string} outDir 运行时包根目录
* @param {{ shardSize?: number }} opts
*/
function splitLevelsDatabase(srcPath, outDir, opts = {}) {
const shardSize = opts.shardSize || DEFAULT_SHARD_SIZE;
const raw = JSON.parse(fs.readFileSync(srcPath, 'utf8'));
const levels = raw.levels || {};
const ids = Object.keys(levels)
.map((k) => parseInt(k, 10))
.filter((n) => !Number.isNaN(n))
.sort((a, b) => a - b);
if (ids.length < 1) {
throw new Error(`关卡库为空: ${srcPath}`);
}
const min = ids[0];
const max = ids[ids.length - 1];
const shardDir = path.join(outDir, 'StreamingAssets', 'aa', 'levels-db');
mkdirp(shardDir);
const shards = [];
for (let start = min; start <= max; start += shardSize) {
const end = Math.min(start + shardSize - 1, max);
const slice = {};
for (const id of ids) {
if (id >= start && id <= end) {
slice[String(id)] = levels[String(id)];
}
}
if (!Object.keys(slice).length) continue;
const fileName = `${start}-${end}.json`;
const relFile = `levels-db/${fileName}`;
const absPath = path.join(shardDir, fileName);
fs.writeFileSync(absPath, JSON.stringify({ levels: slice }), 'utf8');
const br = brotliCompressFile(absPath, absPath + '.br');
console.log(`>>> levels-db/${fileName}: ${formatBytes(br.raw)} → .br ${formatBytes(br.br)}`);
shards.push({ min: start, max: end, file: relFile, count: Object.keys(slice).length });
}
const index = {
version: 2,
mode: 'sharded',
shardSize,
levelIdBase: raw.levelIdBase,
generatedAt: raw.generatedAt || new Date().toISOString(),
source: raw.source || 'split-levels-database',
total: ids.length,
min,
max,
stats: raw.stats,
shards,
};
const indexPath = path.join(outDir, 'levels-db-index.json');
fs.writeFileSync(indexPath, JSON.stringify(index), 'utf8');
const indexBr = brotliCompressFile(indexPath, indexPath + '.br');
console.log(`>>> levels-db-index.json: ${formatBytes(indexBr.raw)} → .br ${formatBytes(indexBr.br)}`);
console.log(`>>> 关卡库分片: ${shards.length} 片, ${ids.length} 关 (${min}${max})`);
return { index, shards, total: ids.length };
}
module.exports = { splitLevelsDatabase, DEFAULT_SHARD_SIZE };
if (require.main === module) {
const src = path.resolve(process.argv[2]);
const out = path.resolve(process.argv[3]);
if (!src || !out) {
console.error('Usage: split-levels-database.js <levels-database.json> <outDir>');
process.exit(1);
}
splitLevelsDatabase(src, out);
}