import { assetManager, Prefab } from 'cc'; import { Singleton } from '../../tools/Singleton'; import { getAllDownloadableBundles, getBattleSilentBundles, getBattleSilentUiIds, getHallSilentUiIds, getHallWarmLongTailResPakage, getHallWarmResPakage, getHerosResPakage, getMainShellResPakage, GBundle, UIConfigs, } from 'db://assets/script/game/ConfigRes'; import { UIID } from 'db://assets/script/game/ConfigRes'; import { ResPkgBuilder } from 'db://assets/script/game/ResPkgBuilder'; import { GEvent } from '../event/GEvent'; import { LoadScheduler } from './LoadScheduler'; import { PRELOAD_RULES, parseBattleScopedToken } from './PreloadRules'; import { ChapterLevelBuilder } from 'db://assets/script/battle/config/ChapterLevelBuilder'; import { DEV } from 'cc/env'; import { PerfReport } from 'db://assets/script/game/PerfReport'; import { PerfLoadTimer } from 'db://assets/script/game/PerfLoadTimer'; import { MemoryProfile } from 'db://assets/script/game/MemoryProfile'; import { BundleLifecycle, isTabScopedBundle } from './BundleLifecycle'; import { awaitWithTimeout, LoadRecoveryConfig } from './LoadRecovery'; import { createMembershipSet, membershipAdd, membershipFromArray, membershipToArray, type MembershipSet, } from '../../tools/MembershipSet'; /** * 规则驱动的静默预加载:大厅分包下载 / 局内弹窗补载 / 战斗前 warm。 * 预制只经 ResourcesManager.loadRes;进战释放 HallWarm 预制,不卸 popCommon zip。 */ export class PreloadPredictor extends Singleton { private _started = false; private _longTailTimer: ReturnType | null = null; private _doneKeys = createMembershipSet(); /** 战场战斗预载已完成(preLoadResPakg 回调结束) */ private _battleWarmComplete = createMembershipSet(); private _battleWarmPromises = new Map>(); /** 大厅静默下载代数:递增即作废未执行队列 */ private _silentGen = 0; /** 已确认不存在的分包,避免重复 404 */ private _silentMissBundles = createMembershipSet(); /** 规则执行中:await 与 emit 共享同一 Promise,完成前不标 done */ private _ruleInflight: Record | undefined> = {}; private static _warmKey(levelId: number): string { return `${levelId}`; } constructor() { super(); this._doneKeys = createMembershipSet(); } private _isBattleExecutedKey(key: string): boolean { return key.startsWith('battle:') || key.startsWith('token:battleScoped:') || key.startsWith('beforeBattle:'); } private _isHallExecutedKey(key: string): boolean { return key.startsWith('enterHall:') || key.startsWith('idleOnHome:') || key.startsWith('tabIntent:') || key.startsWith('panelOpen:') || key.startsWith('token:bundle:') || key.startsWith('token:ui:') || key === 'token:pkg:HallWarmRes' || key === 'token:pkg:HallWarmLongTailRes' || key === 'token:pkg:HerosRes'; } /** MembershipSet 迭代在部分小游戏运行时上不可靠,先收集 string key 再删 */ private _removeExecutedKeys(match: (key: string) => boolean): void { const remove: string[] = []; for (const key of this._doneKeys.keys()) { if (typeof key !== 'string') continue; if (match(key)) remove.push(key); } for (const key of remove) { this._doneKeys.delete(key); } } start(): void { if (this._started) return; this._started = true; GEvent.Ins.on(GEvent.UpdateMainUI, this._scheduleHomeIdle, this); this._scheduleHomeIdle(); } stop(): void { if (!this._started) return; this._started = false; GEvent.Ins.off(GEvent.UpdateMainUI, this._scheduleHomeIdle, this); if (this._longTailTimer) clearTimeout(this._longTailTimer); } /** 大厅战场预载是否已完成(用于 skip beforeBattle 重复预热) */ isChapterPreloadDone(levelId: number): boolean { return gg.game?.isChapterPreloaded?.(levelId) ?? false; } onBeforeBattle(): void { const levelId = gg.game?.CurentSelectLevelId ?? 1; if (this.isChapterPreloadDone(levelId)) return; this.emitSignal('beforeBattle'); } /** * 进战斗正式 loadResPakg 前调用:作废 Predictor 侧 BattleRes 预载登记并清对应节点池。 * 避免 warmFrontier / beforeBattle 的 preLoadResPakg 与 loadResPakg(clearResPkg) 并发导致贴图失效仍被渲染。 */ abortBattleWarmForChapter(levelId: number): void { if (levelId <= 0) return; const levelData = gg.data.project.getLevelConfig(levelId); if (!levelData) return; const mapId = ChapterLevelBuilder.getLevelMapId(levelId); const resPkg = ResPkgBuilder.buildBattlePackage(levelId, mapId, 0); gg.res.invalidatePreloadForPackage(resPkg); gg.res.evictPackageFromBundles(resPkg); const bundleNames = createMembershipSet(); for (const conf of resPkg.ress) { if (typeof conf.bundle === 'string' && conf.bundle) { membershipAdd(bundleNames, conf.bundle); } } gg.res.purgeNodePoolForBundles(membershipToArray(bundleNames)); this._battleWarmComplete.delete(PreloadPredictor._warmKey(levelId)); this._battleWarmPromises.delete(PreloadPredictor._warmKey(levelId)); this._removeExecutedKeys((key) => key.startsWith('battle:') || key.startsWith('beforeBattle:')); } /** 指定战场战斗预载是否已完成(可被 loadResPakg 复用) */ isBattleWarmReady(levelId: number): boolean { if (levelId <= 0) return false; const key = PreloadPredictor._warmKey(levelId); return this._battleWarmComplete.has(key); } /** * 战斗预载是否仍在飞(超时后也可能未结束)。 * @param levelId 关卡 */ isBattleWarmInflight(levelId: number): boolean { if (levelId <= 0) return false; return this._battleWarmPromises.has(PreloadPredictor._warmKey(levelId)); } /** * 等待战斗预载。超时返回 false,不抛错,避免匹配门闩 / 进战被卡死。 * @param levelId 关卡 */ async awaitBattleWarm(levelId: number): Promise { const warmKey = PreloadPredictor._warmKey(levelId); if (levelId <= 0) return false; if (this._battleWarmComplete.has(warmKey)) return true; if (!this._battleWarmPromises.has(warmKey)) { const taskKey = `battle:${warmKey}`; if (this._doneKeys.has(taskKey)) { this._doneKeys.delete(taskKey); } this.scheduleBattleWarm(levelId, 'interactive'); } const promise = this._battleWarmPromises.get(warmKey); if (!promise) return false; try { await awaitWithTimeout( promise, LoadRecoveryConfig.battleWarmTimeoutMs, `battleWarm:lv${levelId}`, ); return this._battleWarmComplete.has(warmKey); } catch (e) { console.warn('[PreloadPredictor] battleWarm 超时,改走进战加载', String(e)); return false; } } /** loadResPakg 复用预载后清除登记,避免下次误跳过 abort */ consumeBattleWarm(levelId: number): void { if (levelId <= 0) return; const warmKey = PreloadPredictor._warmKey(levelId); this._battleWarmComplete.delete(warmKey); this._battleWarmPromises.delete(warmKey); this._doneKeys.delete(`battle:${warmKey}`); } /** * Boot / 首屏:对进度前沿章节(ch1–2)静默预热战斗分包与资源,缩短首关 LD-BTL。 */ warmFrontierChapterBattle(): void { return; MemoryProfile.resolve(); const levelId = Math.max(1, gg.data?.doc?.rankId ?? gg.game?.CurentSelectLevelId ?? 1); if (levelId > 2) return; for (const bundle of [ GBundle.Heros, GBundle.PopBattle, GBundle.BattleGame, GBundle.BattleMap, GBundle.BattleGameMonster, ]) { if (!assetManager.getBundle(bundle)) { assetManager.loadBundle(bundle); } } const levels: number[] = []; if (levelId <= 1) { levels.push(1); if (MemoryProfile.tier === 'tier3') { levels.push(2); } } else { levels.push(levelId); } for (const lv of levels) { this.scheduleBattleWarm(lv, 'silent'); } } /** * 进战斗前等待预载完成。interactive 立刻 preLoadResPakg;silent 大厅预热仍关闭。 */ scheduleBattleWarm( levelId: number, priority: 'silent' | 'interactive' = 'silent', ): void { if (priority === 'silent') return; const warmKey = PreloadPredictor._warmKey(levelId); if (levelId <= 0 || this._battleWarmComplete.has(warmKey)) return; if (this._battleWarmPromises.has(warmKey)) return; const taskKey = `battle:${warmKey}`; if (this._doneKeys.has(taskKey)) { if (this._battleWarmComplete.has(warmKey)) return; if (this._battleWarmPromises.has(warmKey)) return; this._doneKeys.delete(taskKey); } membershipAdd(this._doneKeys, taskKey); const promise = this._runBattleWarm(levelId, warmKey).then(() => { this._battleWarmPromises.delete(warmKey); }); this._battleWarmPromises.set(warmKey, promise); } /** * 预载本关 BattleRes 并登记 warm 完成 * @param levelId 关卡 * @param warmKey 预载键 */ private _runBattleWarm(levelId: number, warmKey: string): Promise { return new Promise((resolve) => { const levelData = gg.data.project.getLevelConfig(levelId); if (!levelData) { membershipAdd(this._battleWarmComplete, warmKey); resolve(); return; } const mapId = ChapterLevelBuilder.getLevelMapId(levelId); const resPkg = ResPkgBuilder.buildBattlePackage(levelId, mapId, 0); gg.res.preLoadResPakg(resPkg, null, (result) => { if (result?.ok) { membershipAdd(this._battleWarmComplete, warmKey); } resolve(); }); }); } emitSignal(signal: string): void { for (const rule of PRELOAD_RULES) { if (rule.when !== signal) continue; void this._executeRule(rule, signal); } } /** 等待信号对应规则执行完成(用于 enterHall 后再章节预载) */ async awaitSignal(signal: string, forUserWait = false): Promise { const rules = PRELOAD_RULES.filter((r) => r.when === signal); if (rules.length === 0) return; await Promise.all(rules.map((rule) => this._executeRuleAwait(rule, signal, forUserWait))); } /** 退战斗回大厅:允许下局重新 beforeBattle / 章节 warm */ resetBattlePhase(): void { this.abortHallSilentDownload(); this._battleWarmComplete.clear(); this._battleWarmPromises.clear(); this._removeExecutedKeys((key) => this._isBattleExecutedKey(key)); } /** 进战斗:清大厅预载登记,回大厅后可重新 enterHall / idle */ resetHallPhase(): void { this.abortHallSilentDownload(); this._removeExecutedKeys((key) => this._isHallExecutedKey(key)); if (this._longTailTimer) { clearTimeout(this._longTailTimer); this._longTailTimer = null; } } /** * 冷启动最早窗口:first/comm/home 已落地后立刻下 icons 与 popCommon zip。 * 只 kickLoad,不 acquire、不 load 预制。 */ startEarlyIconsDownload(): void { const life = BundleLifecycle.getInstance(); life.kickLoad(GBundle.Items); life.kickLoad(GBundle.PopCommon); } /** * BootRes 完成后立刻并行下战斗分包(只 loadBundle,不 acquire)。 * 不走 silent 队列:点开始不会被 abort / beginUserLoad 停掉。 */ startEarlyBattleBundleDownload(): void { const names = getBattleSilentBundles(); const life = BundleLifecycle.getInstance(); for (let i = 0; i < names.length; i++) { const name = names[i]; if (name === GBundle.PopBattle) continue; life.kickLoad(name); } } /** * 闲时点火磁盘上全部尚未落地的业务分包 zip(rank/shenjiang/战斗包)。 * 只 kickLoad,不进 silent 队列、不 acquire、不 load 资源。 */ kickAllRemainingBundleZips(): void { const life = BundleLifecycle.getInstance(); const names = getAllDownloadableBundles(); for (let i = 0; i < names.length; i++) { life.kickLoad(names[i]); } } /** * 进首页后:先 decode 大厅高频弹窗,再静默战斗 zip 与其它页。 * 用户点击 beginUserLoad 停发 silent;进行中的同路径 load 与 openPanel 合并。 */ startHallSilentDownload(): void { this._silentGen++; const gen = this._silentGen; const scheduler = LoadScheduler.getInstance(); scheduler.clearSilentQueue(); void scheduler.enqueue('interactive', () => this._decodeHallWarmThenSilent(gen)); } /** * popCommon zip → HallWarm decode → 再 kick 战斗 zip 与 silent UI。 * @param gen 静默代数 */ private _decodeHallWarmThenSilent(gen: number): Promise { if (gen !== this._silentGen) return Promise.resolve(); return this._downloadBundleIfGen(gen, GBundle.PopCommon).then(() => this._loadHallWarmIfGen(gen)).then(() => { if (gen !== this._silentGen) return; this._enqueueHallSilentTail(gen); }); } /** * 大厅壳 decode 之后再下战斗/Tab zip。用户加载中不 kickLoad,改进行列等停手。 * @param gen 静默代数 */ private _enqueueHallSilentTail(gen: number): void { const scheduler = LoadScheduler.getInstance(); if (!scheduler.isUserLoadActive()) { this.kickAllRemainingBundleZips(); } else { const names = getAllDownloadableBundles(); for (let i = 0; i < names.length; i++) { const name = names[i]; void scheduler.enqueue('silent', () => this._downloadBundleIfGen(gen, name)); } } void scheduler.enqueue('silent', () => this._downloadBundleIfGen(gen, GBundle.Items)); const uiIds = getHallSilentUiIds(); for (let i = 0; i < uiIds.length; i++) { const uiId = uiIds[i]; void scheduler.enqueue('silent', () => this._preloadHallUiIfGen(gen, uiId)); } } /** 进战 / 回大厅重建:丢掉未开始的 silent */ abortHallSilentDownload(): void { this._silentGen++; LoadScheduler.getInstance().clearSilentQueue(); } /** * BattleGame 打开后:preload 局内弹窗,再 load 受击/武器长尾。 * 不 addRef;点开弹窗仍走 openPanel 最高优先。 * @param levelId 本局 Level */ startBattleSilentDownload(levelId: number): void { this._silentGen++; const gen = this._silentGen; const scheduler = LoadScheduler.getInstance(); scheduler.clearSilentQueue(); void scheduler.enqueue('silent', () => this._downloadBundleIfGen(gen, GBundle.PopBattle)); const uiIds = getBattleSilentUiIds(); for (let i = 0; i < uiIds.length; i++) { const uiId = uiIds[i]; void scheduler.enqueue('silent', () => this._preloadHallUiIfGen(gen, uiId)); } const items = ResPkgBuilder.getBattleAfterEnterItems(levelId); for (let i = 0; i < items.length; i++) { const item = items[i]; if (item.name) { void scheduler.enqueue('silent', () => this._loadPrefabIfGen(gen, item.bundle, item.path, item.name)); } else { void scheduler.enqueue('silent', () => this._loadDirIfGen(gen, item.bundle, item.path)); } } } /** * 只下载分包,不 acquire,避免钉死战斗包卸载。 * @param gen 静默代数 * @param bundleName 分包名 */ private _downloadBundleIfGen(gen: number, bundleName: string): Promise { if (gen !== this._silentGen) return Promise.resolve(); if (this._silentMissBundles.has(bundleName)) return Promise.resolve(); if (assetManager.getBundle(bundleName)) return Promise.resolve(); return BundleLifecycle.getInstance().ensureLoaded(bundleName).then((bundle) => { if (bundle || assetManager.getBundle(bundleName)) return; if (BundleLifecycle.getInstance().isLoadInflight(bundleName)) return; membershipAdd(this._silentMissBundles, bundleName); }); } /** * 下载页面分包并 load 预制(经 ResourcesManager 同路径合并)。 * @param gen 静默代数 * @param uiId 面板 id */ private _preloadHallUiIfGen(gen: number, uiId: number): Promise { if (gen !== this._silentGen) return Promise.resolve(); const conf = UIConfigs[uiId]; if (!conf?.bundle || !conf.path || !conf.name) return Promise.resolve(); return this._downloadBundleIfGen(gen, conf.bundle).then(() => { if (gen !== this._silentGen) return; return this._preloadPrefabOnly(conf.bundle, conf.path + conf.name); }); } /** * 大厅高频弹窗 decode(进战会 invalidate 预制,zip 常驻)。 * @param gen 静默代数 */ private _loadHallWarmIfGen(gen: number): Promise { if (gen !== this._silentGen) return Promise.resolve(); return new Promise((resolve) => { gg.res.preLoadResPakg(getHallWarmResPakage(), null, () => { if (gen === this._silentGen) { this._markTokenDone('pkg:HallWarmRes'); } resolve(); }); }); } /** * 走 ResourcesManager.loadRes 同路径合并,避免与 openPanel 双飞丢回调。 * @param bundleName 分包 * @param prefabPath 预制路径 */ private _preloadPrefabOnly(bundleName: string, prefabPath: string): Promise { const bundle = assetManager.getBundle(bundleName); if (!bundle) return Promise.resolve(); return new Promise((resolve) => { gg.res.loadResPublic(bundle, prefabPath, () => resolve(), Prefab); }); } /** * 局内长尾:bundle.load 单预制,供 getNode 命中。 * @param gen 静默代数 * @param bundleName 分包 * @param dirPath 目录 * @param name 预制名 */ private _loadPrefabIfGen(gen: number, bundleName: string, dirPath: string, name: string): Promise { if (gen !== this._silentGen) return Promise.resolve(); const bundle = assetManager.getBundle(bundleName); if (!bundle) return Promise.resolve(); const fullPath = dirPath + name; return new Promise((resolve) => { gg.res.loadResPublic(bundle, fullPath, () => resolve(), Prefab); }); } /** * 局内长尾:bundle.loadDir,不 addRef。 * @param gen 静默代数 * @param bundleName 分包 * @param dirPath 目录 */ private _loadDirIfGen(gen: number, bundleName: string, dirPath: string): Promise { if (gen !== this._silentGen) return Promise.resolve(); const bundle = assetManager.getBundle(bundleName); if (!bundle) return Promise.resolve(); return new Promise((resolve) => { bundle.loadDir(dirPath, Prefab, () => resolve()); }); } /** 内存告警 D3:释放 HallWarm 长尾登记,弹窗打开时再按需加载 */ clearHallLongTailPreload(): void { this._doneKeys.delete('token:pkg:HallWarmLongTailRes'); this._removeExecutedKeys((key) => key.startsWith('idleOnHome:')); } onTabIntent(tabId: UIID): void { const sig = PreloadPredictor.tabSignalFor(tabId); if (sig) this.emitSignal(sig); } /** 等待 Tab 预载规则完成后再 openPanel(用户可见路径:优先 interactive,资源已在包内则跳过) */ async awaitTabIntent(tabId: UIID): Promise { const sig = PreloadPredictor.tabSignalFor(tabId); if (!sig) return; if (this._areSignalTokensReady(sig)) return; await awaitWithTimeout( this.awaitSignal(sig, true), LoadRecoveryConfig.tabIntentTimeoutMs, `tabIntent:${tabId}`, ); } /** * MainShell loadResPakg 完成后:Home bundle + PanelHome 已在缓存,登记 tabIntent:Home 避免排队。 */ markMainShellLoaded(): void { this._markTokenDone('bundle:home'); this._markTokenDone('ui:101'); this._markTokenDone('bundle:heros'); this._markTokenDone('pkg:HerosRes'); const rule = PRELOAD_RULES.find((r) => r.when === 'tabIntent:Home'); if (rule) { membershipAdd(this._doneKeys, `tabIntent:Home:${rule.load.join(',')}`); } } private _areSignalTokensReady(signal: string): boolean { const rules = PRELOAD_RULES.filter((r) => r.when === signal); if (rules.length === 0) return true; for (const rule of rules) { if (!this._areRuleTokensResident(rule, signal)) return false; } return true; } private _areRuleTokensResident(rule: typeof PRELOAD_RULES[0], signal: string): boolean { let pending = rule.load.filter((t) => !this._isTokenDone(t)); if (signal === 'enterHall') { pending = pending.filter((t) => !this._shouldSkipEnterHallToken(t)); } return pending.every((t) => this._isTokenAssetResident(t)); } private _markResidentRuleTokens(rule: typeof PRELOAD_RULES[0], signal: string): void { let pending = rule.load.filter((t) => !this._isTokenDone(t)); if (signal === 'enterHall') { pending = pending.filter((t) => !this._shouldSkipEnterHallToken(t)); } for (const token of pending) { if (this._isTokenAssetResident(token)) { this._markTokenDone(token); } } } private _isTokenAssetResident(token: string): boolean { if (token.startsWith('bundle:')) { return !!assetManager.getBundle(token.slice('bundle:'.length)); } if (token.startsWith('ui:')) { const uiId = Number(token.slice('ui:'.length)); if (!Number.isFinite(uiId)) return false; const conf = UIConfigs[uiId as UIID]; if (!conf) return false; const bundle = assetManager.getBundle(conf.bundle); if (!bundle) return false; return !!bundle.get(conf.path + conf.name, Prefab); } return false; } /** 面板打开后触发 panelOpen 规则(子弹窗预测预载) */ onPanelOpened(panelId: number): void { const map: Partial> = { [UIID.Weapon]: 'panelOpen:Weapon', [UIID.Role]: 'panelOpen:Role', }; const sig = map[panelId as UIID]; if (sig) this.emitSignal(sig); } /** 回大厅后重新调度 idle 长尾预载 */ rescheduleHomeIdle(): void { this._scheduleHomeIdle(); } /** * enterHall 完成后静默登记 Home 相关预载(新产品无 MainUI Tab)。 */ warmHighPriorityHallTabs(): void { this.onTabIntent(UIID.Home); } static tabSignalFor(tabId: UIID): string | null { const map: Partial> = { [UIID.Shop]: 'tabIntent:Shop', [UIID.Weapon]: 'tabIntent:Weapon', [UIID.Role]: 'tabIntent:Role', [UIID.Home]: 'tabIntent:Home', }; return map[tabId] ?? null; } /** 主 Tab 对应的 bundle 列表(与 PreloadRules tabIntent 一致) */ static getTabBundlesForPage(tabId: UIID): readonly string[] { const map: Partial> = { [UIID.Shop]: ['shop', 'icons'], [UIID.Weapon]: ['shenjiang', 'icons'], [UIID.Role]: ['role'], }; return map[tabId] ?? []; } /** Tab bundle 卸载后清除预载登记,切回时可重新 tabIntent 预载 */ clearTabPreloadForBundles(bundleNames: readonly string[]): void { if (!bundleNames.length) return; const names = membershipFromArray(bundleNames); for (const name of names.keys()) { this._doneKeys.delete(`token:bundle:${name}`); } const remove: string[] = []; for (const key of this._doneKeys.keys()) { if (!key.startsWith('tabIntent:')) continue; for (const name of names.keys()) { if (key.includes(`bundle:${name}`)) { remove.push(key); break; } } } for (const key of remove) { this._doneKeys.delete(key); } } private _tokenKey(token: string): string { return `token:${token}`; } private _isTokenDone(token: string): boolean { return this._doneKeys.has(this._tokenKey(token)); } private _markTokenDone(token: string): void { membershipAdd(this._doneKeys, this._tokenKey(token)); } private _loadBundleToken(token: string, bundleName: string): Promise { if (this._isTokenDone(token)) return Promise.resolve(); const owner = isTabScopedBundle(bundleName) ? 'tab' : 'res'; return new Promise((resolve) => { gg.res.loadBundle(bundleName, () => { this._markTokenDone(token); resolve(); }, owner); }); } private _scheduleHomeIdle = (): void => { if (!MemoryProfile.current.enableHallWarmLongTail) return; if (this._longTailTimer) clearTimeout(this._longTailTimer); this._longTailTimer = setTimeout(() => { if (gg.ui?.hasPanel?.(UIID.BattleGame)) return; this.kickAllRemainingBundleZips(); this.emitSignal('idleOnHome:5s'); }, MemoryProfile.current.idleLongTailSec * 1000); }; /** tier0/1 跳过低优先级 enterHall 项,减轻首登内存与 IO 峰值 */ private _shouldSkipEnterHallToken(token: string): boolean { MemoryProfile.resolve(); if (MemoryProfile.tier === 'tier3') return false; if (token === 'bundle:chapterGift' || token === 'bundle:diamondShop') return true; if (MemoryProfile.tier === 'tier0') { return token === 'ui:617' || token === 'bundle:role'; } return false; } private async _executeRule(rule: typeof PRELOAD_RULES[0], signal: string): Promise { await this._runRule(rule, signal, false); } private async _executeRuleAwait( rule: typeof PRELOAD_RULES[0], signal: string, forUserWait = false, ): Promise { await this._runRule(rule, signal, forUserWait); } /** * 执行一条预载规则;完成前不写入 done,同 key 复用 inflight。 * @param rule 规则 * @param signal 信号 * @param forUserWait 用户等待时升为 interactive */ private _runRule(rule: typeof PRELOAD_RULES[0], signal: string, forUserWait: boolean): Promise { if (!forUserWait && rule.priority === 'silent' && gg.ui?.hasPanel?.(UIID.BattleGame)) { return Promise.resolve(); } const dedupeKey = `${signal}:${rule.load.join(',')}`; if (this._doneKeys.has(dedupeKey)) return Promise.resolve(); const existing = this._ruleInflight[dedupeKey]; if (existing) return existing; const run = async (): Promise => { if (this._areRuleTokensResident(rule, signal)) { this._markResidentRuleTokens(rule, signal); membershipAdd(this._doneKeys, dedupeKey); return; } const timer = PerfLoadTimer.start(); const queuePriority = forUserWait ? 'interactive' : rule.priority; if (queuePriority === 'silent') { await LoadScheduler.getInstance().enqueue('silent', () => this._loadRuleTokens(rule, signal)); } else { await LoadScheduler.getInstance().enqueue(queuePriority, () => this._loadRuleTokens(rule, signal)); } membershipAdd(this._doneKeys, dedupeKey); if (signal === 'enterHall') { PerfReport.reportPreloadHall(timer.foregroundMs(), timer.wallMs()); } timer.dispose(); }; const p = run().finally(() => { if (this._ruleInflight[dedupeKey] === p) { this._ruleInflight[dedupeKey] = undefined; } }); this._ruleInflight[dedupeKey] = p; return p; } private async _loadRuleTokens(rule: typeof PRELOAD_RULES[0], signal?: string): Promise { let pending = rule.load.filter((t) => !this._isTokenDone(t)); if (signal === 'enterHall') { pending = pending.filter((t) => !this._shouldSkipEnterHallToken(t)); } if (pending.length === 0) return; const parallel = Math.max(1, MemoryProfile.current.silentPreloadParallel); await this._mapWithConcurrency(pending, parallel, (token) => this._loadToken(token, rule)); } private async _loadToken(token: string, rule: typeof PRELOAD_RULES[0]): Promise { if (this._isTokenDone(token)) return; if (token.startsWith('bundle:')) { const name = token.slice('bundle:'.length); await this._loadBundleToken(token, name); return; } if (token === 'pkg:MainShellRes') { await new Promise((resolve, reject) => { gg.res.loadResPakg(getMainShellResPakage(), null, (result) => { if (!result.ok) { reject(new Error(`MainShellRes fail:${result.failed.length}`)); return; } this._markTokenDone(token); resolve(); }); }); return; } if (token === 'pkg:HallWarmRes') { await new Promise((resolve) => { gg.res.preLoadResPakg(getHallWarmResPakage(), null, () => { this._markTokenDone(token); resolve(); }); }); return; } if (token === 'pkg:HallWarmLongTailRes') { await new Promise((resolve) => { gg.res.preLoadResPakg(getHallWarmLongTailResPakage(), null, () => { this._markTokenDone(token); resolve(); }); }); return; } if (token === 'pkg:HerosRes') { await new Promise((resolve) => { gg.res.preLoadResPakg(getHerosResPakage(), null, () => { this._markTokenDone(token); resolve(); }); }); return; } if (token.startsWith('battleScoped:')) { const scoped = token.slice('battleScoped:'.length); const { levelId } = parseBattleScopedToken(scoped); if (levelId) { this.scheduleBattleWarm(levelId, rule.priority === 'interactive' ? 'interactive' : 'silent'); } this._markTokenDone(token); return; } if (token.startsWith('ui:')) { const uiId = Number(token.slice('ui:'.length)); if (Number.isFinite(uiId)) { await this._preloadUiPrefab(uiId); } } } private async _mapWithConcurrency( items: readonly T[], limit: number, fn: (item: T) => Promise, ): Promise { if (items.length === 0) return; let next = 0; const workerCount = Math.min(Math.max(1, limit), items.length); const workers = Array.from({ length: workerCount }, async () => { while (next < items.length) { const idx = next++; await fn(items[idx]); } }); await Promise.all(workers); } private async _preloadUiPrefab(uiId: number): Promise { const token = `ui:${uiId}`; if (this._isTokenDone(token)) return; const conf = UIConfigs[uiId]; if (!conf?.bundle || !conf.path || !conf.name) { console.error('[PreloadPredictor] ui 配置不完整', uiId); return; } const bundleName = conf.bundle; await this._loadBundleToken(`bundle:${bundleName}`, bundleName); const prefabPath = conf.path + conf.name; const bundle = assetManager.getBundle(bundleName); if (!bundle) { console.error('[PreloadPredictor] ui 分包未就绪', bundleName, uiId); return; } await new Promise((resolve) => { gg.res.loadResPublic(bundle, prefabPath, (asset) => { if (!asset?.isValid) { console.error('[PreloadPredictor] load ui 失败', bundleName, prefabPath); resolve(); return; } this._markTokenDone(token); resolve(); }, Prefab); }); } }