You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
262 lines
9.2 KiB
262 lines
9.2 KiB
import { assetManager, Prefab } from 'cc';
|
|
import { UIConfigs, UIID } from './ConfigRes';
|
|
import { HomeBattlePreload } from './HomeBattlePreload';
|
|
import { BattlePerformance } from '../ui/BattleGame/BattlePerformance';
|
|
|
|
type SoftTask =
|
|
| { kind: 'panel'; uiId: number; priority: number }
|
|
| { kind: 'battle'; chapterId: number; priority: number };
|
|
|
|
/**
|
|
* 智能预测预加载:平衡首次打开体验与内存。
|
|
*
|
|
* - 预测:当前 Tab 邻接 + 老玩家意图(离线奖励/签到/商城/武器)
|
|
* - 调度:页面稳定后空闲执行,任务间 yield,单队列
|
|
* - 降级:内存压力 ≥2 暂停;≥3 清空队列
|
|
* - 只 loadBundle + load prefab(不 instantiate),命中 UIManager.bundle.get
|
|
* - 战斗预热仍走 {@link HomeBattlePreload},由本模块触发时机
|
|
*/
|
|
export class SmartPreload {
|
|
private static readonly SETTLE_MS = 700;
|
|
private static readonly YIELD_MS = 40;
|
|
/** 邻接 Tab 最多预热数(Home 另加意图弹窗) */
|
|
private static readonly MAX_TAB_TASKS = 2;
|
|
private static readonly MAX_SOFT_PANELS = 6;
|
|
private static readonly MAIN_TABS: ReadonlySet<number> = new Set([
|
|
UIID.Home, UIID.Shop, UIID.Role, UIID.Weapon, UIID.Replica,
|
|
]);
|
|
|
|
/** 主 Tab 邻接预测(不含自身) */
|
|
private static readonly NEIGHBORS: Readonly<Record<number, number[]>> = {
|
|
[UIID.Home]: [UIID.Weapon, UIID.Shop, UIID.Role, UIID.Replica],
|
|
[UIID.Shop]: [UIID.Home, UIID.Role],
|
|
[UIID.Role]: [UIID.Home, UIID.Weapon],
|
|
[UIID.Weapon]: [UIID.Home, UIID.Role],
|
|
[UIID.Replica]: [UIID.Home, UIID.Shop],
|
|
};
|
|
|
|
private static _paused = false;
|
|
private static _gen = 0;
|
|
private static _timer: ReturnType<typeof setTimeout> | null = null;
|
|
private static _running = false;
|
|
private static _queue: SoftTask[] = [];
|
|
private static _visits = new Map<number, number>();
|
|
private static _softReady = new Set<number>();
|
|
private static _pageId = 0;
|
|
|
|
/** 切到主界面 Tab / 回大厅后调用 */
|
|
static onMainTab(pageId: number): void {
|
|
if (!pageId) return;
|
|
this._pageId = pageId;
|
|
this._bumpVisit(pageId);
|
|
this._paused = false;
|
|
|
|
if (pageId !== UIID.Home) {
|
|
HomeBattlePreload.pause();
|
|
}
|
|
|
|
this._rebuildQueue(pageId);
|
|
this._scheduleRun(SmartPreload.SETTLE_MS);
|
|
}
|
|
|
|
/** 进战斗 / 离开大厅:停掉软预载 */
|
|
static pause(): void {
|
|
this._clearTimer();
|
|
this._paused = true;
|
|
this._gen++;
|
|
this._queue.length = 0;
|
|
HomeBattlePreload.pause();
|
|
}
|
|
|
|
/** 平台内存告警:升档时暂停或清空 */
|
|
static onMemoryPressure(warnLevel: number): void {
|
|
if (warnLevel >= 15 || BattlePerformance.memoryPressureLevel() >= 3) {
|
|
this.pause();
|
|
return;
|
|
}
|
|
if (warnLevel >= 10 || BattlePerformance.memoryPressureLevel() >= 2) {
|
|
this._clearTimer();
|
|
this._queue.length = 0;
|
|
HomeBattlePreload.pause();
|
|
}
|
|
}
|
|
|
|
/** 是否已软预热过该面板(调试用) */
|
|
static isSoftReady(uiId: number): boolean {
|
|
return this._softReady.has(uiId);
|
|
}
|
|
|
|
private static _bumpVisit(uiId: number): void {
|
|
this._visits.set(uiId, (this._visits.get(uiId) ?? 0) + 1);
|
|
}
|
|
|
|
private static _rebuildQueue(pageId: number): void {
|
|
this._queue.length = 0;
|
|
if (this._memBlocked()) return;
|
|
|
|
// Home 意图弹窗优先于邻接 Tab:老玩家上线先点离线/签到
|
|
if (pageId === UIID.Home) {
|
|
this._pushIntentPops();
|
|
}
|
|
|
|
const neighbors = this.NEIGHBORS[pageId] ?? [];
|
|
const scored: SoftTask[] = [];
|
|
for (let i = 0; i < neighbors.length; i++) {
|
|
const id = neighbors[i];
|
|
if (!this._canPreloadPanel(id)) continue;
|
|
const visit = this._visits.get(id) ?? 0;
|
|
scored.push({ kind: 'panel', uiId: id, priority: visit * 10 + (neighbors.length - i) });
|
|
}
|
|
scored.sort((a, b) => b.priority - a.priority);
|
|
|
|
for (let i = 0; i < scored.length && i < SmartPreload.MAX_TAB_TASKS; i++) {
|
|
this._queue.push(scored[i]);
|
|
}
|
|
|
|
if (pageId === UIID.Home) {
|
|
const chapterId = gg?.game?.CurentSelectChapterId || gg?.data?.doc?.chapter || 0;
|
|
if (chapterId > 0) {
|
|
this._queue.push({ kind: 'battle', chapterId, priority: 1 });
|
|
}
|
|
}
|
|
|
|
this._queue.sort((a, b) => b.priority - a.priority);
|
|
}
|
|
|
|
/**
|
|
* 老玩家高频路径(仅 Home):
|
|
* - 离线奖励 wipeOut(章节>1)+ 领奖弹窗 GetReward
|
|
* - 每天首次登录签到 sign7Day
|
|
*/
|
|
private static _pushIntentPops(): void {
|
|
const chapter = gg?.data?.doc?.chapter ?? 0;
|
|
if (chapter > 1) {
|
|
this._enqueuePanel(UIID.wipeOut, 100);
|
|
this._enqueuePanel(UIID.GetReward, 90);
|
|
}
|
|
if (gg?.data?.IsFirstLoginToday) {
|
|
this._enqueuePanel(UIID.sign7Day, 95);
|
|
}
|
|
}
|
|
|
|
private static _enqueuePanel(uiId: number, priority: number): void {
|
|
if (!this._canPreloadPanel(uiId)) return;
|
|
this._queue.push({ kind: 'panel', uiId, priority });
|
|
}
|
|
|
|
private static _canPreloadPanel(uiId: number): boolean {
|
|
if (this._softReady.has(uiId)) return false;
|
|
if (this._softReady.size >= SmartPreload.MAX_SOFT_PANELS) return false;
|
|
if (gg?.ui?.hasPanel?.(uiId)) {
|
|
this._softReady.add(uiId);
|
|
return false;
|
|
}
|
|
const conf = UIConfigs[uiId];
|
|
if (!conf?.bundle || !conf?.name) return false;
|
|
if (this.MAIN_TABS.has(uiId) && uiId !== UIID.Home) {
|
|
try {
|
|
if (gg?.newFun && !gg.newFun.isUnLockMainPageTab(uiId)) {
|
|
return false;
|
|
}
|
|
} catch {
|
|
// 解锁表未就绪时仍允许预载
|
|
}
|
|
}
|
|
return true;
|
|
}
|
|
|
|
private static _memBlocked(): boolean {
|
|
return BattlePerformance.memoryPressureLevel() >= 2;
|
|
}
|
|
|
|
private static _scheduleRun(delayMs: number): void {
|
|
this._clearTimer();
|
|
if (this._paused || this._queue.length === 0) return;
|
|
this._timer = setTimeout(() => {
|
|
this._timer = null;
|
|
void this._drain();
|
|
}, delayMs);
|
|
}
|
|
|
|
private static _clearTimer(): void {
|
|
if (this._timer != null) {
|
|
clearTimeout(this._timer);
|
|
this._timer = null;
|
|
}
|
|
}
|
|
|
|
private static _alive(gen: number): boolean {
|
|
return !this._paused && this._gen === gen && !gg?.game?.CurentBattle;
|
|
}
|
|
|
|
private static _yield(): Promise<void> {
|
|
return new Promise((r) => setTimeout(r, SmartPreload.YIELD_MS));
|
|
}
|
|
|
|
private static async _drain(): Promise<void> {
|
|
if (this._running) return;
|
|
const gen = ++this._gen;
|
|
this._running = true;
|
|
try {
|
|
while (this._alive(gen) && this._queue.length > 0) {
|
|
if (this._memBlocked()) {
|
|
this._queue.length = 0;
|
|
break;
|
|
}
|
|
const task = this._queue.shift();
|
|
if (!task) break;
|
|
if (task.kind === 'panel') {
|
|
await this._preloadPanel(task.uiId);
|
|
} else if (task.kind === 'battle') {
|
|
// 邻接 Tab 已优先;再错峰启动战斗包预热
|
|
HomeBattlePreload.scheduleResume(task.chapterId, 800);
|
|
}
|
|
if (!this._alive(gen)) break;
|
|
await this._yield();
|
|
}
|
|
} catch (e) {
|
|
console.warn('[SmartPreload] drain failed', e);
|
|
} finally {
|
|
if (this._gen === gen) {
|
|
this._running = false;
|
|
}
|
|
}
|
|
}
|
|
|
|
/** 仅缓存 prefab,供 UIManager.openPanel 命中 bundle.get */
|
|
private static async _preloadPanel(uiId: number): Promise<void> {
|
|
if (this._softReady.has(uiId)) return;
|
|
const conf = UIConfigs[uiId];
|
|
if (!conf) return;
|
|
const path = conf.path + conf.name;
|
|
try {
|
|
let bundle = assetManager.getBundle(conf.bundle);
|
|
if (!bundle) {
|
|
bundle = await new Promise((resolve) => {
|
|
assetManager.loadBundle(conf.bundle, (err, b) => {
|
|
resolve(err ? null : b);
|
|
});
|
|
});
|
|
}
|
|
if (!bundle) return;
|
|
if (bundle.get(path, Prefab)) {
|
|
this._softReady.add(uiId);
|
|
console.log(`[SmartPreload] hit ${conf.bundle}/${path}`);
|
|
return;
|
|
}
|
|
await new Promise<void>((resolve) => {
|
|
bundle!.load(path, Prefab, (err) => {
|
|
if (!err) {
|
|
this._softReady.add(uiId);
|
|
console.log(`[SmartPreload] ready ${conf.bundle}/${path}`);
|
|
} else {
|
|
console.warn(`[SmartPreload] load fail ${conf.bundle}/${path}`, err);
|
|
}
|
|
resolve();
|
|
});
|
|
});
|
|
} catch (e) {
|
|
console.warn('[SmartPreload] preloadPanel', uiId, e);
|
|
}
|
|
}
|
|
}
|
|
|