no message
This commit is contained in:
@@ -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 });
|
||||
|
||||
@@ -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`;
|
||||
}
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
@@ -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:默认只预加载 main,resources / 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 store,loader 不支持 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,
|
||||
};
|
||||
|
||||
@@ -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')}`);
|
||||
}
|
||||
|
||||
94
tools/split-levels-database.js
Normal file
94
tools/split-levels-database.js
Normal 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);
|
||||
}
|
||||
Reference in New Issue
Block a user