将游戏内 玩法介绍弹窗的释放做了优化,并且下载做了提前下载bundle,优先级放在了 GameScene 最低优先级。
This commit is contained in:
parent
ebe5a4ec35
commit
9ae6b0e6af
File diff suppressed because it is too large
Load Diff
|
|
@ -1213,6 +1213,7 @@ export default class MapConroler extends cc.Component {
|
||||||
if (!this.isMapRuntimeAlive()) return;
|
if (!this.isMapRuntimeAlive()) return;
|
||||||
this.ensureWinLoaded(error => onComplete(error));
|
this.ensureWinLoaded(error => onComplete(error));
|
||||||
} : null,
|
} : null,
|
||||||
|
() => this.SceneManager.scheduleGameplayIntroPredownload(),
|
||||||
);
|
);
|
||||||
}, 0);
|
}, 0);
|
||||||
};
|
};
|
||||||
|
|
|
||||||
|
|
@ -45,10 +45,17 @@ export default class SceneManager extends cc.Component {
|
||||||
if (bundleName === "action_bundle") return;
|
if (bundleName === "action_bundle") return;
|
||||||
if (!bundle) return;
|
if (!bundle) return;
|
||||||
setTimeout(() => {
|
setTimeout(() => {
|
||||||
if (cc.fx.GameTool && cc.fx.GameTool.shouldRetainGameplayPopupBundlesForGameSceneReload
|
if (bundleName !== "gameplay_intro" && cc.fx.GameTool && cc.fx.GameTool.shouldRetainGameplayPopupBundlesForGameSceneReload
|
||||||
&& cc.fx.GameTool.shouldRetainGameplayPopupBundlesForGameSceneReload()) return;
|
&& cc.fx.GameTool.shouldRetainGameplayPopupBundlesForGameSceneReload()) return;
|
||||||
const active = SceneManager.activeInstance;
|
const active = SceneManager.activeInstance;
|
||||||
if (active && cc.isValid(active, true)) return;
|
if (active && cc.isValid(active, true)) {
|
||||||
|
if (bundleName !== "gameplay_intro") return;
|
||||||
|
// 旧场景晚到的介绍请求,仅在新场景确实接管此包时保留。
|
||||||
|
if (Object.keys(active.dynamicPopupStates || {}).some(name => {
|
||||||
|
const state = active.dynamicPopupStates[name];
|
||||||
|
return state.bundle === bundle || (state.loading && state.bundleName === bundleName);
|
||||||
|
})) return;
|
||||||
|
}
|
||||||
if (cc.assetManager.getBundle(bundleName) !== bundle) return;
|
if (cc.assetManager.getBundle(bundleName) !== bundle) return;
|
||||||
bundle.releaseAll();
|
bundle.releaseAll();
|
||||||
if (cc.assetManager.getBundle(bundleName) === bundle) {
|
if (cc.assetManager.getBundle(bundleName) === bundle) {
|
||||||
|
|
@ -210,6 +217,36 @@ export default class SceneManager extends cc.Component {
|
||||||
private dynamicPopupRetries: { [popupName: string]: { displayName: string; retry: () => void } } = {};
|
private dynamicPopupRetries: { [popupName: string]: { displayName: string; retry: () => void } } = {};
|
||||||
private dynamicPopupStates: { [popupName: string]: DynamicPopupState } = {};
|
private dynamicPopupStates: { [popupName: string]: DynamicPopupState } = {};
|
||||||
private popupBundleWarmupStarted: boolean = false;
|
private popupBundleWarmupStarted: boolean = false;
|
||||||
|
private gameplayIntroPredownloadScheduled: boolean = false;
|
||||||
|
|
||||||
|
/** 地图就绪且弹窗预热结束后调用;持续空闲一秒才派发,只下载,不 loadBundle。 */
|
||||||
|
scheduleGameplayIntroPredownload() {
|
||||||
|
//@ts-ignore
|
||||||
|
if (cc.sys.platform !== cc.sys.WECHAT_GAME || typeof wx === "undefined" || !wx.preDownloadSubpackage) return;
|
||||||
|
if (this.gameplayIntroPredownloadScheduled || !this.isPopupRuntimeAlive()) return;
|
||||||
|
this.gameplayIntroPredownloadScheduled = true;
|
||||||
|
let idle = false;
|
||||||
|
const check = () => {
|
||||||
|
if (!this.isPopupRuntimeAlive()) return;
|
||||||
|
const tool = cc.fx.GameTool;
|
||||||
|
if (tool.isGameplayIntroDownloaded()) return;
|
||||||
|
const busy = tool.hasPendingGameplayResourceLoads() || cc.fx.GameConfig.isGameplayEntryPreparing()
|
||||||
|
|| Object.keys(this.dynamicPopupLoads).length > 0
|
||||||
|
|| Object.keys(this.dynamicPopupStates).some(name => this.dynamicPopupStates[name].loading);
|
||||||
|
if (busy || !idle) {
|
||||||
|
idle = !busy;
|
||||||
|
this.scheduleOnce(check, 1);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
tool.predownloadGameplayIntro().then(ready => {
|
||||||
|
if (!ready && this.isPopupRuntimeAlive()) {
|
||||||
|
idle = false;
|
||||||
|
this.scheduleOnce(check, 10);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
};
|
||||||
|
this.scheduleOnce(check, 1);
|
||||||
|
}
|
||||||
|
|
||||||
warmupGameplayPopupBundles(
|
warmupGameplayPopupBundles(
|
||||||
preloadInstances: boolean = false,
|
preloadInstances: boolean = false,
|
||||||
|
|
@ -297,11 +334,15 @@ export default class SceneManager extends cc.Component {
|
||||||
&& !!cc.fx.GameTool
|
&& !!cc.fx.GameTool
|
||||||
&& !!cc.fx.GameTool.isIOSDevice
|
&& !!cc.fx.GameTool.isIOSDevice
|
||||||
&& cc.fx.GameTool.isIOSDevice();
|
&& cc.fx.GameTool.isIOSDevice();
|
||||||
if (!shouldWarmupPauseBundle) return;
|
if (!shouldWarmupPauseBundle) {
|
||||||
|
if (onComplete) onComplete();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
const loadedPauseBundle = cc.assetManager.getBundle("pause");
|
const loadedPauseBundle = cc.assetManager.getBundle("pause");
|
||||||
if (loadedPauseBundle) {
|
if (loadedPauseBundle) {
|
||||||
console.log("[PopupBundle] already loaded: pause (iOS low-memory warmup)");
|
console.log("[PopupBundle] already loaded: pause (iOS low-memory warmup)");
|
||||||
|
if (onComplete) onComplete();
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
cc.assetManager.loadBundle("pause", (error: Error, bundle: cc.AssetManager.Bundle) => {
|
cc.assetManager.loadBundle("pause", (error: Error, bundle: cc.AssetManager.Bundle) => {
|
||||||
|
|
@ -311,9 +352,11 @@ export default class SceneManager extends cc.Component {
|
||||||
}
|
}
|
||||||
if (error) {
|
if (error) {
|
||||||
cc.warn("[PopupBundle] loadBundle failed: pause (iOS low-memory warmup)", error.message || error);
|
cc.warn("[PopupBundle] loadBundle failed: pause (iOS low-memory warmup)", error.message || error);
|
||||||
|
if (onComplete) onComplete(error);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
console.log("[PopupBundle] loadBundle complete: pause (iOS low-memory warmup)");
|
console.log("[PopupBundle] loadBundle complete: pause (iOS low-memory warmup)");
|
||||||
|
if (onComplete) onComplete();
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -550,7 +593,10 @@ export default class SceneManager extends cc.Component {
|
||||||
state.root = null;
|
state.root = null;
|
||||||
const bundle = state.bundle || cc.assetManager.getBundle(state.bundleName);
|
const bundle = state.bundle || cc.assetManager.getBundle(state.bundleName);
|
||||||
state.bundle = null;
|
state.bundle = null;
|
||||||
if (bundle && !retainBundle) this.releaseDynamicPopupBundle(state.bundleName, bundle);
|
// 介绍只缓存到当前关,不能混入五个常用弹窗的切关保留策略。
|
||||||
|
if (bundle && (!retainBundle || state.bundleName === "gameplay_intro")) {
|
||||||
|
this.releaseDynamicPopupBundle(state.bundleName, bundle);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
this.finishDynamicPopupLoad(popupName);
|
this.finishDynamicPopupLoad(popupName);
|
||||||
}
|
}
|
||||||
|
|
@ -879,6 +925,8 @@ export default class SceneManager extends cc.Component {
|
||||||
&& cc.fx.GameTool.shouldRetainGameplayPopupBundlesForGameSceneReload();
|
&& cc.fx.GameTool.shouldRetainGameplayPopupBundlesForGameSceneReload();
|
||||||
Object.keys(this.dynamicPopupStates || {}).forEach((popupName) =>
|
Object.keys(this.dynamicPopupStates || {}).forEach((popupName) =>
|
||||||
this.releaseDynamicPopup(popupName, true, retainPopupBundles));
|
this.releaseDynamicPopup(popupName, true, retainPopupBundles));
|
||||||
|
const introBundle = cc.assetManager.getBundle("gameplay_intro");
|
||||||
|
if (introBundle) this.releaseDynamicPopupBundleIfUnused("gameplay_intro", introBundle);
|
||||||
if (!retainPopupBundles) {
|
if (!retainPopupBundles) {
|
||||||
["pause", "NewMode", "propWindow", "lose"].forEach((bundleName) => {
|
["pause", "NewMode", "propWindow", "lose"].forEach((bundleName) => {
|
||||||
const bundle = cc.assetManager.getBundle(bundleName);
|
const bundle = cc.assetManager.getBundle(bundleName);
|
||||||
|
|
|
||||||
|
|
@ -718,7 +718,7 @@ export class GameConfig {
|
||||||
vibrateOpen: true, //震动
|
vibrateOpen: true, //震动
|
||||||
coinnum: 0, //每局的金币数
|
coinnum: 0, //每局的金币数
|
||||||
paid_user: false, //是否是付费用户
|
paid_user: false, //是否是付费用户
|
||||||
version: "1.0.58", //版本号
|
version: "1.0.59", //版本号
|
||||||
shushu_DistinctId: "", //数数访客ID
|
shushu_DistinctId: "", //数数访客ID
|
||||||
shushu_AccountId: "", //数数账号ID
|
shushu_AccountId: "", //数数账号ID
|
||||||
uid: "", //用户和后端唯一id
|
uid: "", //用户和后端唯一id
|
||||||
|
|
|
||||||
|
|
@ -27,6 +27,7 @@ window.initMgr = function () {
|
||||||
cc.fx.GameConfig = GameConfig;
|
cc.fx.GameConfig = GameConfig;
|
||||||
cc.fx.HttpUtil = HttpUtil;
|
cc.fx.HttpUtil = HttpUtil;
|
||||||
cc.fx.GameTool = GameTool;
|
cc.fx.GameTool = GameTool;
|
||||||
|
GameTool.trackResourceLoads();
|
||||||
cc.fx.Notifications = Notifications;
|
cc.fx.Notifications = Notifications;
|
||||||
|
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -65,6 +65,12 @@ export default class BlockAssetManager {
|
||||||
private foregroundLoads = 0;
|
private foregroundLoads = 0;
|
||||||
private entryDownloadHolds = 0;
|
private entryDownloadHolds = 0;
|
||||||
|
|
||||||
|
/** 包括直存下载、排队任务和整批后台缓存,供最低优先级分包让路。 */
|
||||||
|
hasPendingLoads(): boolean {
|
||||||
|
return this.foregroundLoads > 0 || this.entryDownloadHolds > 0 || this.backgroundRunning
|
||||||
|
|| this.loading.size > 0 || this.fileDownloads.size > 0 || this.downloadQueue.length > 0;
|
||||||
|
}
|
||||||
|
|
||||||
/** 只停止后台派发;已在途下载继续完成,进关所需文件仍可提优先级。 */
|
/** 只停止后台派发;已在途下载继续完成,进关所需文件仍可提优先级。 */
|
||||||
holdBackgroundDownloads(): () => void {
|
holdBackgroundDownloads(): () => void {
|
||||||
this.entryDownloadHolds++;
|
this.entryDownloadHolds++;
|
||||||
|
|
|
||||||
|
|
@ -5,6 +5,7 @@ import Freeze from "../../prop/Freeze";
|
||||||
import SceneManager from "../../SceneManager";
|
import SceneManager from "../../SceneManager";
|
||||||
import { MiniGameSdk } from "../../Sdk/MiniGameSdk";
|
import { MiniGameSdk } from "../../Sdk/MiniGameSdk";
|
||||||
import Utils from "../Pay/Utils";
|
import Utils from "../Pay/Utils";
|
||||||
|
import BlockAssetManager from "./BlockAssetManager";
|
||||||
import {
|
import {
|
||||||
starterPackReward, applyStarterPackStatus, starterPackRemaining, starterPackNow,
|
starterPackReward, applyStarterPackStatus, starterPackRemaining, starterPackNow,
|
||||||
starterPackDay, starterPackPopupKey, starterPackFailureKey, starterPackRecordResult
|
starterPackDay, starterPackPopupKey, starterPackFailureKey, starterPackRecordResult
|
||||||
|
|
@ -12,7 +13,7 @@ import {
|
||||||
//@ts-ignore
|
//@ts-ignore
|
||||||
//最大工具类 各种公共方法,以及处理上传,获取后端接口数据
|
//最大工具类 各种公共方法,以及处理上传,获取后端接口数据
|
||||||
var GameTool = {
|
var GameTool = {
|
||||||
MAX_NORMAL_LEVEL: 1980,
|
MAX_NORMAL_LEVEL: 1960,
|
||||||
starterPackReactivationUid: "",
|
starterPackReactivationUid: "",
|
||||||
starterPackQueuedReason: "",
|
starterPackQueuedReason: "",
|
||||||
MEMORY_WARNING_EVENT: "game-memory-warning",
|
MEMORY_WARNING_EVENT: "game-memory-warning",
|
||||||
|
|
@ -34,6 +35,73 @@ var GameTool = {
|
||||||
_lowMemoryMode: false,
|
_lowMemoryMode: false,
|
||||||
_memoryWarningCount: 0,
|
_memoryWarningCount: 0,
|
||||||
_gameplayPopupSubpackageDownloaded: {},
|
_gameplayPopupSubpackageDownloaded: {},
|
||||||
|
_resourceLoadTrackingInstalled: false,
|
||||||
|
_pendingResourceLoads: 0,
|
||||||
|
_gameplayIntroDownload: null as Promise<boolean>,
|
||||||
|
|
||||||
|
/** 在启动入口安装;公开管线覆盖 load/preload/Bundle 及其排队、解析阶段。 */
|
||||||
|
trackResourceLoads() {
|
||||||
|
if (this._resourceLoadTrackingInstalled) return;
|
||||||
|
this._resourceLoadTrackingInstalled = true;
|
||||||
|
[cc.assetManager.pipeline, cc.assetManager.fetchPipeline].forEach(pipeline => {
|
||||||
|
pipeline.insert((task, done) => {
|
||||||
|
this._pendingResourceLoads++;
|
||||||
|
const complete = task.onComplete;
|
||||||
|
let finished = false;
|
||||||
|
task.onComplete = (...args) => {
|
||||||
|
if (!finished) {
|
||||||
|
finished = true;
|
||||||
|
this._pendingResourceLoads--;
|
||||||
|
}
|
||||||
|
if (complete) complete.apply(task, args);
|
||||||
|
};
|
||||||
|
task.output = task.input;
|
||||||
|
done(null);
|
||||||
|
}, 0);
|
||||||
|
});
|
||||||
|
},
|
||||||
|
|
||||||
|
hasPendingGameplayResourceLoads(): boolean {
|
||||||
|
return !this._resourceLoadTrackingInstalled || this._pendingResourceLoads > 0
|
||||||
|
|| !!this._gameplayPopupSubpackagePreloadPromise || !!this._gameplayPopupMemoryWarmupPromise
|
||||||
|
|| BlockAssetManager.instance.hasPendingLoads();
|
||||||
|
},
|
||||||
|
|
||||||
|
isGameplayIntroDownloaded(): boolean {
|
||||||
|
return !!this._gameplayPopupSubpackageDownloaded.gameplay_intro
|
||||||
|
|| !!cc.assetManager.getBundle("gameplay_intro");
|
||||||
|
},
|
||||||
|
|
||||||
|
/** 仅预下载;跨启动的磁盘缓存由微信检查,不持久化可能失效的“已下载”标记。 */
|
||||||
|
predownloadGameplayIntro(): Promise<boolean> {
|
||||||
|
if (this.isGameplayIntroDownloaded()) return Promise.resolve(true);
|
||||||
|
if (this._gameplayIntroDownload) return this._gameplayIntroDownload;
|
||||||
|
// 老基础库不回退到 loadSubpackage,避免后台预下载顺带执行分包脚本。
|
||||||
|
//@ts-ignore
|
||||||
|
const wechat = typeof wx !== "undefined" ? wx : null;
|
||||||
|
if (!wechat || !wechat.preDownloadSubpackage) return Promise.resolve(false);
|
||||||
|
this._gameplayIntroDownload = new Promise<boolean>(resolve => {
|
||||||
|
console.log("[GameplayIntro] idle subpackage predownload start");
|
||||||
|
try {
|
||||||
|
wechat.preDownloadSubpackage({
|
||||||
|
name: "gameplay_intro",
|
||||||
|
success: () => {
|
||||||
|
this._gameplayPopupSubpackageDownloaded.gameplay_intro = true;
|
||||||
|
console.log("[GameplayIntro] subpackage cached; Bundle remains unloaded");
|
||||||
|
resolve(true);
|
||||||
|
},
|
||||||
|
fail: error => { cc.warn("[GameplayIntro] predownload failed", error); resolve(false); },
|
||||||
|
});
|
||||||
|
} catch (error) {
|
||||||
|
cc.warn("[GameplayIntro] predownload failed", error);
|
||||||
|
resolve(false);
|
||||||
|
}
|
||||||
|
}).then(ready => {
|
||||||
|
this._gameplayIntroDownload = null;
|
||||||
|
return ready;
|
||||||
|
});
|
||||||
|
return this._gameplayIntroDownload;
|
||||||
|
},
|
||||||
_gameplayPopupSubpackageRetryTimer: null,
|
_gameplayPopupSubpackageRetryTimer: null,
|
||||||
_gameplayPopupSubpackagePreloadPromise: null,
|
_gameplayPopupSubpackagePreloadPromise: null,
|
||||||
_gameplayPopupMemoryWarmupPromise: null,
|
_gameplayPopupMemoryWarmupPromise: null,
|
||||||
|
|
|
||||||
13
assets/gacha_bundle/img/cat.meta
Normal file
13
assets/gacha_bundle/img/cat.meta
Normal file
|
|
@ -0,0 +1,13 @@
|
||||||
|
{
|
||||||
|
"ver": "1.1.3",
|
||||||
|
"uuid": "c9754f56-ad49-4bcc-a712-8f8e9e59dbb0",
|
||||||
|
"importer": "folder",
|
||||||
|
"isBundle": false,
|
||||||
|
"bundleName": "",
|
||||||
|
"priority": 1,
|
||||||
|
"compressionType": {},
|
||||||
|
"optimizeHotUpdate": {},
|
||||||
|
"inlineSpriteFrames": {},
|
||||||
|
"isRemoteBundle": {},
|
||||||
|
"subMetas": {}
|
||||||
|
}
|
||||||
13
assets/libs/dn-sdk-minigame.meta
Normal file
13
assets/libs/dn-sdk-minigame.meta
Normal file
|
|
@ -0,0 +1,13 @@
|
||||||
|
{
|
||||||
|
"ver": "1.1.3",
|
||||||
|
"uuid": "543de93e-fba1-410a-a3cf-8b39b912464a",
|
||||||
|
"importer": "folder",
|
||||||
|
"isBundle": false,
|
||||||
|
"bundleName": "",
|
||||||
|
"priority": 1,
|
||||||
|
"compressionType": {},
|
||||||
|
"optimizeHotUpdate": {},
|
||||||
|
"inlineSpriteFrames": {},
|
||||||
|
"isRemoteBundle": {},
|
||||||
|
"subMetas": {}
|
||||||
|
}
|
||||||
13
assets/passCheck/script.meta
Normal file
13
assets/passCheck/script.meta
Normal file
|
|
@ -0,0 +1,13 @@
|
||||||
|
{
|
||||||
|
"ver": "1.1.3",
|
||||||
|
"uuid": "4a7bec4e-6a81-4d81-86d8-edf1656cf2cf",
|
||||||
|
"importer": "folder",
|
||||||
|
"isBundle": false,
|
||||||
|
"bundleName": "",
|
||||||
|
"priority": 1,
|
||||||
|
"compressionType": {},
|
||||||
|
"optimizeHotUpdate": {},
|
||||||
|
"inlineSpriteFrames": {},
|
||||||
|
"isRemoteBundle": {},
|
||||||
|
"subMetas": {}
|
||||||
|
}
|
||||||
13
assets/pause/texture.meta
Normal file
13
assets/pause/texture.meta
Normal file
|
|
@ -0,0 +1,13 @@
|
||||||
|
{
|
||||||
|
"ver": "1.1.3",
|
||||||
|
"uuid": "ccd7701f-585a-411b-a8ef-a9f03f22189f",
|
||||||
|
"importer": "folder",
|
||||||
|
"isBundle": false,
|
||||||
|
"bundleName": "",
|
||||||
|
"priority": 1,
|
||||||
|
"compressionType": {},
|
||||||
|
"optimizeHotUpdate": {},
|
||||||
|
"inlineSpriteFrames": {},
|
||||||
|
"isRemoteBundle": {},
|
||||||
|
"subMetas": {}
|
||||||
|
}
|
||||||
|
|
@ -202,6 +202,7 @@ function mapFixture() {
|
||||||
const manager = Object.assign(new Manager(), {
|
const manager = Object.assign(new Manager(), {
|
||||||
node: { getChildByName: () => ({}) }, isPopupRuntimeAlive: () => !state.destroyed,
|
node: { getChildByName: () => ({}) }, isPopupRuntimeAlive: () => !state.destroyed,
|
||||||
scheduleOnce: done => state.frames.push(done),
|
scheduleOnce: done => state.frames.push(done),
|
||||||
|
scheduleGameplayIntroPredownload: () => state.events.push('intro-idle'),
|
||||||
loadDynamicPopup(name, bundle, prefab, parent, done, showLoading) {
|
loadDynamicPopup(name, bundle, prefab, parent, done, showLoading) {
|
||||||
assert.equal(showLoading, false); state.popupCallbacks.push({ name, done });
|
assert.equal(showLoading, false); state.popupCallbacks.push({ name, done });
|
||||||
},
|
},
|
||||||
|
|
@ -299,7 +300,7 @@ test('memory downgrade before map readiness still reveals the map but skips Pref
|
||||||
ready.wall(); assert.equal(state.events.includes('down'), false);
|
ready.wall(); assert.equal(state.events.includes('down'), false);
|
||||||
ready.block(); state.frames.shift()();
|
ready.block(); state.frames.shift()();
|
||||||
assert.equal(Config.firstGameplayEntryComplete, true);
|
assert.equal(Config.firstGameplayEntryComplete, true);
|
||||||
assert.equal(state.holds, 0); assert.equal(state.events.at(-1), 'down');
|
assert.equal(state.holds, 0); assert.deepEqual(state.events.slice(-2), ['down', 'intro-idle']);
|
||||||
assert.equal(state.popupCallbacks.length, 0);
|
assert.equal(state.popupCallbacks.length, 0);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
|
||||||
196
tools/test-gameplay-intro-loading.cjs
Normal file
196
tools/test-gameplay-intro-loading.cjs
Normal file
|
|
@ -0,0 +1,196 @@
|
||||||
|
// Run: node --test tools/test-gameplay-intro-loading.cjs
|
||||||
|
const test = require('node:test');
|
||||||
|
const assert = require('node:assert/strict');
|
||||||
|
const fs = require('node:fs');
|
||||||
|
const vm = require('node:vm');
|
||||||
|
const ts = require('typescript');
|
||||||
|
|
||||||
|
function loadClass(file, names, globals) {
|
||||||
|
const source = ts.createSourceFile(file, fs.readFileSync(file, 'utf8'), ts.ScriptTarget.Latest, true);
|
||||||
|
const declaration = source.statements.find(ts.isClassDeclaration);
|
||||||
|
const members = names.map(name => declaration.members.find(m => m.name && m.name.getText(source) === name).getText(source));
|
||||||
|
return vm.runInNewContext(ts.transpileModule(`class ${declaration.name.text} { ${members.join('\n')} }\n${declaration.name.text}`, {
|
||||||
|
compilerOptions: { target: ts.ScriptTarget.ES2017 },
|
||||||
|
}).outputText, globals);
|
||||||
|
}
|
||||||
|
const flush = async () => { for (let i = 0; i < 5; i++) await Promise.resolve(); };
|
||||||
|
|
||||||
|
function fixture() {
|
||||||
|
const state = { blocksBusy: false, entryBusy: false, destroyed: false, requests: [], schedules: [], timers: [], bundles: new Map(), released: [] };
|
||||||
|
const pipeline = () => ({ pipes: [], insert(fn, index) { this.pipes.splice(index, 0, fn); } });
|
||||||
|
const cc = {
|
||||||
|
sys: { platform: 'wechat', WECHAT_GAME: 'wechat' }, fx: {}, warn() {},
|
||||||
|
Node: { EventType: { TOUCH_END: 'touchend' } },
|
||||||
|
isValid: node => !!node && !node.destroyed,
|
||||||
|
assetManager: {
|
||||||
|
pipeline: pipeline(), fetchPipeline: pipeline(),
|
||||||
|
getBundle: name => state.bundles.get(name),
|
||||||
|
removeBundle: bundle => state.bundles.delete(bundle.name),
|
||||||
|
loadBundle: () => assert.fail('background predownload must not load a Bundle'),
|
||||||
|
},
|
||||||
|
};
|
||||||
|
const wx = { preDownloadSubpackage: options => state.requests.push(options) };
|
||||||
|
const BlockAssetManager = { instance: { hasPendingLoads: () => state.blocksBusy } };
|
||||||
|
const mod = { exports: {} };
|
||||||
|
vm.runInNewContext(ts.transpileModule(fs.readFileSync('assets/Script/module/Tool/GameTool.ts', 'utf8'), {
|
||||||
|
compilerOptions: { target: ts.ScriptTarget.ES2017, module: ts.ModuleKind.CommonJS },
|
||||||
|
}).outputText, {
|
||||||
|
module: mod, exports: mod.exports, cc, wx, console: { log() {} },
|
||||||
|
require: name => name.endsWith('/BlockAssetManager') ? { default: BlockAssetManager } : { default: {}, MiniGameSdk: {} },
|
||||||
|
});
|
||||||
|
const tool = cc.fx.GameTool = mod.exports.GameTool;
|
||||||
|
tool.trackResourceLoads();
|
||||||
|
cc.fx.GameConfig = { isGameplayEntryPreparing: () => state.entryBusy };
|
||||||
|
const Manager = loadClass('assets/Script/SceneManager.ts', [
|
||||||
|
'activeInstance', 'scheduleGameplayIntroPredownload', 'warmupGameplayPopupBundles',
|
||||||
|
'releaseDynamicPopup', 'releaseDynamicPopupBundle', 'releaseDynamicPopupBundleIfUnused',
|
||||||
|
'releaseOrphanedPopupBundle', 'onDestroy',
|
||||||
|
], { cc, wx, console: { log() {} }, setTimeout: fn => state.timers.push(fn) });
|
||||||
|
const manager = Object.assign(new Manager(), {
|
||||||
|
node: { getChildByName: () => null, off() {} },
|
||||||
|
dynamicPopupStates: {}, dynamicPopupLoads: {},
|
||||||
|
isPopupRuntimeAlive: () => !state.destroyed,
|
||||||
|
scheduleOnce: (fn, delay) => state.schedules.push({ fn, delay }),
|
||||||
|
finishDynamicPopupLoad() {}, unbindLosePanel() {}, unbindPropWindowPanel() {},
|
||||||
|
clearDynamicPopupRetry() {}, clearFlyCoins() {}, disposeJungleTreasure() {},
|
||||||
|
});
|
||||||
|
tool.shouldRetainGameplayPopupBundlesForGameSceneReload = () => true;
|
||||||
|
Manager.activeInstance = manager;
|
||||||
|
const tick = () => { assert.ok(state.schedules.length); state.schedules.shift().fn(); };
|
||||||
|
const bundle = name => {
|
||||||
|
const result = { name, releaseAll: () => state.released.push(name) };
|
||||||
|
state.bundles.set(name, result); return result;
|
||||||
|
};
|
||||||
|
return { state, cc, wx, tool, manager, Manager, tick, bundle };
|
||||||
|
}
|
||||||
|
|
||||||
|
test('resource tracking preserves pipeline input/results and drains success and failure exactly once', () => {
|
||||||
|
const { cc, tool } = fixture();
|
||||||
|
tool.trackResourceLoads();
|
||||||
|
for (const pipeline of [cc.assetManager.pipeline, cc.assetManager.fetchPipeline]) {
|
||||||
|
assert.equal(pipeline.pipes.length, 1, 'tracking is installed once');
|
||||||
|
let callbackArgs;
|
||||||
|
const task = { input: ['asset'], onComplete(...args) { callbackArgs = args; } };
|
||||||
|
pipeline.pipes[0](task, () => assert.equal(task.output, task.input));
|
||||||
|
assert.equal(tool.hasPendingGameplayResourceLoads(), true);
|
||||||
|
const error = new Error('download failed');
|
||||||
|
task.onComplete(error, 'result');
|
||||||
|
assert.deepEqual(callbackArgs, [error, 'result']);
|
||||||
|
assert.equal(tool.hasPendingGameplayResourceLoads(), false);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
test('Block idle check includes queued, in-flight, decoding and whole background batches', () => {
|
||||||
|
const Block = loadClass('assets/Script/module/Tool/BlockAssetManager.ts', ['hasPendingLoads'], {});
|
||||||
|
const block = Object.assign(new Block(), {
|
||||||
|
foregroundLoads: 0, entryDownloadHolds: 0, backgroundRunning: false,
|
||||||
|
loading: new Map(), fileDownloads: new Map(), downloadQueue: [],
|
||||||
|
});
|
||||||
|
assert.equal(block.hasPendingLoads(), false);
|
||||||
|
for (const key of ['foregroundLoads', 'entryDownloadHolds']) {
|
||||||
|
block[key] = 1; assert.equal(block.hasPendingLoads(), true); block[key] = 0;
|
||||||
|
}
|
||||||
|
for (const key of ['loading', 'fileDownloads']) {
|
||||||
|
block[key].set('pending', {}); assert.equal(block.hasPendingLoads(), true); block[key].clear();
|
||||||
|
}
|
||||||
|
block.backgroundRunning = true; assert.equal(block.hasPendingLoads(), true); block.backgroundRunning = false;
|
||||||
|
block.downloadQueue.push({}); assert.equal(block.hasPendingLoads(), true);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('intro waits for block queue, engine loading, popup downloads, user popup and stable idle', async () => {
|
||||||
|
const { state, cc, tool, manager, tick } = fixture();
|
||||||
|
manager.scheduleGameplayIntroPredownload();
|
||||||
|
manager.scheduleGameplayIntroPredownload();
|
||||||
|
assert.equal(state.schedules.length, 1);
|
||||||
|
state.blocksBusy = true; tick(); tick();
|
||||||
|
state.blocksBusy = false;
|
||||||
|
const task = { input: 'queued asset' };
|
||||||
|
cc.assetManager.pipeline.pipes[0](task, () => {}); tick();
|
||||||
|
task.onComplete(null, {});
|
||||||
|
tool._gameplayPopupSubpackagePreloadPromise = {}; tick();
|
||||||
|
tool._gameplayPopupSubpackagePreloadPromise = null;
|
||||||
|
tool._gameplayPopupMemoryWarmupPromise = {}; tick();
|
||||||
|
tool._gameplayPopupMemoryWarmupPromise = null;
|
||||||
|
manager.dynamicPopupLoads.pause = true; tick();
|
||||||
|
delete manager.dynamicPopupLoads.pause;
|
||||||
|
state.entryBusy = true; tick(); state.entryBusy = false;
|
||||||
|
tick(); assert.equal(state.requests.length, 0);
|
||||||
|
tick(); assert.equal(state.requests.length, 1);
|
||||||
|
assert.equal(state.requests[0].name, 'gameplay_intro');
|
||||||
|
state.requests[0].success(); await flush();
|
||||||
|
assert.equal(tool.isGameplayIntroDownloaded(), true);
|
||||||
|
assert.equal(state.bundles.size, 0, 'disk caching does not register a Bundle');
|
||||||
|
await tool.predownloadGameplayIntro();
|
||||||
|
assert.equal(state.requests.length, 1, 'successful session cache avoids duplicate download checks');
|
||||||
|
});
|
||||||
|
|
||||||
|
test('failure retries only after a delay and another idle check; leaving scene cancels pending work', async () => {
|
||||||
|
const { state, manager, tick } = fixture();
|
||||||
|
manager.scheduleGameplayIntroPredownload(); tick(); tick();
|
||||||
|
state.requests[0].fail(new Error('offline')); await flush();
|
||||||
|
assert.equal(state.schedules[0].delay, 10);
|
||||||
|
state.blocksBusy = true; tick(); tick();
|
||||||
|
assert.equal(state.requests.length, 1);
|
||||||
|
state.blocksBusy = false; tick(); tick();
|
||||||
|
assert.equal(state.requests.length, 2);
|
||||||
|
state.destroyed = true; state.requests[1].fail(new Error('offline')); await flush();
|
||||||
|
assert.equal(state.schedules.length, 0);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('departing before idle never starts a download; old SDK does not execute a subpackage', () => {
|
||||||
|
const f = fixture(); f.manager.scheduleGameplayIntroPredownload();
|
||||||
|
f.state.destroyed = true; f.tick(); assert.equal(f.state.requests.length, 0);
|
||||||
|
const old = fixture(); delete old.wx.preDownloadSubpackage;
|
||||||
|
old.wx.loadSubpackage = () => assert.fail('must not execute scripts as background predownload');
|
||||||
|
old.manager.scheduleGameplayIntroPredownload(); assert.equal(old.state.schedules.length, 0);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('concurrent callers share one download, and cached Bundle needs no predownload', async () => {
|
||||||
|
const { tool, state, bundle } = fixture();
|
||||||
|
const first = tool.predownloadGameplayIntro();
|
||||||
|
assert.equal(tool.predownloadGameplayIntro(), first);
|
||||||
|
state.requests[0].success(); assert.equal(await first, true);
|
||||||
|
const second = fixture(); second.bundle('gameplay_intro');
|
||||||
|
assert.equal(await second.tool.predownloadGameplayIntro(), true);
|
||||||
|
assert.equal(second.state.requests.length, 0);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('scene reload releases intro resources while retaining ordinary popup bundles', () => {
|
||||||
|
const { state, manager, bundle } = fixture();
|
||||||
|
for (const [name, bundleName] of [['gameplayIntro', 'gameplay_intro'], ['pause', 'pause']]) {
|
||||||
|
const root = { active: true, removeFromParent() {}, destroy() { this.destroyed = true; } };
|
||||||
|
manager.dynamicPopupStates[name] = { bundleName, bundle: bundle(bundleName), root, callbacks: [], requestId: 1 };
|
||||||
|
}
|
||||||
|
manager.onDestroy();
|
||||||
|
assert.deepEqual(state.released, ['gameplay_intro']);
|
||||||
|
assert.equal(state.bundles.has('pause'), true);
|
||||||
|
assert.equal(state.bundles.has('gameplay_intro'), false);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('unowned intro bundle is cleaned even with retention enabled and no popup state', () => {
|
||||||
|
const { state, manager, bundle } = fixture(); bundle('gameplay_intro'); manager.onDestroy();
|
||||||
|
assert.deepEqual(state.released, ['gameplay_intro']);
|
||||||
|
});
|
||||||
|
|
||||||
|
for (const owned of [false, true]) {
|
||||||
|
test(`late old-scene intro callback respects actual new-scene ownership: ${owned}`, () => {
|
||||||
|
const { state, manager, Manager, bundle } = fixture(); const intro = bundle('gameplay_intro');
|
||||||
|
if (owned) manager.dynamicPopupStates.gameplayIntro = { bundleName: 'gameplay_intro', loading: true };
|
||||||
|
Manager.releaseOrphanedPopupBundle('gameplay_intro', intro); state.timers.shift()();
|
||||||
|
assert.equal(state.bundles.has('gameplay_intro'), owned);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
for (const mode of ['skip', 'cached', 'load', 'failure']) {
|
||||||
|
test(`low-memory warmup signals completion so idle download can start: ${mode}`, () => {
|
||||||
|
const { cc, tool, manager, bundle } = fixture(); let calls = 0, done;
|
||||||
|
tool.isIOSDevice = () => mode !== 'skip';
|
||||||
|
if (mode === 'cached') bundle('pause');
|
||||||
|
cc.assetManager.loadBundle = (name, callback) => { assert.equal(name, 'pause'); done = callback; };
|
||||||
|
manager.warmupGameplayPopupBundles(false, null, error => {
|
||||||
|
assert.equal(!!error, mode === 'failure'); calls++;
|
||||||
|
});
|
||||||
|
if (done) { assert.equal(calls, 0); done(mode === 'failure' ? new Error('offline') : null, {}); }
|
||||||
|
assert.equal(calls, 1);
|
||||||
|
});
|
||||||
|
}
|
||||||
Loading…
Reference in New Issue
Block a user