/** WeChat uses verified JSON files; Creator preview and other platforms keep the local Bundle. */ export default class LevelResources { /** Shared with the injected loader. No player data or JSON contents enter performance logs. */ static trace(stage: string, fields?: any): (result?: any) => void { const perf = typeof window !== "undefined" && (window as any).__levelPerf; return perf ? perf.begin(stage, fields) : () => {}; } static phase(name: string, fields?: any): void { const perf = typeof window !== "undefined" && (window as any).__levelPerf; if (perf) perf.phase(name, fields); } /** Cocos 2.4.15 diagnostics only: preserve the engine's load/switch path and restore every hook. */ static traceSceneLoad(sceneName: string, fields: any = {}): (result?: any) => void { const perf = typeof window !== "undefined" && (window as any).__levelPerf; const director = cc.director as any; if (!perf || !perf.enabled() || !director || typeof director.runSceneImmediate !== "function") return () => {}; const restore: Array<() => void> = []; const begin = (part: string) => perf.begin("scene." + sceneName + "." + part, fields); const loaded = begin("resourceLoad"); const pendingResources = new Set<(result?: any) => void>(); const resourceInfo = new Map(); let closed = false; const cleanup = () => { if (closed) return; closed = true; for (let i = restore.length - 1; i >= 0; i--) { try { restore[i](); } catch (_) { /* Diagnostics must not interrupt scene callbacks. */ } } pendingResources.forEach(done => done({ status: 'traceClosed' })); resourceInfo.clear(); perf.flush(); }; const replace = (target: any, key: string, wrap: (original: Function) => Function) => { if (!target || typeof target[key] !== "function") return; const original = target[key], own = Object.getOwnPropertyDescriptor(target, key); const replacement = wrap(original); try { target[key] = replacement; } catch (_) { return; } if (target[key] !== replacement) return; restore.push(() => { if (target[key] !== replacement) return; if (own) Object.defineProperty(target, key, own); else delete target[key]; }); }; const measure = (target: any, key: string, part: string) => replace(target, key, original => function (...args: any[]) { const done = begin(part); try { const value = original.apply(this, args); done({ status: "ok" }); return value; } catch (error) { done({ status: "error" }); throw error; } }); // Cocos 2.4.15 copies custom options to dependency tasks and RequestItems. // Tag only Bundle.loadScene's explicit loadAny call, so other background loads stay out. const manager = cc.assetManager as any, marker = {}, tag = '__levelSceneTrace__'; let parsingOwner = '', lastProgressAt = 0; const clean = (value: any) => String(value || '').split(/[?#]/)[0].slice(0, 180); const identify = (item: any) => ({ uuid: clean(item && item.uuid), path: clean(item && item.info && item.info.path), url: clean(item && item.url), bundle: clean(item && item.config && item.config.name), type: clean(item && item.ext) }); const detail = (part: string, data: any) => { const end = perf.begin('scene.resource.' + part, Object.assign({ scene: sceneName }, fields, data)); const done = (result: any = {}) => { pendingResources.delete(done); end(result); }; pendingResources.add(done); return done; }; replace(manager, 'loadAny', original => function (...args: any[]) { if (closed || !args[0] || args[0].scene !== sceneName || args.length !== 4) return original.apply(this, args); args[1] = Object.assign({}, args[1], { [tag]: marker }); const progress = args[2]; args[2] = function (finished: number, total: number, item: any) { const at = Date.now(); if (!closed && (at - lastProgressAt >= 1000 || finished === total)) { lastProgressAt = at; perf.emit('scene.progress', Object.assign({ scene: sceneName, finished, total }, fields, identify(item))); } if (progress) return progress.apply(this, arguments); }; return original.apply(this, args); }); replace(manager.packManager, 'load', original => function (...args: any[]) { if (closed || !args[1] || args[1][tag] !== marker) return original.apply(this, args); const info = identify(args[0]); resourceInfo.set(clean(args[0].id), info); const done = detail('fetch', info), callback = args[2]; args[2] = function (error: any) { done({ status: error ? 'error' : 'ok' }); return callback.apply(this, arguments); }; try { return original.apply(this, args); } catch (error) { done({ status: 'error' }); throw error; } }); for (const [target, key, part] of [[manager.downloader, 'download', 'download'], [manager.parser, 'parse', 'parse']]) { replace(target, key, original => function (...args: any[]) { if (closed || !args[3] || args[3][tag] !== marker) return original.apply(this, args); const id = clean(args[0]), data: any = Object.assign({}, resourceInfo.get(id), { resource: id, type: clean(args[2]) }); if (part === 'download') data.url = clean(args[1]); const done = detail(part, data), callback = args[4]; args[4] = function (error: any) { done({ status: error ? 'error' : 'ok' }); const previous = parsingOwner; if (part === 'parse') parsingOwner = id; try { return callback.apply(this, arguments); } finally { parsingOwner = previous; } }; try { return original.apply(this, args); } catch (error) { done({ status: 'error' }); throw error; } }); } replace(manager.pipeline, 'async', original => function (task: any) { if (closed || !task.options || task.options[tag] !== marker || !Array.isArray(task.input) || !task.input.length) return original.apply(this, arguments); const done = detail('dependencies', { parent: parsingOwner, count: task.input.length, sample: task.input.slice(0, 3).map((item: any) => clean(item.uuid || item.url || item)) }); const callback = task.onComplete; const wrapped = function (error: any) { if (task.onComplete === wrapped) task.onComplete = callback; done({ status: error ? 'error' : 'ok' }); if (callback) return callback.apply(this, arguments); }; task.onComplete = wrapped; try { return original.apply(this, arguments); } catch (error) { if (task.onComplete === wrapped) task.onComplete = callback; done({ status: 'error' }); throw error; } }); replace(director, "runSceneImmediate", original => function (...args: any[]) { const incoming = args[0], scene = incoming && (incoming.scene || incoming); if (!scene || (scene.name !== sceneName && incoming.name !== sceneName)) return original.apply(this, args); // Director calls runSceneImmediate only after Bundle.loadScene has finished loading/parsing. loaded({ status: "ok" }); const switched = begin("switch.sync"); measure(scene, "_load", "initialize.sync"); measure((cc.assetManager as any)._releaseManager, "_autoRelease", "releaseAssets.sync"); measure(director.getScene(), "destroy", "destroyOldScene.sync"); measure((cc as any).Object, "_deferredDestroy", "deferredDestroy.sync"); // _activate includes onLoad/onEnable; start executes later in the scheduler, outside this span. measure(scene, "_activate", "activate.sync"); try { const value = original.apply(this, args); switched({ status: "ok" }); return value; } catch (error) { switched({ status: "error" }); throw error; } finally { cleanup(); } }); return (result = {}) => { loaded(result); cleanup(); }; } static runtime(): any { return typeof window !== "undefined" && (window as any).__levelResources; } static required(): any { const runtime = this.runtime(); if (!runtime && cc.sys.platform === cc.sys.WECHAT_GAME) throw new Error("关卡资源加载器未安装,请完整构建微信小游戏"); return runtime; } static beginLogin(data: any, version: string): void { const runtime = this.required(); if (!runtime) return; const limits = runtime.beginLogin({ maxLevel: data.maxLevel, bLevelCount: data.bLevelCount, levelGroup: data.levelGroup, version }); cc.fx.GameTool.MAX_NORMAL_LEVEL = limits.maxLevel; } static prepareLogin(): Promise { try { const runtime = this.required(), info = cc.fx.GameConfig.GM_INFO; return runtime ? runtime.prepareLogin(info.otherLevel > 0 ? info.otherLevel : info.level + 1, info.otherLevel > 0 ? 'A' : runtime.limits().levelGroup) : Promise.resolve(); } catch (error) { return Promise.reject(error); } } static load(level: number, mainline: boolean, local: () => Promise): Promise { try { const runtime = this.required(); return runtime ? runtime.load(level, mainline ? runtime.limits().levelGroup : 'A').then(result => result.data) : local(); } catch (error) { return Promise.reject(error); } } static prefetch(): void { const runtime = this.runtime(); if (runtime && runtime.limits()) runtime.prefetch(cc.fx.GameConfig.GM_INFO.level + 1).catch(error => cc.warn('关卡预下载稍后重试', error)); } static holdBackground(): () => void { const runtime = this.runtime(); return runtime ? runtime.holdBackground() : () => {}; } static allReady(): boolean { const runtime = this.required(); return !runtime || runtime.allReady(); } static ensureAll(foreground = true): Promise { try { const runtime = this.required(); return runtime ? runtime.ensureAll(foreground) : Promise.resolve(); } catch (error) { return Promise.reject(error); } } }