Files
MatchMaster/assets/Script/module/Config/LevelResources.ts
T

171 lines
11 KiB
TypeScript

/** 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<string, any>();
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<void> {
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<any>): Promise<any> {
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<void> {
try { const runtime = this.required(); return runtime ? runtime.ensureAll(foreground) : Promise.resolve(); }
catch (error) { return Promise.reject(error); }
}
}