Merge pull request 'no message' (#1) from cursor/cocos-port-unity-webgl into main
Reviewed-on: #1
This commit was merged in pull request #1.
This commit is contained in:
@@ -7,12 +7,8 @@ import { ViewController } from './controller/ViewController';
|
||||
import { LineGridRenderer } from './gameplay/LineGridRenderer';
|
||||
import { UIMain } from './ui/UIMain';
|
||||
import { loadLevelDatabase, refreshLevelIdBounds, LEVEL_ID_BASE } from './level/LevelRegistry';
|
||||
import { loadThemeDatabase } from './theme/ThemeRegistry';
|
||||
import { ensureResourcesBundle } from './core/ResourcesBundle';
|
||||
import { loadTileDisplayMeta } from './visual/TileDisplayMeta';
|
||||
import { ThemeBackground } from './theme/ThemeBackground';
|
||||
import { GridSnapHelper } from './level/GridSnapHelper';
|
||||
import { GameAudio } from './audio/GameAudio';
|
||||
import { GameplayDebugBar } from './ui/GameplayDebugBar';
|
||||
import {
|
||||
CELL_PIXEL, CAMERA_ORTHO_HALF, DESIGN_WIDTH, DESIGN_HEIGHT,
|
||||
@@ -55,12 +51,8 @@ export class AppBootstrap extends Component {
|
||||
console.log('[AppBootstrap] 开始初始化…');
|
||||
applyEmbeddedDesignResolution();
|
||||
view.on('canvas-resize', this.onCanvasResize, this);
|
||||
await ensureResourcesBundle();
|
||||
await loadThemeDatabase();
|
||||
await loadTileDisplayMeta();
|
||||
await loadLevelDatabase();
|
||||
refreshLevelIdBounds();
|
||||
await GameAudio.preload();
|
||||
|
||||
const scene = director.getScene()!;
|
||||
find('UICanvas', scene)?.destroy();
|
||||
@@ -108,7 +100,7 @@ export class AppBootstrap extends Component {
|
||||
// 关卡预制体由 SwitchLevel → createNewLevel 按需加载(loader 进关再下 levels_all)
|
||||
ctl.markReady();
|
||||
ctl.onBootstrapReady();
|
||||
console.log('[AppBootstrap] 引擎已就绪;SwitchLevel 进关时再加载关卡预制体');
|
||||
console.log('[AppBootstrap] 引擎已就绪;SwitchLevel 进关时再加载资源/关卡库分片/预制体');
|
||||
}
|
||||
|
||||
private ensureGameRoot(scene: Node, size: { width: number; height: number }, mainCam: Camera): Node {
|
||||
|
||||
@@ -14,6 +14,7 @@ import {
|
||||
} from './core/Define';
|
||||
import { EventManager, EventType } from './core/EventManager';
|
||||
import { ExternalLevelInfo, JsBridge } from './bridge/JsBridge';
|
||||
import { ensureRuntimeAssetsForLevel } from './core/RuntimePack';
|
||||
import {
|
||||
hasLevel, getMaxLevelId, getMinLevelId, LEVEL_ID_BASE, resolveLevelConfig,
|
||||
nextLevelId, prevLevelId,
|
||||
@@ -25,7 +26,7 @@ import {
|
||||
} from './level/EntitySpawnPlacement';
|
||||
import { EntityVisualOptions } from './visual/EntityTextureResolver';
|
||||
import { getLevelPrefabResourcePath } from './level/LevelPrefabPaths';
|
||||
import { loadLevelPrefab } from './level/LevelPrefabLoader';
|
||||
import { loadLevelPrefab as loadLevelPrefabImpl } from './level/LevelPrefabLoader';
|
||||
import { PlayerController } from './controller/PlayerController';
|
||||
import { VehicleController } from './controller/VehicleController';
|
||||
import { PropController } from './controller/PropController';
|
||||
@@ -52,6 +53,13 @@ interface GridEntry {
|
||||
node: Node;
|
||||
}
|
||||
|
||||
/** Web 主站:优先走 loader 注入的 loadLevelPrefab(按关下载 + 内存 bundle) */
|
||||
function loadLevelPrefabForRuntime(path: string) {
|
||||
const hook = (globalThis as { __tfrhLoadLevelPrefab?: typeof loadLevelPrefabImpl }).__tfrhLoadLevelPrefab;
|
||||
if (typeof hook === 'function') return hook(path);
|
||||
return loadLevelPrefabImpl(path);
|
||||
}
|
||||
|
||||
/**
|
||||
* 主站唯一入口组件(原 GameManager + GameController 合并)。
|
||||
* Inspector:填写 inputLevel,预览 ▶ 后点 SwitchLevel(对齐 Unity TestGame2)。
|
||||
@@ -793,6 +801,13 @@ export class GameController extends Component {
|
||||
async createNewLevel(levelID: number) {
|
||||
if (this.creating) return;
|
||||
this.creating = true;
|
||||
try {
|
||||
await ensureRuntimeAssetsForLevel(levelID);
|
||||
} catch (e) {
|
||||
console.error('[GameController] 运行时资源加载失败', e);
|
||||
this.creating = false;
|
||||
return;
|
||||
}
|
||||
const config = resolveLevelConfig(levelID);
|
||||
if (!config || !this.mainLevelEntrance) {
|
||||
this.creating = false;
|
||||
@@ -809,7 +824,7 @@ export class GameController extends Component {
|
||||
|
||||
const path = getLevelPrefabResourcePath(levelID, config);
|
||||
try {
|
||||
const prefab = await loadLevelPrefab(path);
|
||||
const prefab = await loadLevelPrefabForRuntime(path);
|
||||
await VisualAssets.preload(this.uiStyle);
|
||||
// 在 instantiate 前无法拦截;先禁用 prefab 内 GridSnapHelper 的 showGrid
|
||||
GridSnapHelper.stripBeforePlayFromPrefab(prefab);
|
||||
|
||||
40
assets/scripts/core/RuntimePack.ts
Normal file
40
assets/scripts/core/RuntimePack.ts
Normal file
@@ -0,0 +1,40 @@
|
||||
/**
|
||||
* 进关前按需加载:assets_all、主题/贴图元数据、关卡库分片。
|
||||
* 由 loader 注入 __tfrhEnsureAssetsCore / __tfrhEnsureLevelDbShard。
|
||||
*/
|
||||
import { ensureResourcesBundle } from './ResourcesBundle';
|
||||
import { loadThemeDatabase } from '../theme/ThemeRegistry';
|
||||
import { loadTileDisplayMeta } from '../visual/TileDisplayMeta';
|
||||
import { ensureLevelShardLoaded } from '../level/LevelDatabase';
|
||||
|
||||
declare global {
|
||||
interface Window {
|
||||
__tfrhEnsureAssetsCore?: (onProgress?: (frac: number) => void) => Promise<void>;
|
||||
__tfrhEnsureLevelDbShard?: (levelId: number) => Promise<void>;
|
||||
__tfrhEnsureLevelPack?: (levelId: number) => Promise<void>;
|
||||
}
|
||||
}
|
||||
|
||||
let runtimePrimed = false;
|
||||
|
||||
/** 首关 SwitchLevel 前拉齐运行时依赖(bootstrap 不再预载) */
|
||||
export async function ensureRuntimeAssetsForLevel(levelId: number): Promise<void> {
|
||||
if (typeof window !== 'undefined') {
|
||||
if (window.__tfrhEnsureAssetsCore) {
|
||||
await window.__tfrhEnsureAssetsCore();
|
||||
}
|
||||
if (window.__tfrhEnsureLevelDbShard) {
|
||||
await window.__tfrhEnsureLevelDbShard(levelId);
|
||||
}
|
||||
if (window.__tfrhEnsureLevelPack) {
|
||||
await window.__tfrhEnsureLevelPack(levelId);
|
||||
}
|
||||
}
|
||||
await ensureResourcesBundle();
|
||||
if (!runtimePrimed) {
|
||||
await loadThemeDatabase();
|
||||
await loadTileDisplayMeta();
|
||||
runtimePrimed = true;
|
||||
}
|
||||
await ensureLevelShardLoaded(levelId);
|
||||
}
|
||||
9
assets/scripts/core/RuntimePack.ts.meta
Normal file
9
assets/scripts/core/RuntimePack.ts.meta
Normal file
@@ -0,0 +1,9 @@
|
||||
{
|
||||
"ver": "4.0.24",
|
||||
"importer": "typescript",
|
||||
"imported": true,
|
||||
"uuid": "2de0b14b-7d3b-4ac4-963c-f7b352db60c9",
|
||||
"files": [],
|
||||
"subMetas": {},
|
||||
"userData": {}
|
||||
}
|
||||
@@ -21,19 +21,41 @@ export interface LevelDatabaseFile {
|
||||
levels: Record<string, LevelConfig>;
|
||||
}
|
||||
|
||||
export interface LevelsDbIndex {
|
||||
version: number;
|
||||
mode: 'sharded';
|
||||
shardSize?: number;
|
||||
levelIdBase?: number;
|
||||
generatedAt?: string;
|
||||
source?: string;
|
||||
total: number;
|
||||
min: number;
|
||||
max: number;
|
||||
stats?: LevelDatabaseFile['stats'];
|
||||
shards: Array<{ min: number; max: number; file: string; count?: number }>;
|
||||
}
|
||||
|
||||
declare global {
|
||||
interface Window {
|
||||
/** 主站 scratch-gui 注入:/unity/levels-database.json */
|
||||
__tfrhLevelsDatabaseUrl?: string;
|
||||
/** loader 预注入的 JSON(可选) */
|
||||
/** loader 预注入的 JSON(可选,legacy 全量) */
|
||||
__tfrhLevelsDatabaseJson?: LevelDatabaseFile;
|
||||
/** 分片索引 URL / 预注入 */
|
||||
__tfrhLevelsDbIndexUrl?: string;
|
||||
__tfrhLevelsDbIndex?: LevelsDbIndex;
|
||||
/** loader 按关拉取分片 */
|
||||
__tfrhEnsureLevelDbShard?: (levelId: number) => Promise<void>;
|
||||
}
|
||||
}
|
||||
|
||||
let fileCache: LevelDatabaseFile | null = null;
|
||||
let indexCache: LevelsDbIndex | null = null;
|
||||
let levelsMap: Record<number, LevelConfig> = {};
|
||||
let sortedIds: number[] = [];
|
||||
let loadPromise: Promise<void> | null = null;
|
||||
const loadedShardKeys = new Set<string>();
|
||||
const shardLoadPromises = new Map<string, Promise<void>>();
|
||||
|
||||
function rebuildIndex() {
|
||||
sortedIds = Object.keys(levelsMap)
|
||||
@@ -42,18 +64,62 @@ function rebuildIndex() {
|
||||
.sort((a, b) => a - b);
|
||||
}
|
||||
|
||||
function ingestFile(data: LevelDatabaseFile) {
|
||||
function ingestFile(data: LevelDatabaseFile, merge = false) {
|
||||
if (!merge) {
|
||||
fileCache = data;
|
||||
levelsMap = {};
|
||||
} else if (!fileCache) {
|
||||
fileCache = {
|
||||
version: data.version || 1,
|
||||
levels: {},
|
||||
};
|
||||
}
|
||||
for (const [k, cfg] of Object.entries(data.levels ?? {})) {
|
||||
const id = parseInt(k, 10);
|
||||
if (!Number.isNaN(id)) {
|
||||
levelsMap[id] = { ...cfg, levelID: id };
|
||||
if (fileCache?.levels) {
|
||||
fileCache.levels[String(id)] = levelsMap[id];
|
||||
}
|
||||
}
|
||||
}
|
||||
rebuildIndex();
|
||||
}
|
||||
|
||||
function ingestIndex(data: LevelsDbIndex) {
|
||||
indexCache = data;
|
||||
if (!fileCache) {
|
||||
fileCache = {
|
||||
version: data.version,
|
||||
generatedAt: data.generatedAt,
|
||||
source: data.source,
|
||||
levelIdBase: data.levelIdBase,
|
||||
stats: data.stats,
|
||||
levels: {},
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
function findShardForLevel(levelId: number): LevelsDbIndex['shards'][0] | null {
|
||||
if (!indexCache?.shards?.length) return null;
|
||||
for (const shard of indexCache.shards) {
|
||||
if (levelId >= shard.min && levelId <= shard.max) return shard;
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
function shardKey(shard: { file: string }) {
|
||||
return shard.file;
|
||||
}
|
||||
|
||||
function validateIndex(index: LevelsDbIndex): void {
|
||||
if (index.total < 100 || index.min < LEVEL_ID_BASE) {
|
||||
throw new Error(
|
||||
`关卡库索引过旧 (${index.total} 关),请重新 package-for-project`,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
function resolveRemoteUrl(): string | null {
|
||||
if (typeof window === 'undefined') return null;
|
||||
const url = window.__tfrhLevelsDatabaseUrl;
|
||||
@@ -61,6 +127,10 @@ function resolveRemoteUrl(): string | null {
|
||||
}
|
||||
|
||||
function validateIngested(): void {
|
||||
if (indexCache) {
|
||||
validateIndex(indexCache);
|
||||
return;
|
||||
}
|
||||
const total = sortedIds.length;
|
||||
const minId = sortedIds[0] ?? 0;
|
||||
if (total < 100 || minId < LEVEL_ID_BASE) {
|
||||
@@ -90,6 +160,139 @@ function fetchCandidates(): string[] {
|
||||
return [...new Set(out)];
|
||||
}
|
||||
|
||||
function indexFetchCandidates(): string[] {
|
||||
const out: string[] = [];
|
||||
if (typeof window !== 'undefined') {
|
||||
const configured = window.__tfrhLevelsDbIndexUrl?.trim();
|
||||
if (configured && /^https?:\/\//i.test(configured)) {
|
||||
out.push(configured);
|
||||
}
|
||||
}
|
||||
if (canUseHttpFetch()) {
|
||||
out.push(new URL('levels-db-index.json', window.location.href).href);
|
||||
out.push(new URL('/unity/levels-db-index.json', window.location.origin).href);
|
||||
}
|
||||
return [...new Set(out)];
|
||||
}
|
||||
|
||||
async function fetchJsonWithBrotli(url: string): Promise<unknown> {
|
||||
const brUrl = url.replace(/\.json(\?.*)?$/i, '.json.br$1');
|
||||
try {
|
||||
const brRes = await fetch(brUrl);
|
||||
if (brRes.ok && typeof DecompressionStream !== 'undefined') {
|
||||
const text = await new Response(await brRes.arrayBuffer())
|
||||
.pipeThrough(new DecompressionStream('brotli'))
|
||||
.text();
|
||||
return JSON.parse(text);
|
||||
}
|
||||
} catch {
|
||||
/* fallback */
|
||||
}
|
||||
const res = await fetch(url, { credentials: 'same-origin' });
|
||||
if (!res.ok) throw new Error(`HTTP ${res.status} for ${url}`);
|
||||
return res.json();
|
||||
}
|
||||
|
||||
async function loadIndexFromNetwork(): Promise<void> {
|
||||
const candidates = indexFetchCandidates();
|
||||
if (!candidates.length) {
|
||||
throw new Error('[LevelDatabase] 无法 fetch 关卡库索引');
|
||||
}
|
||||
const errors: unknown[] = [];
|
||||
for (const url of candidates) {
|
||||
try {
|
||||
const json = await fetchJsonWithBrotli(url) as LevelsDbIndex;
|
||||
if (json?.mode !== 'sharded' || !json.shards?.length) {
|
||||
throw new Error('非分片索引');
|
||||
}
|
||||
ingestIndex(json);
|
||||
validateIngested();
|
||||
console.log(
|
||||
`[LevelDatabase] 已加载索引 ${json.total} 关 (${json.min}–${json.max})`,
|
||||
url,
|
||||
);
|
||||
return;
|
||||
} catch (e) {
|
||||
errors.push(e);
|
||||
}
|
||||
}
|
||||
throw new Error(
|
||||
`[LevelDatabase] 无法加载关卡库索引: ${candidates.join(', ')}; `
|
||||
+ errors.map((e) => String(e)).join('; '),
|
||||
);
|
||||
}
|
||||
|
||||
function resolveShardUrl(shardFile: string): string {
|
||||
const rel = shardFile.replace(/^\/+/, '');
|
||||
if (canUseHttpFetch()) {
|
||||
return new URL(`/unity/StreamingAssets/aa/${rel}`, window.location.origin).href;
|
||||
}
|
||||
return rel;
|
||||
}
|
||||
|
||||
async function loadShardFromNetwork(shard: LevelsDbIndex['shards'][0]): Promise<void> {
|
||||
const rel = shard.file.replace(/^\/+/, '');
|
||||
const url = resolveShardUrl(rel);
|
||||
const json = await fetchJsonWithBrotli(url) as LevelDatabaseFile;
|
||||
ingestFile(json, true);
|
||||
console.log(
|
||||
`[LevelDatabase] 已加载分片 ${shard.min}–${shard.max} (+${Object.keys(json.levels ?? {}).length} 关)`,
|
||||
);
|
||||
}
|
||||
|
||||
async function loadShardNow(shard: LevelsDbIndex['shards'][0]): Promise<void> {
|
||||
const key = shardKey(shard);
|
||||
if (loadedShardKeys.has(key)) return;
|
||||
const pending = shardLoadPromises.get(key);
|
||||
if (pending) {
|
||||
await pending;
|
||||
return;
|
||||
}
|
||||
const promise = (async () => {
|
||||
if (typeof window !== 'undefined' && window.__tfrhEnsureLevelDbShard) {
|
||||
await window.__tfrhEnsureLevelDbShard(shard.min);
|
||||
const json = window.__tfrhLevelsDatabaseJson;
|
||||
if (json?.levels) {
|
||||
const partial: LevelDatabaseFile = { version: json.version || 2, levels: {} };
|
||||
for (const [k, cfg] of Object.entries(json.levels)) {
|
||||
const id = parseInt(k, 10);
|
||||
if (!Number.isNaN(id) && id >= shard.min && id <= shard.max) {
|
||||
partial.levels[k] = cfg;
|
||||
}
|
||||
}
|
||||
if (Object.keys(partial.levels).length) {
|
||||
ingestFile(partial, true);
|
||||
loadedShardKeys.add(key);
|
||||
console.log(
|
||||
`[LevelDatabase] 已合并分片 ${shard.min}–${shard.max} (+${Object.keys(partial.levels).length} 关)`,
|
||||
);
|
||||
return;
|
||||
}
|
||||
}
|
||||
}
|
||||
await loadShardFromNetwork(shard);
|
||||
loadedShardKeys.add(key);
|
||||
})();
|
||||
shardLoadPromises.set(key, promise);
|
||||
try {
|
||||
await promise;
|
||||
} finally {
|
||||
shardLoadPromises.delete(key);
|
||||
}
|
||||
}
|
||||
|
||||
/** 进关前确保该关所在分片已加载 */
|
||||
export async function ensureLevelShardLoaded(levelId: number): Promise<void> {
|
||||
if (!indexCache) return;
|
||||
if (levelId in levelsMap) return;
|
||||
const shard = findShardForLevel(levelId);
|
||||
if (!shard) {
|
||||
console.warn(`[LevelDatabase] 关卡 ${levelId} 不在索引范围`);
|
||||
return;
|
||||
}
|
||||
await loadShardNow(shard);
|
||||
}
|
||||
|
||||
function loadFromRemote(url: string): Promise<void> {
|
||||
const abs = /^https?:\/\//i.test(url)
|
||||
? url
|
||||
@@ -161,12 +364,19 @@ async function waitForInjection(maxMs = 2500): Promise<LevelDatabaseFile | null>
|
||||
return null;
|
||||
}
|
||||
|
||||
/** 异步加载(AppBootstrap 启动时调用) */
|
||||
/** 异步加载(AppBootstrap 启动时调用;分片模式仅拉 index) */
|
||||
export function loadLevelDatabase(): Promise<void> {
|
||||
if (fileCache) return Promise.resolve();
|
||||
if (fileCache || indexCache) return Promise.resolve();
|
||||
if (loadPromise) return loadPromise;
|
||||
loadPromise = (async () => {
|
||||
try {
|
||||
if (typeof window !== 'undefined' && window.__tfrhLevelsDbIndex) {
|
||||
ingestIndex(window.__tfrhLevelsDbIndex);
|
||||
validateIngested();
|
||||
console.log(`[LevelDatabase] 已注入索引 ${indexCache!.total} 关`);
|
||||
return;
|
||||
}
|
||||
|
||||
if (typeof window !== 'undefined' && window.__tfrhLevelsDatabaseJson) {
|
||||
ingestFile(window.__tfrhLevelsDatabaseJson);
|
||||
validateIngested();
|
||||
@@ -196,6 +406,13 @@ export function loadLevelDatabase(): Promise<void> {
|
||||
}
|
||||
}
|
||||
|
||||
try {
|
||||
await loadIndexFromNetwork();
|
||||
return;
|
||||
} catch (e) {
|
||||
console.warn('[LevelDatabase] 分片索引不可用,回退全量库', e);
|
||||
}
|
||||
|
||||
await loadFromNetwork();
|
||||
} catch (e) {
|
||||
loadPromise = null;
|
||||
@@ -206,7 +423,11 @@ export function loadLevelDatabase(): Promise<void> {
|
||||
}
|
||||
|
||||
export function isLevelDatabaseReady(): boolean {
|
||||
return fileCache !== null;
|
||||
return fileCache !== null || indexCache !== null;
|
||||
}
|
||||
|
||||
export function isLevelDatabaseSharded(): boolean {
|
||||
return indexCache !== null;
|
||||
}
|
||||
|
||||
// --- 查 ---
|
||||
@@ -216,7 +437,11 @@ export function getLevelConfig(levelID: number): LevelConfig | null {
|
||||
|
||||
/** 是否在 Cocos 导出的关卡库中 */
|
||||
export function hasLevel(levelID: number): boolean {
|
||||
return levelID in levelsMap;
|
||||
if (levelID in levelsMap) return true;
|
||||
if (indexCache && levelID >= indexCache.min && levelID <= indexCache.max) {
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
export function getLevelIds(): number[] {
|
||||
@@ -227,19 +452,32 @@ export function getLevelCount(): number {
|
||||
return sortedIds.length;
|
||||
}
|
||||
|
||||
export const MIN_LEVEL_ID = (): number => sortedIds[0] ?? LEVEL_ID_BASE;
|
||||
export const MAX_LEVEL_ID = (): number => sortedIds[sortedIds.length - 1] ?? LEVEL_ID_BASE;
|
||||
export const MIN_LEVEL_ID = (): number => indexCache?.min ?? sortedIds[0] ?? LEVEL_ID_BASE;
|
||||
export const MAX_LEVEL_ID = (): number => indexCache?.max ?? sortedIds[sortedIds.length - 1] ?? LEVEL_ID_BASE;
|
||||
|
||||
export function nextLevelId(cur: number): number {
|
||||
if (sortedIds.length) {
|
||||
const i = sortedIds.indexOf(cur);
|
||||
if (i < 0) return sortedIds[0] ?? cur;
|
||||
return sortedIds[(i + 1) % sortedIds.length];
|
||||
}
|
||||
const max = MAX_LEVEL_ID();
|
||||
const min = MIN_LEVEL_ID();
|
||||
if (cur < min) return min;
|
||||
if (cur >= max) return min;
|
||||
return cur + 1;
|
||||
}
|
||||
|
||||
export function prevLevelId(cur: number): number {
|
||||
if (sortedIds.length) {
|
||||
const i = sortedIds.indexOf(cur);
|
||||
if (i < 0) return sortedIds[0] ?? cur;
|
||||
return sortedIds[(i - 1 + sortedIds.length) % sortedIds.length];
|
||||
}
|
||||
const max = MAX_LEVEL_ID();
|
||||
const min = MIN_LEVEL_ID();
|
||||
if (cur <= min) return max;
|
||||
return cur - 1;
|
||||
}
|
||||
|
||||
// --- 增改(运行时 / 编辑器脚本) ---
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
@@ -10,6 +10,11 @@
|
||||
var streamingBase = '';
|
||||
var bundleManifest = null;
|
||||
var levelsManifest = null;
|
||||
var levelsManifestPromise = null;
|
||||
var assetsCoreLoaded = false;
|
||||
var assetsCorePromise = null;
|
||||
var loadedLevelDbShards = new Set();
|
||||
var levelDbShardPromises = new Map();
|
||||
var levelPackPromises = new Map();
|
||||
var loadedLevelIds = new Set();
|
||||
var levelPrefabsBundlePromise = null;
|
||||
@@ -54,9 +59,8 @@
|
||||
return;
|
||||
}
|
||||
var existing = am.getBundle('level-prefabs');
|
||||
if (existing) {
|
||||
if (onComplete) onComplete(null, existing);
|
||||
return existing;
|
||||
if (existing && typeof am.removeBundle === 'function') {
|
||||
try { am.removeBundle(existing); } catch (e) { /* ignore */ }
|
||||
}
|
||||
if (levelPrefabsBundlePromise) {
|
||||
levelPrefabsBundlePromise.then(function (bundle) {
|
||||
@@ -131,6 +135,40 @@
|
||||
}
|
||||
}
|
||||
|
||||
/** Cocos bundle 常请求 import/NN/uuid.json,zip 内路径带 assets/.../import/ 前缀(NN 为 uuid 前 2 位 hex) */
|
||||
var IMPORT_SHARD = '[0-9a-fA-F]{2}';
|
||||
function resolveImportFileKey(pathname) {
|
||||
if (!pathname) return null;
|
||||
var p = String(pathname).replace(/\\/g, '/').replace(/^\/+/, '');
|
||||
var shardRe = new RegExp('^unity/assets/(level-prefabs|resources|main|internal)/(' + IMPORT_SHARD + ')/(.+)$', 'i');
|
||||
p = p.replace(shardRe, function (_, bundle, dir, file) {
|
||||
return 'assets/' + bundle + '/import/' + dir + '/' + file;
|
||||
});
|
||||
if (files.has(p)) return p;
|
||||
var bundleImport = new RegExp('^assets/(level-prefabs|resources|main|internal)/(' + IMPORT_SHARD + ')/([^/]+)$', 'i').exec(p);
|
||||
if (bundleImport) {
|
||||
var withImport = 'assets/' + bundleImport[1] + '/import/' + bundleImport[2] + '/' + bundleImport[3];
|
||||
if (files.has(withImport)) return withImport;
|
||||
}
|
||||
var short = new RegExp('^(' + IMPORT_SHARD + ')/(.+)$', 'i').exec(p);
|
||||
if (short) {
|
||||
var bases = [
|
||||
'assets/level-prefabs/import/',
|
||||
'assets/resources/import/',
|
||||
'assets/main/import/',
|
||||
'assets/internal/import/',
|
||||
'assets/resources/native/',
|
||||
'assets/main/native/',
|
||||
'assets/internal/native/',
|
||||
];
|
||||
for (var i = 0; i < bases.length; i++) {
|
||||
var key = bases[i] + short[1] + '/' + short[2];
|
||||
if (files.has(key)) return key;
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
function lookupFile(pathname) {
|
||||
var keys = [pathname, pathname.replace(/^\.?\//, '')];
|
||||
var parts = pathname.split('/');
|
||||
@@ -141,6 +179,8 @@
|
||||
for (var i = 0; i < keys.length; i++) {
|
||||
var k = keys[i];
|
||||
if (files.has(k)) return files.get(k);
|
||||
var importKey = resolveImportFileKey(k);
|
||||
if (importKey && files.has(importKey)) return files.get(importKey);
|
||||
var idx = k.indexOf('StreamingAssets/aa/WebGL/');
|
||||
if (idx >= 0 && files.has(k.slice(idx + 'StreamingAssets/aa/WebGL/'.length))) {
|
||||
return files.get(k.slice(idx + 'StreamingAssets/aa/WebGL/'.length));
|
||||
@@ -151,6 +191,8 @@
|
||||
}
|
||||
if (k.indexOf('assets/') === 0 && files.has(k)) return files.get(k);
|
||||
}
|
||||
var fallbackKey = resolveImportFileKey(pathname);
|
||||
if (fallbackKey && files.has(fallbackKey)) return files.get(fallbackKey);
|
||||
return null;
|
||||
}
|
||||
|
||||
@@ -221,10 +263,20 @@
|
||||
var u = new URL(String(raw), global.location.href);
|
||||
var p = u.pathname;
|
||||
if (files.size > 0) {
|
||||
var importKey = resolveImportFileKey(p) || resolveImportFileKey(p.replace(/^\/+/, ''));
|
||||
if (importKey) {
|
||||
var importVirt = virtualUrlForBundleFile(importKey);
|
||||
if (importVirt) return importVirt;
|
||||
}
|
||||
var rel = relPathFromUrlPath(p);
|
||||
if (rel) {
|
||||
var virt = virtualUrlForBundleFile(rel);
|
||||
if (virt) return virt;
|
||||
importKey = resolveImportFileKey(rel);
|
||||
if (importKey) {
|
||||
importVirt = virtualUrlForBundleFile(importKey);
|
||||
if (importVirt) return importVirt;
|
||||
}
|
||||
}
|
||||
}
|
||||
if (p.indexOf(getRootPathname()) === 0) return u.href;
|
||||
@@ -366,6 +418,12 @@
|
||||
proto.__tfrhLevelPrefabScript = true;
|
||||
}
|
||||
|
||||
function needsAssetsCore(pathname) {
|
||||
var p = String(pathname || '');
|
||||
if (needsLevelPrefabs(p)) return false;
|
||||
return p.indexOf('assets/') >= 0 || p.indexOf('/assets/') >= 0;
|
||||
}
|
||||
|
||||
function installFetchShim() {
|
||||
global.fetch = function (input, init) {
|
||||
input = rewriteEmbeddedUrl(input);
|
||||
@@ -378,6 +436,19 @@
|
||||
headers: { 'Content-Type': mimeForPath(p) },
|
||||
}));
|
||||
}
|
||||
if (needsAssetsCore(p)) {
|
||||
return ensureAssetsCoreLoaded().then(function () {
|
||||
var hit2 = lookupFile(p);
|
||||
if (hit2) {
|
||||
var body2 = /\.(m?js|json)$/i.test(p) ? bytesToText(hit2) : hit2;
|
||||
return new Response(body2, {
|
||||
status: 200,
|
||||
headers: { 'Content-Type': mimeForPath(p) },
|
||||
});
|
||||
}
|
||||
return origFetch(input, init);
|
||||
});
|
||||
}
|
||||
if (needsLevelPrefabs(p)) {
|
||||
return ensureLevelPackForPath(p).then(function () {
|
||||
var hit2 = lookupFile(p);
|
||||
@@ -407,6 +478,18 @@
|
||||
var origSend = xhr.send;
|
||||
xhr.send = function () {
|
||||
var hit = lookupFile(toPath(reqUrl));
|
||||
if (!hit && needsAssetsCore(reqUrl)) {
|
||||
ensureAssetsCoreLoaded().then(function () {
|
||||
hit = lookupFile(toPath(reqUrl));
|
||||
if (!hit) return origSend.apply(xhr, arguments);
|
||||
var isText = /\.(m?js|json|txt|xml|atlas|tmx|tsx|vsh|fsh|fnt|plist)$/i.test(toPath(reqUrl))
|
||||
|| xhr.responseType === 'json'
|
||||
|| xhr.responseType === 'text'
|
||||
|| !xhr.responseType;
|
||||
deliverXhrHit(xhr, isText ? bytesToText(hit) : null, isText ? null : hit);
|
||||
}).catch(function () { origSend.apply(xhr, arguments); });
|
||||
return;
|
||||
}
|
||||
if (!hit && needsLevelPrefabs(reqUrl)) {
|
||||
ensureLevelPackForPath(reqUrl).then(function () {
|
||||
hit = lookupFile(toPath(reqUrl));
|
||||
@@ -510,6 +593,9 @@
|
||||
if (onProgress) pending.then(function () { onProgress(1); }).catch(function () {});
|
||||
return pending;
|
||||
}
|
||||
var promise = ensureAssetsCoreLoaded().then(function () {
|
||||
return ensureLevelsManifestLoaded();
|
||||
}).then(function () {
|
||||
if (!levelsManifest || !levelsManifest.levels || !levelsManifest.levels[levelId]) {
|
||||
return Promise.reject(new Error('关卡包未在 manifest 中: Level' + levelId));
|
||||
}
|
||||
@@ -517,7 +603,7 @@
|
||||
var entry = levelsManifest.levels[levelId];
|
||||
var url = joinUrl(streamingBase, 'aa/WebGL/' + entry.bundle);
|
||||
dispatchLevelsBundleProgress(0);
|
||||
var promise = fetchBinaryWithProgress(
|
||||
return fetchBinaryWithProgress(
|
||||
url,
|
||||
function (frac) {
|
||||
dispatchLevelsBundleProgress(frac);
|
||||
@@ -536,6 +622,7 @@
|
||||
}).then(function () {
|
||||
dispatchLevelsBundleProgress(1);
|
||||
if (onProgress) onProgress(1);
|
||||
});
|
||||
}).catch(function (e) {
|
||||
levelPackPromises.delete(levelId);
|
||||
throw e;
|
||||
@@ -721,6 +808,7 @@
|
||||
var raw = String(path || '').trim();
|
||||
var m = /Level(\d+)/.exec(raw);
|
||||
var lid = m ? parseInt(m[1], 10) : NaN;
|
||||
var loadPath = raw.indexOf('level-prefabs/') === 0 ? raw : ('level-prefabs/' + raw.replace(/^level-prefabs\//, ''));
|
||||
var prep = (typeof global.__tfrhEnsureLevelPack === 'function' && !Number.isNaN(lid))
|
||||
? global.__tfrhEnsureLevelPack(lid)
|
||||
: Promise.resolve();
|
||||
@@ -728,17 +816,35 @@
|
||||
return prep.then(function () {
|
||||
return invalidateLevelPrefabsBundle();
|
||||
}).then(function () {
|
||||
return ensureLevelsManifestLoaded();
|
||||
}).then(function (manifest) {
|
||||
var entry = manifest && manifest.levels && manifest.levels[String(lid)];
|
||||
var uuid = entry && entry.uuid;
|
||||
return new Promise(function (resolve, reject) {
|
||||
function finish(err, asset) {
|
||||
if (!err && asset) resolve(asset);
|
||||
else reject(err || new Error('missing prefab: ' + loadPath));
|
||||
}
|
||||
function tryBundleLoad() {
|
||||
cc.assetManager.loadBundle('level-prefabs', function (err, bundle) {
|
||||
if (err || !bundle) {
|
||||
reject(err || new Error('level-prefabs bundle unavailable'));
|
||||
return;
|
||||
}
|
||||
bundle.load(raw, Prefab, function (e, asset) {
|
||||
if (e || !asset) reject(e || new Error('missing prefab: ' + raw));
|
||||
else resolve(asset);
|
||||
bundle.load(loadPath, Prefab, finish);
|
||||
});
|
||||
}
|
||||
if (uuid) {
|
||||
cc.assetManager.loadAny({ uuid: uuid, type: Prefab }, function (err, asset) {
|
||||
if (!err && asset) {
|
||||
resolve(asset);
|
||||
return;
|
||||
}
|
||||
tryBundleLoad();
|
||||
});
|
||||
return;
|
||||
}
|
||||
tryBundleLoad();
|
||||
});
|
||||
});
|
||||
}
|
||||
@@ -1060,26 +1166,83 @@
|
||||
return out;
|
||||
}
|
||||
|
||||
function isZipBytes(bytes) {
|
||||
return bytes && bytes.length >= 2 && bytes[0] === 0x50 && bytes[1] === 0x4b;
|
||||
}
|
||||
|
||||
function isJsonText(text) {
|
||||
if (!text) return false;
|
||||
var c = text.charCodeAt(0);
|
||||
return c === 0x7b || c === 0x5b;
|
||||
}
|
||||
|
||||
function decodeBrotliToBytes(ab) {
|
||||
if (typeof DecompressionStream !== 'undefined') {
|
||||
return new Response(ab).pipeThrough(new DecompressionStream('brotli'))
|
||||
.arrayBuffer()
|
||||
.then(function (buf) { return new Uint8Array(buf); });
|
||||
}
|
||||
return Promise.reject(new Error('浏览器不支持 Brotli 解压'));
|
||||
}
|
||||
|
||||
/** OSS 若对 .br 文件再设 Content-Encoding:br,fetch 已解压成 zip/json,勿二次 Brotli */
|
||||
function normalizeBinaryPayload(bytes, expectBrotli) {
|
||||
if (!expectBrotli || isZipBytes(bytes)) {
|
||||
return Promise.resolve(bytes);
|
||||
}
|
||||
var slice = bytes.buffer.slice(bytes.byteOffset, bytes.byteOffset + bytes.byteLength);
|
||||
return decodeBrotliToBytes(slice).catch(function () {
|
||||
if (isZipBytes(bytes)) return bytes;
|
||||
return Promise.reject(new Error('Brotli 解压失败'));
|
||||
});
|
||||
}
|
||||
|
||||
function fetchBinary(url, onProgress) {
|
||||
return origFetch(url).then(function (res) {
|
||||
if (!res.ok) throw new Error('HTTP ' + res.status + ' for ' + url);
|
||||
if (onProgress) onProgress(0.3);
|
||||
return res.arrayBuffer();
|
||||
}).then(function (ab) {
|
||||
if (onProgress) onProgress(0.6);
|
||||
return new Uint8Array(ab);
|
||||
return fetchBinaryWithProgress(url, onProgress, 0, 1);
|
||||
}
|
||||
|
||||
function decodeBrotliToText(ab) {
|
||||
if (typeof DecompressionStream !== 'undefined') {
|
||||
return new Response(ab).pipeThrough(new DecompressionStream('brotli'))
|
||||
.arrayBuffer()
|
||||
.then(function (buf) { return new TextDecoder().decode(buf); });
|
||||
}
|
||||
return Promise.reject(new Error('浏览器不支持 Brotli 解压'));
|
||||
}
|
||||
|
||||
function normalizeJsonPayload(ab, expectBrotli) {
|
||||
var text = new TextDecoder().decode(ab);
|
||||
if (!expectBrotli || isJsonText(text)) return Promise.resolve(text);
|
||||
return decodeBrotliToText(ab).catch(function () {
|
||||
text = new TextDecoder().decode(ab);
|
||||
if (isJsonText(text)) return text;
|
||||
return Promise.reject(new Error('Brotli JSON 解压失败'));
|
||||
});
|
||||
}
|
||||
|
||||
function fetchBinaryWithProgress(url, onProgress, weightStart, weightEnd) {
|
||||
weightStart = weightStart == null ? 0 : weightStart;
|
||||
weightEnd = weightEnd == null ? 1 : weightEnd;
|
||||
return origFetch(url).then(function (res) {
|
||||
var brUrl = String(url).replace(/\.bundle(\?.*)?$/i, '.bundle.br$1');
|
||||
if (!/\.br(\?|$)/i.test(brUrl) && brUrl.indexOf('.bundle') >= 0) {
|
||||
/* brUrl already set */
|
||||
} else if (/\.json(\?|$)/i.test(url)) {
|
||||
brUrl = url.replace(/\.json(\?.*)?$/i, '.json.br$1');
|
||||
} else if (!/\.br(\?|$)/i.test(url)) {
|
||||
brUrl = url + '.br';
|
||||
} else {
|
||||
brUrl = url;
|
||||
}
|
||||
|
||||
function readResponse(res, isBrotli) {
|
||||
if (!res.ok) throw new Error('HTTP ' + res.status + ' for ' + url);
|
||||
if (!res.body || !res.body.getReader) {
|
||||
return res.arrayBuffer().then(function (ab) {
|
||||
var bytes = new Uint8Array(ab);
|
||||
return normalizeBinaryPayload(bytes, isBrotli).then(function (out) {
|
||||
if (onProgress) onProgress(weightEnd);
|
||||
return new Uint8Array(ab);
|
||||
return out;
|
||||
});
|
||||
});
|
||||
}
|
||||
var reader = res.body.getReader();
|
||||
@@ -1089,13 +1252,19 @@
|
||||
function pump() {
|
||||
return reader.read().then(function (result) {
|
||||
if (result.done) {
|
||||
if (onProgress) onProgress(weightEnd);
|
||||
var out = new Uint8Array(received);
|
||||
var off = 0;
|
||||
for (var i = 0; i < chunks.length; i++) {
|
||||
out.set(chunks[i], off);
|
||||
off += chunks[i].length;
|
||||
}
|
||||
if (isBrotli) {
|
||||
return normalizeBinaryPayload(out, true).then(function (decoded) {
|
||||
if (onProgress) onProgress(weightEnd);
|
||||
return decoded;
|
||||
});
|
||||
}
|
||||
if (onProgress) onProgress(weightEnd);
|
||||
return out;
|
||||
}
|
||||
chunks.push(result.value);
|
||||
@@ -1108,11 +1277,45 @@
|
||||
});
|
||||
}
|
||||
return pump();
|
||||
}
|
||||
|
||||
return origFetch(brUrl).then(function (res) {
|
||||
if (res.ok) return readResponse(res, true);
|
||||
return origFetch(url).then(function (plain) { return readResponse(plain, false); });
|
||||
}).catch(function () {
|
||||
return origFetch(url).then(function (plain) { return readResponse(plain, false); });
|
||||
});
|
||||
}
|
||||
|
||||
function ensureAssetsCoreLoaded(onProgress) {
|
||||
if (assetsCoreLoaded) return Promise.resolve();
|
||||
if (assetsCorePromise) return assetsCorePromise;
|
||||
if (!bundleManifest || !bundleManifest.assetsUrl) {
|
||||
return Promise.reject(new Error('assets_all manifest 未就绪'));
|
||||
}
|
||||
assetsCorePromise = fetchBinaryWithProgress(
|
||||
bundleManifest.assetsUrl,
|
||||
onProgress || function () {},
|
||||
0,
|
||||
1,
|
||||
).then(function (buf) {
|
||||
mergeZipIntoFiles(unzip(buf));
|
||||
patchSettingsScriptPackages();
|
||||
assetsCoreLoaded = true;
|
||||
}).catch(function (e) {
|
||||
assetsCorePromise = null;
|
||||
throw e;
|
||||
});
|
||||
return assetsCorePromise;
|
||||
}
|
||||
|
||||
global.__tfrhEnsureAssetsCore = function (onProgress) {
|
||||
return ensureAssetsCoreLoaded(onProgress);
|
||||
};
|
||||
|
||||
function loadStartupBundles(manifest, onProgress) {
|
||||
onProgress(0.05);
|
||||
/* internal/main 等引擎启动即需,assets_all 须与 scenes 并行首屏加载 */
|
||||
var scenesP = fetchBinaryWithProgress(manifest.scenesUrl, onProgress, 0.05, 0.42);
|
||||
var assetsP = fetchBinaryWithProgress(manifest.assetsUrl, onProgress, 0.42, 0.58);
|
||||
return Promise.all([scenesP, assetsP]).then(function (bufs) {
|
||||
@@ -1120,6 +1323,7 @@
|
||||
assertCocosRuntimeLoaded();
|
||||
mergeZipIntoFiles(unzip(bufs[1]));
|
||||
patchSettingsScriptPackages();
|
||||
assetsCoreLoaded = true;
|
||||
onProgress(0.6);
|
||||
});
|
||||
}
|
||||
@@ -1190,6 +1394,58 @@
|
||||
});
|
||||
}
|
||||
|
||||
function ensureLevelsManifestLoaded() {
|
||||
if (levelsManifest) return Promise.resolve(levelsManifest);
|
||||
if (levelsManifestPromise) return levelsManifestPromise;
|
||||
if (!streamingBase) return Promise.reject(new Error('streamingBase 未初始化'));
|
||||
levelsManifestPromise = loadLevelsManifest(streamingBase).then(function (lm) {
|
||||
levelsManifest = lm;
|
||||
return lm;
|
||||
});
|
||||
return levelsManifestPromise;
|
||||
}
|
||||
|
||||
function findLevelDbShard(index, levelId) {
|
||||
if (!index || !index.shards) return null;
|
||||
var id = Number(levelId);
|
||||
for (var i = 0; i < index.shards.length; i++) {
|
||||
var s = index.shards[i];
|
||||
if (id >= s.min && id <= s.max) return s;
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
function ensureLevelDbShardLoaded(levelId) {
|
||||
var index = global.__tfrhLevelsDbIndex;
|
||||
if (!index || index.mode !== 'sharded') return Promise.resolve();
|
||||
var shard = findLevelDbShard(index, levelId);
|
||||
if (!shard) return Promise.resolve();
|
||||
var key = shard.file;
|
||||
if (loadedLevelDbShards.has(key)) return Promise.resolve();
|
||||
if (levelDbShardPromises.has(key)) return levelDbShardPromises.get(key);
|
||||
if (!streamingBase) return Promise.reject(new Error('streamingBase 未初始化'));
|
||||
var url = joinUrl(streamingBase, 'aa/' + key.replace(/^\/+/, ''));
|
||||
var promise = fetchLevelsDatabaseJson(url).then(function (json) {
|
||||
loadedLevelDbShards.add(key);
|
||||
if (!global.__tfrhLevelsDatabaseJson) {
|
||||
global.__tfrhLevelsDatabaseJson = { version: index.version || 2, levels: {} };
|
||||
}
|
||||
var merged = global.__tfrhLevelsDatabaseJson.levels || {};
|
||||
var incoming = (json && json.levels) || {};
|
||||
Object.keys(incoming).forEach(function (k) { merged[k] = incoming[k]; });
|
||||
global.__tfrhLevelsDatabaseJson.levels = merged;
|
||||
}).catch(function (e) {
|
||||
levelDbShardPromises.delete(key);
|
||||
throw e;
|
||||
});
|
||||
levelDbShardPromises.set(key, promise);
|
||||
return promise;
|
||||
}
|
||||
|
||||
global.__tfrhEnsureLevelDbShard = function (levelId) {
|
||||
return ensureLevelDbShardLoaded(levelId);
|
||||
};
|
||||
|
||||
function loadBundleManifest(config) {
|
||||
var base = resolveStreamingBase(config);
|
||||
var catalogUrl = joinUrl(base, 'aa/catalog.json');
|
||||
@@ -1198,8 +1454,6 @@
|
||||
return res.json();
|
||||
}).then(function (catalog) {
|
||||
var bundles = parseCatalogBundles(catalog);
|
||||
return loadLevelsManifest(base).then(function (lm) {
|
||||
levelsManifest = lm;
|
||||
var manifest = {
|
||||
scenesUrl: joinUrl(base, 'aa/WebGL/' + bundles.scenes),
|
||||
assetsUrl: joinUrl(base, 'aa/WebGL/' + bundles.assets),
|
||||
@@ -1208,7 +1462,6 @@
|
||||
bundleManifest = manifest;
|
||||
return manifest;
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
function bootstrapCocos(onProgress, useLocalDisk) {
|
||||
@@ -1268,36 +1521,33 @@
|
||||
packageRoot = getPackageRoot();
|
||||
installEmbeddedPathFix();
|
||||
|
||||
function resolveLevelsDatabaseUrl(config) {
|
||||
if (global.__tfrhLevelsDatabaseUrl) return global.__tfrhLevelsDatabaseUrl;
|
||||
function resolveLevelsDbBase(config) {
|
||||
var sa = config && config.streamingAssetsUrl;
|
||||
if (sa && /^https?:\/\//i.test(sa)) {
|
||||
var cdnRoot = String(sa).replace(/\/StreamingAssets\/?$/i, '');
|
||||
return joinUrl(cdnRoot, 'levels-database.json');
|
||||
return String(sa).replace(/\/StreamingAssets\/?$/i, '');
|
||||
}
|
||||
return joinUrl(packageRoot || getPackageRoot(), 'levels-database.json');
|
||||
return packageRoot || getPackageRoot();
|
||||
}
|
||||
|
||||
function decodeBrotliToText(ab) {
|
||||
if (typeof DecompressionStream !== 'undefined') {
|
||||
return new Response(ab).pipeThrough(new DecompressionStream('brotli'))
|
||||
.arrayBuffer()
|
||||
.then(function (buf) { return new TextDecoder().decode(buf); });
|
||||
function resolveLevelsDatabaseUrl(config) {
|
||||
if (global.__tfrhLevelsDatabaseUrl) return global.__tfrhLevelsDatabaseUrl;
|
||||
return joinUrl(resolveLevelsDbBase(config), 'levels-database.json');
|
||||
}
|
||||
return Promise.reject(new Error('浏览器不支持 Brotli 解压'));
|
||||
|
||||
function resolveLevelsDbIndexUrl(config) {
|
||||
if (global.__tfrhLevelsDbIndexUrl) return global.__tfrhLevelsDbIndexUrl;
|
||||
return joinUrl(resolveLevelsDbBase(config), 'levels-db-index.json');
|
||||
}
|
||||
|
||||
function fetchLevelsDatabaseJson(url) {
|
||||
var brUrl = url.replace(/\.json(\?.*)?$/i, '.json.br$1');
|
||||
return origFetch(brUrl).then(function (res) {
|
||||
if (!res.ok) throw new Error('HTTP ' + res.status);
|
||||
var enc = (res.headers.get('content-encoding') || '').toLowerCase();
|
||||
if (enc === 'br' || /\.br(\?|$)/i.test(brUrl)) {
|
||||
return res.arrayBuffer().then(decodeBrotliToText).then(function (text) {
|
||||
return res.arrayBuffer().then(function (ab) {
|
||||
return normalizeJsonPayload(ab, true).then(function (text) {
|
||||
return JSON.parse(text);
|
||||
});
|
||||
}
|
||||
return res.json();
|
||||
});
|
||||
}).catch(function () {
|
||||
return origFetch(url).then(function (res) {
|
||||
if (!res.ok) throw new Error('HTTP ' + res.status);
|
||||
@@ -1306,14 +1556,20 @@
|
||||
});
|
||||
}
|
||||
|
||||
function prefetchMainSiteLevelDb(config) {
|
||||
var url = resolveLevelsDatabaseUrl(config);
|
||||
global.__tfrhLevelsDatabaseUrl = url;
|
||||
if (!url || global.__tfrhLevelsDatabaseJson) return Promise.resolve();
|
||||
return fetchLevelsDatabaseJson(url).then(function (json) {
|
||||
global.__tfrhLevelsDatabaseJson = json;
|
||||
function prefetchLevelDbIndex(config) {
|
||||
var indexUrl = resolveLevelsDbIndexUrl(config);
|
||||
var legacyUrl = resolveLevelsDatabaseUrl(config);
|
||||
global.__tfrhLevelsDbIndexUrl = indexUrl;
|
||||
global.__tfrhLevelsDatabaseUrl = legacyUrl;
|
||||
if (global.__tfrhLevelsDbIndex) return Promise.resolve();
|
||||
return fetchLevelsDatabaseJson(indexUrl).then(function (json) {
|
||||
if (json && json.mode === 'sharded') {
|
||||
global.__tfrhLevelsDbIndex = json;
|
||||
return;
|
||||
}
|
||||
throw new Error('非分片索引');
|
||||
}).catch(function (e) {
|
||||
console.warn('[mstest5] 关卡库预取失败', url, e);
|
||||
console.warn('[mstest5] 关卡库索引预取失败,启动时将由 LevelDatabase 回退', indexUrl, e);
|
||||
});
|
||||
}
|
||||
|
||||
@@ -1339,7 +1595,7 @@
|
||||
onProgress = onProgress || function () {};
|
||||
onProgress(0.02);
|
||||
resolveStreamingBase(config);
|
||||
return prefetchMainSiteLevelDb(config).then(function () {
|
||||
return prefetchLevelDbIndex(config).then(function () {
|
||||
return shouldUseBundlePackFlow(config);
|
||||
}).then(function (useBundles) {
|
||||
if (!useBundles) {
|
||||
|
||||
Reference in New Issue
Block a user