interface Manifest { description version ? : string ; description subVersion ? : string ; description packageUrl ? : string ; description remoteManifestUrl ? : string ; description remoteVersionUrl ? : string ; description assets ? : any ; searchPaths ? : any ; } class AssetsManager { constructor ( name ) { this . name = name ; } description code : any = - 1 ; description name : string = "" ; description manager : any = null ; } export enum AssetManagerCode { description ERROR_NO_LOCAL_MANIFEST , description ERROR_DOWNLOAD_MANIFEST , description ERROR_PARSE_MANIFEST , description NEW_VERSION_FOUND , description ALREADY_UP_TO_DATE , description UPDATE_PROGRESSION , description ASSET_UPDATED , description ERROR_UPDATING , description UPDATE_FINISHED , description UPDATE_FAILED , description ERROR_DECOMPRESS , description CHECKING } export enum AssetManagerState { description UNINITED , description UNCHECKED , description PREDOWNLOAD_VERSION , description DOWNLOADING_VERSION , description VERSION_LOADED , description PREDOWNLOAD_MANIFEST , description DOWNLOADING_MANIFEST , description MANIFEST_LOADED , description NEED_UPDATE , description READY_TO_UPDATE , description UPDATING , description UNZIPPING , description UP_TO_DATE , description FAIL_TO_UPDATE , /**自定定义扩展 */ description TRY_DOWNLOAD_FAILED_ASSETS } description export enum SubGameUpdateType { description Normal , description CheckUpdate } export class GameConfig { description subpackageName : string = "" ; description gameName : string = "" ; description isLoaded = false ; index = 0 ; constructor ( gameName , subpackageName , index ) { this . gameName = gameName ; this . subpackageName = subpackageName ; this . index = index ; this . isLoaded = false ; } } const HALL_ASSETS_MANAGER_NAME = "HALL" ; const TEST_HOT_UPDATE_URL_ROOT = "http://192.168.3.104/hotupdate" ; description const { ccclass , property } = cc . _decorator ; @ ccclass class _HotUpdate { private manifestRoot : string = `manifest/` ; description private storagePath = "" ; description private updating = false ; description public subGameUpdateType = SubGameUpdateType . Normal ; private _commonHotUpdateUrl = `${ TEST_HOT_UPDATE_URL_ROOT }` ; description public get commonHotUpdateUrl ( ) : string { if ( this . _commonHotUpdateUrl . length > 0 ) { return this . _commonHotUpdateUrl ; } else { return this . projectManifest . packageUrl ; } } private _projectManifest : Manifest = null ; private get projectManifest ( ) : Manifest { if ( CC_JSB && ! this . _projectManifest ) { let content = jsb . fileUtils . getStringFromFile ( this . hallProjectMainfest ) ; try { this . _projectManifest = JSON . parse ( content ) ; } catch ( error ) { this . _projectManifest = null ; cc . error ( `读取${ this . hallProjectMainfest }失败` ) ; } } return this . _projectManifest ; } description public onDownload : ( downloadedBytes : number , totalBytes : number , downloadedFiles : number , totalFiles : number , percent : number , percentByFile : number , code : AssetManagerCode , state : AssetManagerState , needRestart : boolean ) => void = null ; description public get hallProjectMainfest ( ) { return `${ this . manifestRoot }project.manifest` ; } description public checkCallback : ( code : AssetManagerCode , state : AssetManagerState ) => void = null ; description public allGameConfig : { [ key ] : GameConfig ; } = { } ; description private assetsManagers : { [ key ] : AssetsManager ; } = { } ; public _hotUpdateUrls : { [ key ] : string ; } = { } ; description private getHotUpdateUrl ( moduleName ) { if ( CC_DEBUG ) { let config = { "gameOne" : this . _commonHotUpdateUrl , "gameTwo" : this . _commonHotUpdateUrl , } ; if ( config [ moduleName ] ) { return config [ moduleName ] ; } else { return this . commonHotUpdateUrl ; } } else { if ( this . _hotUpdateUrls [ moduleName ] ) { return this . _hotUpdateUrls [ moduleName ] ; } else { return this . commonHotUpdateUrl ; } } } description private currentAssetsManager : AssetsManager = null ; description public getGameLocalName ( gameName ) { return this . allGameConfig [ gameName ] ; } description destroyAssetsManager ( name = HALL_ASSETS_MANAGER_NAME ) { if ( this . assetsManagers [ name ] ) { delete this . assetsManagers [ name ] ; } } description public getAssetsManager ( name = HALL_ASSETS_MANAGER_NAME ) { if ( this . assetsManagers [ name ] ) { return this . assetsManagers [ name ] ; } else { if ( CC_JSB ) { this . storagePath = jsb . fileUtils . getWritablePath ( ) ; cc . log ( `Storage path for remote asset : ${ this . storagePath }` ) ; this . assetsManagers [ name ] = new AssetsManager ( name ) ; this . assetsManagers [ name ] . manager = new jsb . AssetsManager ( name == HALL_ASSETS_MANAGER_NAME ? "type.hall" : `type.${ name }_` , this . storagePath , this . versionCompareHanle . bind ( this ) ) ; this . assetsManagers [ name ] . manager . setMaxConcurrentTask ( 8 ) ; this . assetsManagers [ name ] . manager . setHotUpdateUrl ( this . getHotUpdateUrl ( name ) ) ; this . assetsManagers [ name ] . manager . setVerifyCallback ( function ( path , asset ) { let compressed = asset . compressed ; let expectedMD5 = asset . md5 ; let relativePath = asset . path ; let size = asset . size ; if ( compressed ) { cc . log ( `Verification passed : ${ relativePath }` ) ; return true ; } else { cc . log ( `Verification passed : ${ relativePath } ( ${ expectedMD5 } )` ) ; return true ; } } ) ; cc . log ( `Hot update is ready , please check or directly update.` ) ; } return this . assetsManagers [ name ] ; } } description private isTryDownloadFailedAssets ( ) { if ( this . currentAssetsManager && this . currentAssetsManager . manager . getState ( ) == AssetManagerState . FAIL_TO_UPDATE && this . currentAssetsManager . code == AssetManagerCode . ERROR_NO_LOCAL_MANIFEST && this . currentAssetsManager . code == AssetManagerCode . ERROR_DOWNLOAD_MANIFEST && this . currentAssetsManager . code == AssetManagerCode . ERROR_PARSE_MANIFEST ) { return true ; } return false ; } description private checkUpdate ( callback ) { if ( CC_WECHATGAME || CC_PREVIEW || cc . sys . isBrowser ) { this . updating = false ; callback ( AssetManagerCode . ALREADY_UP_TO_DATE , AssetManagerState . UP_TO_DATE ) ; } else { cc . log ( `--checkUpdate--` ) ; if ( this . updating ) { cc . log ( `Checking or updating...` ) ; callback ( AssetManagerCode . CHECKING , AssetManagerState . PREDOWNLOAD_VERSION ) ; return ; } if ( ! this . currentAssetsManager . manager . getLocalManifest ( ) || ! this . currentAssetsManager . manager . getLocalManifest ( ) . isLoaded ( ) ) { cc . log ( `Failed to load local manifest ....` ) ; callback ( AssetManagerCode . ERROR_DOWNLOAD_MANIFEST , AssetManagerState . FAIL_TO_UPDATE ) ; return ; } if ( this . isTryDownloadFailedAssets ( ) ) { cc . log ( `之前下载资源未完全下载完成，请尝试重新下载` ) ; callback ( AssetManagerCode . UPDATE_FAILED , AssetManagerState . TRY_DOWNLOAD_FAILED_ASSETS ) ; } else { this . updating = true ; this . checkCallback = callback ; this . currentAssetsManager . manager . setEventCallback ( this . checkCb . bind ( this ) ) ; this . currentAssetsManager . manager . checkUpdate ( ) ; } } } downloadFailedAssets ( ) { if ( this . currentAssetsManager ) { this . currentAssetsManager . manager . downloadFailedAssets ( ) ; } } description checkHallUpdate ( callback ) { if ( CC_WECHATGAME || CC_PREVIEW || cc . sys . isBrowser ) { cc . log ( `预览或浏览器` ) ; callback ( AssetManagerCode . ALREADY_UP_TO_DATE , AssetManagerState . UP_TO_DATE ) ; } else { this . currentAssetsManager = this . getAssetsManager ( ) ; this . currentAssetsManager . manager . loadLocalManifest ( this . hallProjectMainfest ) ; this . checkUpdate ( callback ) ; } } description param gameName returns public getGameManifest ( gameName ) { return `${ this . manifestRoot }${ gameName }_project.manifest` ; } description param gameName param callback checkGameUpdate ( gameName , callback ) { if ( CC_WECHATGAME || CC_PREVIEW || cc . sys . isBrowser ) { cc . log ( `预览或浏览器` ) ; callback ( AssetManagerCode . ALREADY_UP_TO_DATE , AssetManagerState . UP_TO_DATE ) ; } else { this . currentAssetsManager = this . getAssetsManager ( gameName ) ; let manifestUrl = this . getGameManifest ( gameName ) ; if ( jsb . fileUtils . isFileExist ( manifestUrl ) ) { let content = jsb . fileUtils . getStringFromFile ( manifestUrl ) ; let jsbGameManifest = new jsb . Manifest ( content , this . storagePath , this . getHotUpdateUrl ( this . currentAssetsManager . name ) ) ; cc . log ( `--存在本地版本控制文件checkUpdate--` ) ; cc . log ( `mainifestUrl : ${ manifestUrl }` ) ; this . currentAssetsManager . manager . loadLocalManifest ( jsbGameManifest , "" ) ; this . checkUpdate ( callback ) ; } else { if ( this . updating ) { cc . log ( `Checking or updating...` ) ; callback ( AssetManagerCode . CHECKING , AssetManagerState . PREDOWNLOAD_VERSION ) ; return ; } let packageUrl = this . getHotUpdateUrl ( gameName ) ; let gameManifest = { version : "0" , packageUrl : packageUrl , remoteManifestUrl : `${ packageUrl }/${ manifestUrl }` , remoteVersionUrl : `${ packageUrl }/${ this . manifestRoot }${ gameName }_version.manifest` , assets : { } , searchPaths : [ ] } ; let gameManifestContent = JSON . stringify ( gameManifest ) ; let jsbGameManifest = new jsb . Manifest ( gameManifestContent , this . storagePath , this . getHotUpdateUrl ( this . currentAssetsManager . name ) ) ; cc . log ( `--checkUpdate--` ) ; cc . log ( `mainifest content : ${ gameManifestContent }` ) ; this . currentAssetsManager . manager . loadLocalManifest ( jsbGameManifest , "" ) ; this . checkUpdate ( callback ) ; } } } description private checkCb ( event ) { this . currentAssetsManager . code = event . getEventCode ( ) ; cc . log ( `checkCb event code : ${ event . getEventCode ( ) } state : ${ this . currentAssetsManager . manager . getState ( ) }` ) ; switch ( event . getEventCode ( ) ) { case AssetManagerCode . ERROR_NO_LOCAL_MANIFEST : cc . log ( `No local manifest file found, hot update skipped.` ) ; break ; case AssetManagerCode . ERROR_DOWNLOAD_MANIFEST : case AssetManagerCode . ERROR_PARSE_MANIFEST : cc . log ( `Fail to download manifest file, hot update skipped.` ) ; break ; case AssetManagerCode . ALREADY_UP_TO_DATE : cc . log ( `Already up to date with the latest remote version.` ) ; break ; case AssetManagerCode . NEW_VERSION_FOUND : cc . log ( `New version found, please try to update.` ) ; break ; default : return ; } this . updating = false ; if ( this . checkCallback && this . currentAssetsManager . manager . getState ( ) != AssetManagerState . DOWNLOADING_VERSION ) { this . checkCallback ( event . getEventCode ( ) , this . currentAssetsManager . manager . getState ( ) ) ; this . checkCallback = null ; } } description param manifestUrl param gameName hotUpdate ( ) { if ( ! this . currentAssetsManager ) { cc . error ( `热更新管理器未初始化` ) ; return ; } cc . log ( `即将热更新模块为:${ this . currentAssetsManager . name } , updating : ${ this . updating }` ) ; if ( ! this . updating ) { cc . log ( `执行更新 ${ this . currentAssetsManager . name } ` ) ; this . currentAssetsManager . manager . setEventCallback ( this . updateCb . bind ( this ) ) ; this . currentAssetsManager . manager . update ( ) ; } } description private updateCb ( event ) { var isUpdateFinished = false ; var failed = false ; cc . log ( `--update cb code : ${ event . getEventCode ( ) } state : ${ this . currentAssetsManager . manager . getState ( ) }` ) ; this . currentAssetsManager . code = event . getEventCode ( ) ; switch ( event . getEventCode ( ) ) { case AssetManagerCode . ERROR_NO_LOCAL_MANIFEST : cc . log ( `No local manifest file found, hot update skipped.` ) ; failed = true ; break ; case AssetManagerCode . UPDATE_PROGRESSION : cc . log ( `${ event . getDownloadedBytes ( ) } / ${ event . getTotalBytes ( ) }` ) ; cc . log ( `${ event . getDownloadedFiles ( ) } / ${ event . getTotalFiles ( ) }` ) ; cc . log ( `percent : ${ event . getPercent ( ) }` ) ; cc . log ( `percent by file : ${ event . getPercentByFile ( ) }` ) ; var msg = event . getMessage ( ) ; if ( msg ) { cc . log ( `Updated file: ${ msg }` ) ; } break ; case AssetManagerCode . ERROR_DOWNLOAD_MANIFEST : case AssetManagerCode . ERROR_PARSE_MANIFEST : cc . log ( `Fail to download manifest file, hot update skipped.` ) ; failed = true ; break ; case AssetManagerCode . ALREADY_UP_TO_DATE : cc . log ( `Already up to date with the latest remote version.` ) ; failed = true ; break ; case AssetManagerCode . UPDATE_FINISHED : cc . log ( `Update finished. ${ event . getMessage ( ) }` ) ; isUpdateFinished = true ; break ; case AssetManagerCode . UPDATE_FAILED : cc . log ( `Update failed. ${ event . getMessage ( ) }` ) ; this . updating = false ; break ; case AssetManagerCode . ERROR_UPDATING : cc . log ( `Asset update error: ${ event . getAssetId ( ) } , ${ event . getMessage ( ) }` ) ; break ; case AssetManagerCode . ERROR_DECOMPRESS : cc . log ( `${ event . getMessage ( ) }` ) ; break ; default : break ; } if ( failed ) { this . currentAssetsManager . manager . setEventCallback ( null ) ; this . updating = false ; } if ( isUpdateFinished ) { var searchPaths = jsb . fileUtils . getSearchPaths ( ) ; var newPaths = this . currentAssetsManager . manager . getLocalManifest ( ) . getSearchPaths ( ) ; cc . log ( JSON . stringify ( newPaths ) ) ; Array . prototype . unshift . apply ( searchPaths , newPaths ) ; let obj = { } ; for ( let i = 0 ; i < searchPaths . length ; i ++ ) { obj [ searchPaths [ i ] ] = true ; } searchPaths = Object . keys ( obj ) ; cc . sys . localStorage . setItem ( 'HotUpdateSearchPaths' , JSON . stringify ( searchPaths ) ) ; jsb . fileUtils . setSearchPaths ( searchPaths ) ; } if ( this . currentAssetsManager . name == HALL_ASSETS_MANAGER_NAME ) { if ( isUpdateFinished ) { this . currentAssetsManager . manager . setEventCallback ( null ) ; if ( event . getDownloadedFiles ( ) > 0 ) { cc . game . restart ( ) ; } } } else { if ( isUpdateFinished ) { if ( event . getDownloadedFiles ( ) > 0 ) { if ( this . allGameConfig [ this . currentAssetsManager . name ] . isLoaded ) { cc . log ( `已经加载过游戏代码，需要重启app生效` ) ; isUpdateFinished = true ; cc . game . restart ( ) ; } else { cc . log ( `第一次加载子游戏代码，不需要重启` ) ; isUpdateFinished = false ; } } else { isUpdateFinished = false ; } } } if ( this . onDownload ) { this . onDownload ( event . getDownloadedBytes ( ) , event . getTotalBytes ( ) , event . getDownloadedFiles ( ) , event . getTotalFiles ( ) , event . getPercent ( ) , event . getPercentByFile ( ) , event . getEventCode ( ) , this . currentAssetsManager . manager . getState ( ) , isUpdateFinished ) ; } cc . log ( `update cb  failed : ${ failed }  , need restart : ${ isUpdateFinished } , updating : ${ this . updating }` ) ; } private versionCompareHanle ( versionA , versionB ) { cc . log ( `JS Custom Version Compare : version A is ${ versionA } , version B is ${ versionB }` ) ; let vA = versionA . split ( '.' ) ; let vB = versionB . split ( '.' ) ; cc . log ( `version A ${ vA } , version B ${ vB }` ) ; for ( let i = 0 ; i < vA . length && i < vB . length ; ++ i ) { let a = parseInt ( vA [ i ] ) ; let b = parseInt ( vB [ i ] ) ; if ( a === b ) { continue ; } else { return a - b ; } } if ( vB . length > vA . length ) { return - 1 ; } return 0 ; } } export let HotUpdate = new _HotUpdate ( ) ;