import { _decorator, Component, Node, director, sys, Asset, game, instantiate, AssetManager, assetManager } from ‘cc’;
import { ResourceLoader } from ‘./ResourceLoader’;
import { Logger } from ‘./Logger’;
import { HotUpdatePage } from ‘./HotUpdatePage’;
const { ccclass, property } = _decorator;
@ccclass(‘LaunchLoading’)
export class LaunchLoading extends Component {
@property(Node)
public popRoot: Node = null!;
@property(Asset)
public manifestUrl: Asset = null!;
private _updatePanel: HotUpdatePage | null = null;
private jsb: any = null;
private _am: any = null;
private _storagePath = '';
private _updating: boolean = false;
private _canRetry: boolean = false;
// 新增:缓存热更新信息,用于弹窗展示
private _updateTotalFiles: number = 0; // 待更新总文件数
private _updateTotalBytes: number = 0; // 待更新总字节数
private _newVersion: string = ""; // 远端新版本号
onLoad() {
Logger.log("【入口】LaunchLoading onLoad执行");
this.initHotUpdateEnv();
}
private initHotUpdateEnv() {
// ==========新增平台调试打印==========
Logger.log("【平台检测】sys.isNative =", sys.isNative);
Logger.log("【平台检测】sys.platform =", sys.platform);
Logger.log("【平台检测】原始window.jsb是否存在 =", !!(window as any).jsb);
this.jsb = (window as any).jsb;
Logger.log("【JSB赋值后】this.jsb是否有效 =", !!this.jsb);
//============================一直都是旧包除非重启所以增加打印=======================================
if (this.jsb) {
const paths = this.jsb.fileUtils.getSearchPaths();
Logger.log("【路径-启动初始化】当前fileUtils搜索路径:", JSON.stringify(paths));
}
//==============================================================
// 拆分判断,精准打印是哪个条件触发跳过
if (!this.jsb) {
Logger.error("【跳过原因】window.jsb不存在,非原生环境,跳过热更");
this.enterGameDirect();
return;
}
if (!sys.isNative) {
Logger.error("【跳过原因】sys.isNative=false,非原生环境,跳过热更");
this.enterGameDirect();
return;
}
// ======================================
this._storagePath = this.jsb.fileUtils.getWritablePath() + "dasi_hotupdate/";
Logger.log("【路径】热更本地缓存路径:", this._storagePath);
const versionCompare = (vA: string, vB: string) => {
Logger.log("【版本对比】本地ver:", vA, "远端ver:", vB);
const arrA = vA.split(".");
const arrB = vB.split(".");
for (let i = 0; i < arrA.length; i++) {
const numA = Number(arrA[i]);
const numB = Number(arrB[i] || "0");
if (numA !== numB) return numA - numB;
}
return arrB.length > arrA.length ? -1 : 0;
};
const localManifestPath = this.manifestUrl.nativeUrl;
Logger.log("【AM初始化】传入本地manifest物理路径:", localManifestPath);
this._am = new this.jsb.AssetsManager(localManifestPath, this._storagePath, versionCompare);
// this._am = new this.jsb.AssetsManager("", this._storagePath, versionCompare);
Logger.log("【AM初始化】AssetsManager实例创建完成");
this._am.setVerifyCallback((path: string, assetInfo: any) => {
const isZip = assetInfo.compressed;
const md5 = assetInfo.md5;
const relPath = assetInfo.path;
if (isZip) {
Logger.log("【校验】压缩包校验通过:", relPath);
return true;
} else {
Logger.log("【校验】文件校验通过:", relPath, md5);
return true;
}
});
Logger.log("【流程】准备进入checkVersion检测版本");
this.checkVersion();
}
/** 让 FileUtils 立刻感知热更目录(幂等操作,调多次不伤) */
private applyHotUpdateSearchPaths(): void {
if (!this._am) return;
const manifest = this._am.getLocalManifest();
if (!manifest || !manifest.isLoaded()) return;
const hotPaths = manifest.getSearchPaths(); // ← 这就是热更目录数组,例如 [".../dasi_hotupdate/"]
if (!hotPaths || hotPaths.length === 0) return;
const sysPaths = this.jsb.fileUtils.getSearchPaths();
// 去重:避免多次 unshift 导致同一路径出现 N 次
const already = hotPaths.every(p => sysPaths.includes(p));
if (!already) {
Array.prototype.unshift.apply(sysPaths, hotPaths);
}
this.jsb.fileUtils.setSearchPaths(sysPaths);
sys.localStorage.setItem('HotUpdateSearchPaths', JSON.stringify(sysPaths));
Logger.log('【SearchPaths】已应用热更搜索路径:', sysPaths);
}
private checkVersion() {
Logger.log("【检测】进入checkVersion,_updating=", this._updating);
if (this._updating) {
Logger.log("【检测】正在检测/更新中,请勿重复触发");
return;
}
const amState = this._am.getState();
Logger.log("【AM状态】当前AssetsManager状态码:", amState, "UNINITED=", this.jsb.AssetsManager.State.UNINITED);
const localManifest = this._am.getLocalManifest();
Logger.log("【本地清单】getLocalManifest获取对象是否存在:", !!localManifest);
let manifestLoaded = false;
let localVer = "";
if (localManifest) {
manifestLoaded = localManifest.isLoaded();
localVer = localManifest.getVersion(); // 官方唯一合法读取接口
Logger.log("【本地清单】isLoaded=", manifestLoaded, "本地版本号=", localVer);
}
if (!localManifest || !manifestLoaded) {
Logger.error("【致命】本地manifest加载失败,直接跳过热更进游戏");
this.enterGameDirect();
return;
}
Logger.log("【检测】绑定更新回调,发起checkUpdate请求远端");
this._am.setEventCallback(this.checkCb.bind(this));
this._am.checkUpdate();
this._updating = true;
Logger.log("【检测】已上锁_updating=true");
}
private checkCb(event: any) {
const code = event.getEventCode();
const errMsg = event.getMessage ? event.getMessage() : "无错误详情";
Logger.log("【检测回调】事件code=", code, "原生错误msg:", errMsg);
let isFinalEvent = false;
let needEnterGame = true;
switch (code) {
case this.jsb.EventAssetsManager.ERROR_NO_LOCAL_MANIFEST:
Logger.error("【异常】ERROR_NO_LOCAL_MANIFEST:未找到本地manifest,跳过热更", errMsg);
isFinalEvent = true;
break;
case this.jsb.EventAssetsManager.ERROR_DOWNLOAD_MANIFEST:
Logger.error("【异常】ERROR_DOWNLOAD_MANIFEST:远程manifest下载失败", errMsg);
isFinalEvent = true;
break;
case this.jsb.EventAssetsManager.ERROR_PARSE_MANIFEST:
Logger.error("【异常】ERROR_PARSE_MANIFEST:远程manifest解析失败【核心报错】", errMsg);
isFinalEvent = true;
break;
case this.jsb.EventAssetsManager.ALREADY_UP_TO_DATE:
Logger.log("【版本】已是最新版本,直接进游戏");
isFinalEvent = true;
this.applyHotUpdateSearchPaths();
break;
case this.jsb.EventAssetsManager.NEW_VERSION_FOUND:
Logger.log("【版本】检测到新版本,准备加载更新弹窗");
const self = this;
// 全部逻辑抛到主线程执行
// 主线程:通过 AssetsManager 实例读取更新数据(原生环境安全)
const remoteManifest = self._am.getRemoteManifest();
const localManifest = self._am.getLocalManifest();
// 赋值 + 容错兜底(防止空对象报错)
self._updateTotalFiles = self._am.getTotalFiles() || 0;
self._updateTotalBytes = self._am.getTotalBytes() || 0;
self._newVersion = remoteManifest ? remoteManifest.getVersion() : "";
// 主线程打印日志
Logger.log(`【更新信息】待更新文件数:${self._updateTotalFiles}`);
Logger.log(`【更新信息】原始总字节:${self._updateTotalBytes} B`);
Logger.log(`【更新信息】远端版本:${self._newVersion}`);
// 加载更新弹窗
self.loadUpdatePopup();
isFinalEvent = true;
needEnterGame = false; // 找到新版本,不自动进游戏
break;
case this.jsb.EventAssetsManager.UPDATE_PROGRESSION:
Logger.log("【检测-下载进度】正在拉取远端version.manifest");
// 进度事件:非结束事件,不做收尾逻辑
return;
default:
Logger.log("【检测】未知事件码:", code);
isFinalEvent = true;
break;
}
// 仅最终事件才解绑回调、解锁
if (isFinalEvent) {
this._am.setEventCallback(null!);
this._updating = false;
Logger.log("【检测回调】清空回调,解锁_updating=false");
if (needEnterGame) {
Logger.log("【流程】无新版本/异常,进入enterGameDirect加载Bundle");
this.enterGameDirect();
}
}
}
/**
* 字节格式化:B → KB / MB
* @param bytes 原始字节数
* @returns 格式化后带单位的字符串
*/
private formatBytes(bytes: number): string {
if (bytes < 1024) {
return `${bytes} B`;
} else if (bytes < 1024 * 1024) {
const kb = (bytes / 1024).toFixed(1);
return `${kb} KB`;
} else {
const mb = (bytes / (1024 * 1024)).toFixed(1);
return `${mb} MB`;
}
}
private startUpdate() {
if (this._updating) return;
this._updating = true;
this._canRetry = false;
this._am.setEventCallback(this.updateCb.bind(this));
Logger.log("【开始下载】调用am.update()");
this._am.update();
}
private updateCb(event: any) {
const code = event.getEventCode();
let needRestart = false;
let updateFailed = false;
const msg = event.getMessage ? event.getMessage() : '';
switch (code) {
case this.jsb.EventAssetsManager.UPDATE_PROGRESSION:
const rawPercent = event.getPercent();
const currFile = event.getDownloadedFiles();
const totalFile = event.getTotalFiles();
// 容错:过滤首帧 NaN,无有效进度时显示 0
let showPercent = 0;
if (!isNaN(rawPercent)) {
showPercent = Math.floor(rawPercent * 100); // 转为 0~100 整数百分比
}
// 打印日志(不再出现 NaN)
Logger.log(`【下载进度】文件${currFile}/${totalFile} 进度 ${showPercent}%`);
if (this._updatePanel) {
this._updatePanel.setByteProgress(event.getDownloadedBytes(), event.getTotalBytes());
this._updatePanel.setFileProgress(currFile, totalFile);
this._updatePanel.setInfo(`正在下载:${msg}`);
}
break;
case this.jsb.EventAssetsManager.ERROR_NO_LOCAL_MANIFEST:
case this.jsb.EventAssetsManager.ERROR_DOWNLOAD_MANIFEST:
case this.jsb.EventAssetsManager.ERROR_PARSE_MANIFEST:
Logger.error("【更新阶段】清单异常终止更新:", msg);
updateFailed = true;
if (this._updatePanel) this._updatePanel.setInfo("更新配置文件拉取失败,跳过更新");
break;
case this.jsb.EventAssetsManager.UPDATE_FINISHED:
this.applyHotUpdateSearchPaths();
Logger.log("【更新完成】热更全部下载完毕,准备重启");
needRestart = true;
if (this._updatePanel) this._updatePanel.setInfo("更新完成,即将重启游戏");
break;
case this.jsb.EventAssetsManager.UPDATE_FAILED:
Logger.error("【更新失败】部分资源下载失败,可重试", msg);
this._canRetry = true;
this._updating = false;
if (this._updatePanel) {
this._updatePanel.showRetryBtn(this.retryUpdate);
this._updatePanel.setInfo("部分资源下载失败,请点击重试");
}
break;
case this.jsb.EventAssetsManager.ERROR_UPDATING:
Logger.error("【资源异常】", event.getAssetId(), msg);
if (this._updatePanel) {
this._updatePanel.setInfo(`资源异常:${event.getAssetId()}`);
this._updatePanel.showRetryBtn(this.retryUpdate);
}
break;
case this.jsb.EventAssetsManager.ERROR_DECOMPRESS:
Logger.error("【解压失败】", msg);
if (this._updatePanel) {
this._updatePanel.setInfo("压缩包解压失败,可重试");
this._updatePanel.showRetryBtn(this.retryUpdate);
}
break;
case this.jsb.EventAssetsManager.ALREADY_UP_TO_DATE:
Logger.log("【更新】已是最新资源");
updateFailed = true;
if (this._updatePanel) this._updatePanel.setInfo("当前已是最新版本");
break;
}
if (updateFailed) {
this._am.setEventCallback(null!);
this._updating = false;
Logger.log("【更新失败分支】跳转enterGameDirect");
this.enterGameDirect();
}
if (needRestart) {
this._am.setEventCallback(null!);
this.applyHotUpdateSearchPaths(); // ← 替换掉原来那段手写的
// ============================新增日志
const pathsBeforeRestart = this.jsb.fileUtils.getSearchPaths();
Logger.log("【路径-更新完成、重启前】", JSON.stringify(pathsBeforeRestart));
//===================================================================
Logger.log("【重启】热更路径写入完成,延时重启");
setTimeout(() => {
game.restart();
Logger.log("【重启】调用重启成功?");
}, 800);
}
}
public retryUpdate() {
if (!this._updating && this._canRetry) {
this._canRetry = false;
this._updating = true;
this._am.setEventCallback(this.updateCb.bind(this));
Logger.log("【重试】开始下载失败资源");
this._am.downloadFailedAssets();
}
}
private async loadUpdatePopup() {
Logger.log("【弹窗】开始从common加载热更弹窗");
try {
const popPrefab = await ResourceLoader.Instance.loadPrefabFromCommon("ui/HotUpdatePage");
const popNode = instantiate(popPrefab);
this._updatePanel = popNode.getComponent(HotUpdatePage);
popNode.setParent(this.popRoot);
Logger.log("【弹窗】正常加载common热更弹窗");
// ========= 新增:向主弹窗传递更新数据 =========
if (this._updatePanel) {
const sizeStr = this.formatBytes(this._updateTotalBytes);
// 调用弹窗方法 / 直接赋值属性(二选一,推荐用方法)
this._updatePanel.setUpdateInfo(
this._newVersion,
this._updateTotalFiles,
sizeStr
);
this._updatePanel.setByteProgress(0,this._updateTotalBytes);
this._updatePanel.setFileProgress(0, this._updateTotalFiles);
this._updatePanel.showSureBtn(this.startUpdate.bind(this));
Logger.log(">>>>>>>>>>>>>显示确定弹窗");
} else {
Logger.log(">>>>>>>>>>>>>>>>>hotupdate 脚本为空");
}
} catch (err) {
Logger.error("【弹窗异常】common弹窗加载失败,启用本地兜底弹窗", err);
const backupPrefab = await ResourceLoader.loadPrefab("ui/HotUpdateBackupPage");
if (backupPrefab) {
const popNode = instantiate(backupPrefab);
this._updatePanel = popNode.getComponent(HotUpdatePage);
popNode.setParent(this.popRoot);
Logger.log("【弹窗】兜底弹窗加载成功");
// ========= 新增:向兜底弹窗也传递更新数据 =========
if (this._updatePanel) {
const sizeStr = this.formatBytes(this._updateTotalBytes);
this._updatePanel.setUpdateInfo(
this._newVersion,
this._updateTotalFiles,
sizeStr
);
this._updatePanel.setByteProgress(0,this._updateTotalBytes);
this._updatePanel.setFileProgress(0, this._updateTotalFiles);
this._updatePanel.showSureBtn(this.startUpdate.bind(this));
Logger.log(">>>>>>>>>>>>>显示确定弹窗");
} else {
Logger.log(">>>>>>>>>>>>>>>>>hotupdate 脚本为空");
}
} else {
Logger.error("【弹窗】兜底弹窗也加载失败");
}
}
}
private async enterGameDirect() {
// 路径日志(上一层已加,保留)
if (this.jsb) {
const finalPaths = this.jsb.fileUtils.getSearchPaths();
Logger.log("【路径-加载common前最终快照】", JSON.stringify(finalPaths));
}
// ========== 新增:检查常驻Bundle缓存状态 ==========
const hasCommonCache = ResourceLoader.Instance.checkResidentBundle("common");
Logger.log("【Bundle缓存-加载前】common是否存在内存缓存:", hasCommonCache);
// ==============================================
Logger.log(">>>>>>>>>>>>>>>>>>>>>>先加载common")
await ResourceLoader.Instance.loadResidentBundle("common");
// ========== 新增:加载后再打印 ==========
Logger.log("【Bundle缓存-加载后】common加载完成");
// ==============================================
Logger.log(">>>>>>>>>>>>>>>>>>>>>>common加载完毕,再加载remote_scripts")
await ResourceLoader.Instance.loadResidentBundle("remote_scripts");
Logger.log(">>>>>>>>>>>>>>>>>>>>>>两个Bundle加载完成,跳转test场景")
director.loadScene("test");
}
}这是我的热更新脚本,我给豆老师看过了,说没有问题,没有问题就是最大的问题,我猜测还可能是我的resouceloade 脚本写错了,或者缓存问题?可是缓存在重启,或者杀死进程就应该没有了啊,为啥还是旧的呢,这很奇怪,这太奇怪了…