From 4d481f0379ef57235655a58a7b14bcf16d502cbf Mon Sep 17 00:00:00 2001
From: ALEXTANG <574809918@qq.com>
Date: Fri, 10 Jun 2022 17:40:02 +0800
Subject: [PATCH] Hot Update DownLoader
Hot Update DownLoader
---
.../Runtime/DownloadHandlerFileRange.cs | 179 +++++++++++
.../Runtime/DownloadHandlerFileRange.cs.meta | 11 +
.../Runtime/HotUpdate/Runtime/DownloadImpl.cs | 299 ++++++++++++++++++
.../HotUpdate/Runtime/DownloadImpl.cs.meta | 11 +
.../Runtime/HotUpdate/Runtime/LoadData.cs | 26 ++
.../HotUpdate/Runtime/LoadData.cs.meta | 11 +
.../Runtime/HotUpdate/Runtime/LoadMgr.cs | 98 ++++++
.../Runtime/HotUpdate/Runtime/LoadMgr.cs.meta | 11 +
.../HotUpdate/Runtime/LoaderUtilities.cs | 67 ++++
.../HotUpdate/Runtime/LoaderUtilities.cs.meta | 11 +
10 files changed, 724 insertions(+)
create mode 100644 Assets/TEngine/Runtime/HotUpdate/Runtime/DownloadHandlerFileRange.cs
create mode 100644 Assets/TEngine/Runtime/HotUpdate/Runtime/DownloadHandlerFileRange.cs.meta
create mode 100644 Assets/TEngine/Runtime/HotUpdate/Runtime/DownloadImpl.cs
create mode 100644 Assets/TEngine/Runtime/HotUpdate/Runtime/DownloadImpl.cs.meta
create mode 100644 Assets/TEngine/Runtime/HotUpdate/Runtime/LoadData.cs
create mode 100644 Assets/TEngine/Runtime/HotUpdate/Runtime/LoadData.cs.meta
create mode 100644 Assets/TEngine/Runtime/HotUpdate/Runtime/LoadMgr.cs
create mode 100644 Assets/TEngine/Runtime/HotUpdate/Runtime/LoadMgr.cs.meta
create mode 100644 Assets/TEngine/Runtime/HotUpdate/Runtime/LoaderUtilities.cs
create mode 100644 Assets/TEngine/Runtime/HotUpdate/Runtime/LoaderUtilities.cs.meta
diff --git a/Assets/TEngine/Runtime/HotUpdate/Runtime/DownloadHandlerFileRange.cs b/Assets/TEngine/Runtime/HotUpdate/Runtime/DownloadHandlerFileRange.cs
new file mode 100644
index 00000000..1107bba7
--- /dev/null
+++ b/Assets/TEngine/Runtime/HotUpdate/Runtime/DownloadHandlerFileRange.cs
@@ -0,0 +1,179 @@
+using System.IO;
+using UnityEngine.Networking;
+
+namespace TEngine
+{
+ public class DownloadHandlerFileRange : DownloadHandlerScript, IDownload
+ {
+ private readonly string _url;
+ private string _path;
+ private string _md5;
+ private FileStream _fileStream;
+ private UnityWebRequest _unityWebRequest;
+
+ private long _totalFileSize = 0;
+ private long _curFileSize = 0;
+ public bool HasError { get; private set; }
+ protected BackgroundDownloadStatus _status = BackgroundDownloadStatus.NotBegin;
+
+ private DownloadImpl _imp;
+
+ public void SetImp(DownloadImpl imp)
+ {
+ _imp = imp;
+ }
+
+ public DownloadHandlerFileRange(string url, string path, long totalLength, string md5) : base(new byte[1024 * 1024])
+ {
+ TLogger.LogInfo($"DownloadHandlerFileRange url:{url},path:{path},totalLength:{totalLength}");
+
+ _url = url;
+ _path = path;
+ _md5 = md5;
+
+ var dirPath = Path.GetDirectoryName(_path);
+ if (dirPath != null)
+ {
+ TLogger.LogInfo($"DownloadHandlerFileRange dirPath:{dirPath}");
+ if (!Directory.Exists(dirPath))
+ {
+ Directory.CreateDirectory(dirPath);
+ }
+ }
+
+ _totalFileSize = totalLength;
+ _status = BackgroundDownloadStatus.NotBegin;
+ }
+
+ ///
+ /// 兼容断点续传
+ ///
+ public void StartDownload()
+ {
+ if (_status != BackgroundDownloadStatus.NotBegin)
+ {
+ return;
+ }
+
+ var index = _path.IndexOf("_md5_");
+ var fileMd5 = string.Empty;
+ if (File.Exists(_path))
+ {
+ fileMd5 = LoaderUtilities.GetMd5Hash(_path);
+ }
+ else
+ {
+ if (index >= 0)
+ {
+ fileMd5 = LoaderUtilities.GetMd5Hash(_path.Substring(0, index));
+ }
+ }
+
+ if (fileMd5 == _md5)
+ {
+ _status = BackgroundDownloadStatus.Done;
+ return;
+ }
+
+ try
+ {
+ _fileStream = new FileStream(_path, FileMode.OpenOrCreate, FileAccess.Write);
+ var localFileSize = _fileStream.Length;
+ _fileStream.Seek(localFileSize, SeekOrigin.Begin);
+ _curFileSize = localFileSize;
+ _unityWebRequest = UnityWebRequest.Get(_url);
+ _unityWebRequest.SetRequestHeader("Range", "bytes=" + localFileSize + "-" + _totalFileSize);
+ _unityWebRequest.downloadHandler = this;
+ _unityWebRequest.SendWebRequest();
+
+ _status = BackgroundDownloadStatus.Downloading;
+ }
+ catch (System.Exception e)
+ {
+ TLogger.LogError($"DownloadHandlerFileRange.StartDownload,Exception,{e.StackTrace}");
+ _status = BackgroundDownloadStatus.Failed;
+ throw;
+ }
+ }
+
+ public new void Dispose()
+ {
+ if (_status == BackgroundDownloadStatus.Downloading)
+ {
+ _status = BackgroundDownloadStatus.Failed;
+ }
+
+ base.Dispose();
+
+ if (_fileStream != null)
+ {
+ _fileStream.Close();
+ _fileStream.Dispose();
+ _fileStream = null;
+ }
+
+ if (_unityWebRequest != null)
+ {
+ _unityWebRequest.Abort();
+ _unityWebRequest.Dispose();
+ _unityWebRequest = null;
+ }
+ }
+ #region
+ public float Progress => _totalFileSize == 0 ? 0 : ((float)_curFileSize) / _totalFileSize;
+
+ public long TotalSize => _totalFileSize;
+
+ public long CurrentSize => _curFileSize;
+
+ protected override bool ReceiveData(byte[] data, int dataLength)
+ {
+ if (data == null || dataLength <= 0)
+ {
+ return false;
+ }
+ _fileStream.Write(data, 0, dataLength);
+ _fileStream.Flush();
+ _curFileSize += dataLength;
+ LoadUpdateLogic.Instance.Down_Progress_Action?.Invoke(_curFileSize);
+
+ return true;
+ }
+ #endregion
+ #region IEnumerator
+ public object Current
+ {
+ get
+ {
+ return null;
+ }
+ }
+
+ public BackgroundDownloadStatus Status => _status;
+
+ public bool MoveNext()
+ {
+ if (_status == BackgroundDownloadStatus.Done ||
+ _status == BackgroundDownloadStatus.Failed ||
+ _status == BackgroundDownloadStatus.NetworkError)
+ {
+ return false;
+ }
+
+ if (_unityWebRequest.isNetworkError || _unityWebRequest.isHttpError)
+ {
+ _status = BackgroundDownloadStatus.NetworkError;
+ }
+ else if (_unityWebRequest.isDone)
+ {
+ _status = BackgroundDownloadStatus.Done;
+ }
+
+ return _status == BackgroundDownloadStatus.Downloading;
+ }
+
+ public void Reset()
+ { }
+ #endregion
+ }
+}
diff --git a/Assets/TEngine/Runtime/HotUpdate/Runtime/DownloadHandlerFileRange.cs.meta b/Assets/TEngine/Runtime/HotUpdate/Runtime/DownloadHandlerFileRange.cs.meta
new file mode 100644
index 00000000..f71035a2
--- /dev/null
+++ b/Assets/TEngine/Runtime/HotUpdate/Runtime/DownloadHandlerFileRange.cs.meta
@@ -0,0 +1,11 @@
+fileFormatVersion: 2
+guid: 13ec5fbe4e3143e479b624eac7b1431e
+MonoImporter:
+ externalObjects: {}
+ serializedVersion: 2
+ defaultReferences: []
+ executionOrder: 0
+ icon: {instanceID: 0}
+ userData:
+ assetBundleName:
+ assetBundleVariant:
diff --git a/Assets/TEngine/Runtime/HotUpdate/Runtime/DownloadImpl.cs b/Assets/TEngine/Runtime/HotUpdate/Runtime/DownloadImpl.cs
new file mode 100644
index 00000000..60e01462
--- /dev/null
+++ b/Assets/TEngine/Runtime/HotUpdate/Runtime/DownloadImpl.cs
@@ -0,0 +1,299 @@
+using System;
+using System.Collections;
+using System.Collections.Generic;
+using System.IO;
+using System.Linq;
+using System.Text;
+using System.Threading.Tasks;
+using UnityEngine;
+
+namespace TEngine
+{
+ ///
+ /// 下载状态
+ ///
+ public enum BackgroundDownloadStatus
+ {
+ NotBegin = 0, //未开始
+ Downloading = 1, //下载中
+ NetworkError = 2, //网络变化
+ Done = 3, //下载完成
+ Failed = 4, //下载失败
+ }
+
+ ///
+ /// 资源加载过程中的状态
+ ///
+ public enum DownLoadResult
+ {
+ StartDownLoad = 0, //开始下载
+ HeadRequestFail = 1, //请求头失败
+ DownLoadRequestFail = 2, //现在请求失败
+ AreadyDownLoaded = 3, //已经下载过而且下载好了
+ DownLoading = 4, //下载中
+ NetChanged = 5,
+ DownLoaded = 6, //下载完成
+ DownLoadingError = 7,//接收数据的那个过程中出错
+ HeadRequestError = 8,//获取下载包大小报错
+ ReceiveNullData = 9,//接受到空数据
+ DownError = 10,//数据没有接受完但是isDone为true
+ ReceiveError = 11,//接收数据失败
+ Md5Wrong = 12,//md5错误
+ AllDownLoaded = 13//全部下载完成
+ }
+
+ public class DownloadImpl
+ {
+ private List _files;
+ private string _path;
+ private Action> _callback = null;
+ private long _totalFileSize = 0;
+ private long _downLoadedSize = 0;
+ private long _currentLoadSize = 0;
+
+ private float _last_record_time = 0f;
+ private float _last_record_process = 0f;
+ private long _speed = 0;
+
+ IDownload _downloader = null;
+
+ public DownloadImpl(List files, string path, Action> callback)
+ {
+ _files = files;
+ _path = path;
+ _callback = callback;
+ _downLoadedSize = 0;
+ _totalFileSize = 0;
+ foreach (var item in files)
+ {
+ _totalFileSize += item.Size;
+ }
+
+ var dirPath = Path.GetDirectoryName(_path);
+ if (dirPath != null)
+ {
+ if (!Directory.Exists(dirPath))
+ {
+ Directory.CreateDirectory(dirPath);
+ }
+ }
+ }
+
+ public IEnumerator DownLoad()
+ {
+ _downLoadedSize = 0;
+ bool result = false;
+ foreach (var item in _files)
+ {
+ string remoteUrl = item.RemoteUrl + "?" + GameConfig.Instance.GameBundleVersion;
+ Callback(DownLoadResult.StartDownLoad);
+ _downloader = GetDownloader(remoteUrl, _path + item.Url, item.Size, item.Md5, this);
+ _downloader.StartDownload();
+ yield return _downloader;
+
+ if (_downloader != null)
+ {
+ _downloader.Dispose();
+ result = _DealWithDownLoadOk(DownLoadResult.DownLoaded, _downloader.Status, item);
+ if (result == false)
+ {
+ yield break;
+ }
+
+ if (_downloader.Status == BackgroundDownloadStatus.Done)
+ {
+ _downLoadedSize += item.Size;
+ }
+ }
+ yield return null;
+ }
+
+ if (result)
+ {
+ _DownLoaded(_files);
+ }
+ }
+
+ void _DownLoaded(List list)
+ {
+ Callback(DownLoadResult.AllDownLoaded, list);
+ }
+
+ bool _DealWithDownLoadOk(DownLoadResult downloadType, BackgroundDownloadStatus status, LoadResource data)
+ {
+ string fileLocalPath = _path + data.Url;
+
+ if (status == BackgroundDownloadStatus.NetworkError)
+ {
+ Callback(DownLoadResult.NetChanged);
+ return false;
+ }
+
+ if (status == BackgroundDownloadStatus.Failed)
+ {
+ LoaderUtilities.DeleteFile(fileLocalPath);
+ TLogger.LogError("DownloaderImpl._DownLoaded, Load failed");
+ Callback(DownLoadResult.ReceiveError);
+ return false;
+ }
+
+ int index = fileLocalPath.IndexOf("_md5_");
+ var tempMd5 = LoaderUtilities.GetMd5Hash(fileLocalPath);
+ if (index >= 0)
+ {
+ var fileInfo = new FileInfo(fileLocalPath);
+ string newFilename = fileLocalPath.Substring(0, index);
+ if (tempMd5 == data.Md5)
+ {
+ if (File.Exists(newFilename))
+ {
+ File.Delete(newFilename);
+ }
+
+ fileInfo.MoveTo(newFilename);
+ Callback(downloadType);
+ }
+ else
+ {
+ if (File.Exists(newFilename))
+ {
+ return true;
+ }
+ else
+ {
+ TLogger.LogError($"DownloaderImpl._DownLoaded, Current md5:{tempMd5},Target md5:{data.Md5} not match,path:{data.Url}");
+ LoaderUtilities.DeleteFile(fileLocalPath);
+ Callback(DownLoadResult.Md5Wrong);
+ return false;
+ }
+ }
+ }
+ else
+ {
+ if (tempMd5 == data.Md5)
+ {
+ Callback(downloadType);
+ }
+ else
+ {
+ TLogger.LogError($"DownloaderImpl._DownLoaded, Current md5:{tempMd5},Target md5:{data.Md5} not match,path:{data.Url}");
+ LoaderUtilities.DeleteFile(fileLocalPath);
+ Callback(DownLoadResult.Md5Wrong);
+ return false;
+ }
+ }
+ return true;
+ }
+
+ void Callback(DownLoadResult result, List files = null)
+ {
+ _callback?.Invoke((int)result, files);
+ }
+
+ ///
+ /// 文件总大小
+ ///
+ public long FileSize
+ {
+ get
+ {
+ return _totalFileSize;
+ }
+ }
+
+ public long DownLoadSize
+ {
+ get => _downLoadedSize;
+ set => _downLoadedSize = value;
+ }
+
+ public long CurrentLoadSize()
+ {
+ return _downLoadedSize + _downloader.CurrentSize;
+ }
+
+ ///
+ /// 返回下载速度
+ ///
+ public long Speed
+ {
+ get
+ {
+ if (_downloader == null)
+ return 0;
+
+ if (Time.time - _last_record_time < 0.5)
+ {
+ return _speed;
+ }
+
+ float progress = _downloader.Progress;
+ if (progress == _last_record_process)
+ {
+ return _speed;
+ }
+
+ _speed = (long)((progress - _last_record_process) * _downloader.TotalSize / (Time.time - _last_record_time));
+ _last_record_process = progress;
+ _last_record_time = Time.time;
+ return _speed;
+ }
+ }
+
+ public static IDownload GetDownloader(string url, string path, long totalLength, string md5, DownloadImpl imp)
+ {
+ DownloadHandlerFileRange loader = new DownloadHandlerFileRange(url, path, totalLength, md5);
+ loader.SetImp(imp);
+ return loader;
+ }
+
+ public bool IsLoading()
+ {
+ if (_downloader == null)
+ return false;
+ return _downloader.Status == BackgroundDownloadStatus.Downloading;
+ }
+
+ public BackgroundDownloadStatus Statue()
+ {
+ if (_downloader == null)
+ return BackgroundDownloadStatus.NotBegin;
+ return _downloader.Status;
+ }
+
+ public bool IsNetWorkChanged()
+ {
+ if (_downloader == null)
+ return false;
+ return _downloader.Status == BackgroundDownloadStatus.NetworkError;
+ }
+
+ public void StopDownLoad()
+ {
+ if (_downloader != null)
+ {
+ _downloader.Dispose();
+ }
+ }
+
+ public void Release()
+ {
+ _files = null;
+ _path = "";
+ _callback = null;
+ _totalFileSize = 0;
+ StopDownLoad();
+ }
+ }
+
+ public interface IDownload : IEnumerator
+ {
+ float Progress { get; }
+ long TotalSize { get; }
+ long CurrentSize { get; }
+ BackgroundDownloadStatus Status { get; }
+
+ void Dispose();
+ void StartDownload();
+ }
+}
diff --git a/Assets/TEngine/Runtime/HotUpdate/Runtime/DownloadImpl.cs.meta b/Assets/TEngine/Runtime/HotUpdate/Runtime/DownloadImpl.cs.meta
new file mode 100644
index 00000000..678f2020
--- /dev/null
+++ b/Assets/TEngine/Runtime/HotUpdate/Runtime/DownloadImpl.cs.meta
@@ -0,0 +1,11 @@
+fileFormatVersion: 2
+guid: a6103e1339c9a164ba4dc41f33be1cf3
+MonoImporter:
+ externalObjects: {}
+ serializedVersion: 2
+ defaultReferences: []
+ executionOrder: 0
+ icon: {instanceID: 0}
+ userData:
+ assetBundleName:
+ assetBundleVariant:
diff --git a/Assets/TEngine/Runtime/HotUpdate/Runtime/LoadData.cs b/Assets/TEngine/Runtime/HotUpdate/Runtime/LoadData.cs
new file mode 100644
index 00000000..cff8cda3
--- /dev/null
+++ b/Assets/TEngine/Runtime/HotUpdate/Runtime/LoadData.cs
@@ -0,0 +1,26 @@
+using System;
+using System.Collections.Generic;
+using System.Linq;
+using System.Text;
+using System.Threading.Tasks;
+
+namespace TEngine
+{
+ public enum GameStatus
+ {
+ First = 0,
+ AssetLoad = 1
+ }
+
+ public struct LoadResource
+ {
+ public string Url;//资源名称
+ public string Md5;//资源的md5码
+ public long Size; //资源大小(字节为单位)
+ public string RemoteUrl;//服务器地址
+ }
+
+ public class LoadData
+ {
+ }
+}
diff --git a/Assets/TEngine/Runtime/HotUpdate/Runtime/LoadData.cs.meta b/Assets/TEngine/Runtime/HotUpdate/Runtime/LoadData.cs.meta
new file mode 100644
index 00000000..d91e11b1
--- /dev/null
+++ b/Assets/TEngine/Runtime/HotUpdate/Runtime/LoadData.cs.meta
@@ -0,0 +1,11 @@
+fileFormatVersion: 2
+guid: c685f02226943e946853424f953dcfee
+MonoImporter:
+ externalObjects: {}
+ serializedVersion: 2
+ defaultReferences: []
+ executionOrder: 0
+ icon: {instanceID: 0}
+ userData:
+ assetBundleName:
+ assetBundleVariant:
diff --git a/Assets/TEngine/Runtime/HotUpdate/Runtime/LoadMgr.cs b/Assets/TEngine/Runtime/HotUpdate/Runtime/LoadMgr.cs
new file mode 100644
index 00000000..4103dab7
--- /dev/null
+++ b/Assets/TEngine/Runtime/HotUpdate/Runtime/LoadMgr.cs
@@ -0,0 +1,98 @@
+using System;
+using System.IO;
+
+namespace TEngine
+{
+ public class LoadUpdateLogic
+ {
+ private static LoadUpdateLogic _instance;
+
+ public Action Download_Complete_Action = null;
+ public Action Down_Progress_Action = null;
+ public Action _Unpacked_Complete_Action = null;
+ public Action _Unpacked_Progress_Action = null;
+
+ public static LoadUpdateLogic Instance
+ {
+ get
+ {
+ if (_instance == null)
+ _instance = new LoadUpdateLogic();
+ return _instance;
+ }
+ }
+ }
+
+ public class LoadMgr : TSingleton
+ {
+ ///
+ /// 资源版本号
+ ///
+ public string LatestResId { get; set; }
+
+ private Action _startGameEvent;
+ private int _curTryCount;
+ private const int MaxTryCount = 3;
+ private bool _connectBack;
+ private bool _needUpdate = false;
+
+ public LoadMgr()
+ {
+ _curTryCount = 0;
+ _connectBack = false;
+ _startGameEvent = null;
+ }
+
+ public void StartLoadInit(Action onUpdateComplete)
+ {
+#if RELEASE_BUILD || _DEVELOPMENT_BUILD_
+ StartLoad(() => { FinishCallBack(onUpdateComplete); });
+#else
+ onUpdateComplete();
+#endif
+ }
+
+ ///
+ /// 开启热更新逻辑
+ ///
+ ///
+ public void StartLoad(Action action)
+ {
+ _startGameEvent = action;
+ _connectBack = false;
+ _curTryCount = 0;
+ RequestVersion();
+ }
+
+ private void FinishCallBack(Action callBack)
+ {
+ GameConfig.Instance.WriteVersion(LatestResId);
+ if (_needUpdate)
+ {
+ callBack();
+ }
+ else
+ {
+ callBack();
+ }
+ }
+
+ ///
+ /// 请求热更数据
+ ///
+ private void RequestVersion()
+ {
+ if (_connectBack)
+ {
+ return;
+ }
+
+ _curTryCount++;
+
+ if (_curTryCount > MaxTryCount)
+ {
+
+ }
+ }
+ }
+}
diff --git a/Assets/TEngine/Runtime/HotUpdate/Runtime/LoadMgr.cs.meta b/Assets/TEngine/Runtime/HotUpdate/Runtime/LoadMgr.cs.meta
new file mode 100644
index 00000000..85cb03f5
--- /dev/null
+++ b/Assets/TEngine/Runtime/HotUpdate/Runtime/LoadMgr.cs.meta
@@ -0,0 +1,11 @@
+fileFormatVersion: 2
+guid: 56d713df43471c642a913a349900d028
+MonoImporter:
+ externalObjects: {}
+ serializedVersion: 2
+ defaultReferences: []
+ executionOrder: 0
+ icon: {instanceID: 0}
+ userData:
+ assetBundleName:
+ assetBundleVariant:
diff --git a/Assets/TEngine/Runtime/HotUpdate/Runtime/LoaderUtilities.cs b/Assets/TEngine/Runtime/HotUpdate/Runtime/LoaderUtilities.cs
new file mode 100644
index 00000000..3d2739f2
--- /dev/null
+++ b/Assets/TEngine/Runtime/HotUpdate/Runtime/LoaderUtilities.cs
@@ -0,0 +1,67 @@
+using System;
+using System.Collections.Generic;
+using System.IO;
+using System.Linq;
+using System.Security.Cryptography;
+using System.Text;
+using System.Threading.Tasks;
+
+namespace TEngine
+{
+ public class LoaderUtilities
+ {
+
+ ///
+ /// 删除文件
+ ///
+ /// 文件路径
+ public static void DeleteFile(string filePath)
+ {
+ try
+ {
+ if (File.Exists(filePath))
+ {
+ File.Delete(filePath);
+ }
+ }
+ catch (Exception e)
+ {
+ TLogger.LogError(e.ToString());
+ }
+ }
+
+ ///
+ /// 获取文件的md5码
+ ///
+ ///
+ ///
+ public static string GetMd5Hash(string fileName)
+ {
+ if (!File.Exists(fileName))
+ {
+ TLogger.LogWarning($"not exit file,path:{fileName}");
+ return string.Empty;
+ }
+ try
+ {
+ using (FileStream file = new FileStream(fileName, FileMode.Open, FileAccess.Read, FileShare.ReadWrite))
+ {
+ MD5 md5 = new MD5CryptoServiceProvider();
+ byte[] retVal = md5.ComputeHash(file);
+ StringBuilder sb = new StringBuilder();
+ for (int i = 0; i < retVal.Length; i++)
+ {
+ sb.Append(retVal[i].ToString("x2"));
+ }
+ return sb.ToString();
+ }
+
+ }
+ catch (Exception ex)
+ {
+ TLogger.LogError("GetMD5Hash() fail,error:" + ex.Message);
+ return string.Empty;
+ }
+ }
+ }
+}
diff --git a/Assets/TEngine/Runtime/HotUpdate/Runtime/LoaderUtilities.cs.meta b/Assets/TEngine/Runtime/HotUpdate/Runtime/LoaderUtilities.cs.meta
new file mode 100644
index 00000000..07fa06ef
--- /dev/null
+++ b/Assets/TEngine/Runtime/HotUpdate/Runtime/LoaderUtilities.cs.meta
@@ -0,0 +1,11 @@
+fileFormatVersion: 2
+guid: 186c876c2ef7f2f4db981fd403e22ce7
+MonoImporter:
+ externalObjects: {}
+ serializedVersion: 2
+ defaultReferences: []
+ executionOrder: 0
+ icon: {instanceID: 0}
+ userData:
+ assetBundleName:
+ assetBundleVariant: