#if UNITY_EDITOR using System; using System.Collections.Generic; using UnityEditor; using UnityEngine.ResourceManagement.AsyncOperations; using UnityEngine.ResourceManagement.Exceptions; using UnityEngine.ResourceManagement.ResourceLocations; using UnityEngine.ResourceManagement.Util; using UnityEngine.Serialization; namespace UnityEngine.ResourceManagement.ResourceProviders.Simulation { abstract class VBAsyncOperation { public abstract DownloadStatus GetDownloadStatus(); public abstract bool WaitForCompletion(); } class VBAsyncOperation : VBAsyncOperation { protected TObject m_Result; protected AsyncOperationStatus m_Status; protected Exception m_Error; protected object m_Context; DelegateList> m_CompletedAction; Action> m_OnDestroyAction; public override DownloadStatus GetDownloadStatus() => default; public override bool WaitForCompletion() => true; public override string ToString() { var instId = ""; var or = m_Result as Object; if (or != null) instId = "(" + or.GetInstanceID() + ")"; return string.Format("{0}, result='{1}', status='{2}', location={3}.", base.ToString(), (m_Result + instId), m_Status, m_Context); } public event Action> Completed { add { if (IsDone) { DelayedActionManager.AddAction(value, 0, this); } else { if (m_CompletedAction == null) m_CompletedAction = DelegateList>.CreateWithGlobalCache(); m_CompletedAction.Add(value); } } remove { m_CompletedAction.Remove(value); } } public AsyncOperationStatus Status { get { return m_Status; } protected set { m_Status = value; } } /// public Exception OperationException { get { return m_Error; } protected set { m_Error = value; if (m_Error != null && ResourceManager.ExceptionHandler != null) ResourceManager.ExceptionHandler(new AsyncOperationHandle(null), value); } } public TObject Result { get { return m_Result; } } public virtual bool IsDone { get { return Status == AsyncOperationStatus.Failed || Status == AsyncOperationStatus.Succeeded; } } /// public virtual float PercentComplete { get { return IsDone ? 1f : 0f; } } /// public object Context { get { return m_Context; } set { m_Context = value; } } public void InvokeCompletionEvent() { if (m_CompletedAction != null) { m_CompletedAction.Invoke(this); m_CompletedAction.Clear(); } } public virtual void SetResult(TObject result) { m_Result = result; m_Status = (m_Result == null) ? AsyncOperationStatus.Failed : AsyncOperationStatus.Succeeded; } public VBAsyncOperation StartCompleted(object context, object key, TObject val, Exception error = null) { Context = context; OperationException = error; m_Result = val; m_Status = (m_Result == null) ? AsyncOperationStatus.Failed : AsyncOperationStatus.Succeeded; DelayedActionManager.AddAction((Action)InvokeCompletionEvent); return this; } } /// /// Contains data needed to simulate a bundled asset /// [Serializable] public class VirtualAssetBundleEntry { [FormerlySerializedAs("m_name")] [SerializeField] string m_Name; /// /// The name of the asset. /// public string Name { get { return m_Name; } } [FormerlySerializedAs("m_size")] [SerializeField] long m_Size; /// /// The file size of the asset, in bytes. /// public long Size { get { return m_Size; } } [SerializeField] internal string m_AssetPath; /// /// Construct a new VirtualAssetBundleEntry /// public VirtualAssetBundleEntry() {} /// /// Construct a new VirtualAssetBundleEntry /// /// The name of the asset. /// The size of the asset, in bytes. public VirtualAssetBundleEntry(string name, long size) { m_Name = name; m_Size = size; } } /// /// Contains data need to simulate an asset bundle. /// [Serializable] public class VirtualAssetBundle : ISerializationCallbackReceiver, IAssetBundleResource { [FormerlySerializedAs("m_name")] [SerializeField] string m_Name; [FormerlySerializedAs("m_isLocal")] [SerializeField] bool m_IsLocal; [FormerlySerializedAs("m_dataSize")] [SerializeField] long m_DataSize; [FormerlySerializedAs("m_headerSize")] [SerializeField] long m_HeaderSize; [FormerlySerializedAs("m_latency")] [SerializeField] float m_Latency; [SerializeField] uint m_Crc; [SerializeField] string m_Hash; [FormerlySerializedAs("m_serializedAssets")] [SerializeField] List m_SerializedAssets = new List(); long m_HeaderBytesLoaded; long m_DataBytesLoaded; LoadAssetBundleOp m_BundleLoadOperation; List m_AssetLoadOperations = new List(); Dictionary m_AssetMap; /// /// The name of the bundle. /// public string Name { get { return m_Name; } } /// /// The assets contained in the bundle. /// public List Assets { get { return m_SerializedAssets; } } const long k_SynchronousBytesPerSecond = (long) 1024 * 1024 * 1024 * 10; // 10 Gb/s /// /// Construct a new VirtualAssetBundle object. /// public VirtualAssetBundle() { } /// /// The percent of data that has been loaded. /// public float PercentComplete { get { if (m_HeaderSize + m_DataSize <= 0) return 1; return (float)(m_HeaderBytesLoaded + m_DataBytesLoaded) / (m_HeaderSize + m_DataSize); } } /// /// Construct a new VirtualAssetBundle /// /// The name of the bundle. /// Is the bundle local or remote. This is used to determine which bandwidth value to use when simulating loading. public VirtualAssetBundle(string name, bool local, uint crc, string hash) { m_Latency = .1f; m_Name = name; m_IsLocal = local; m_HeaderBytesLoaded = 0; m_DataBytesLoaded = 0; m_Crc = crc; m_Hash = hash; } /// /// Set the size of the bundle. /// /// The size of the data. /// The size of the header. public void SetSize(long dataSize, long headerSize) { m_HeaderSize = headerSize; m_DataSize = dataSize; } /// /// Not used /// public void OnBeforeSerialize() { } /// /// Load serialized data into runtime structures. /// public void OnAfterDeserialize() { m_AssetMap = new Dictionary(); foreach (var a in m_SerializedAssets) m_AssetMap.Add(a.Name, a); } class LoadAssetBundleOp : VBAsyncOperation { VirtualAssetBundle m_Bundle; float m_TimeInLoadingState; bool m_crcHashValidated; public LoadAssetBundleOp(IResourceLocation location, VirtualAssetBundle bundle) { Context = location; m_Bundle = bundle; m_TimeInLoadingState = 0.0f; } public override bool WaitForCompletion() { SetResult(m_Bundle); InvokeCompletionEvent(); return true; } public override DownloadStatus GetDownloadStatus() { if (m_Bundle.m_IsLocal) return new DownloadStatus() { IsDone = IsDone }; return new DownloadStatus() { DownloadedBytes = m_Bundle.m_DataBytesLoaded, TotalBytes = m_Bundle.m_DataSize, IsDone = IsDone }; } public override float PercentComplete { get { if (IsDone) return 1f; return m_Bundle.PercentComplete; } } public void Update(long localBandwidth, long remoteBandwidth, float unscaledDeltaTime) { if (m_Result != null) return; if (!m_crcHashValidated) { var location = Context as IResourceLocation; var reqOptions = location.Data as AssetBundleRequestOptions; if (reqOptions != null) { if (reqOptions.Crc != 0 && m_Bundle.m_Crc != reqOptions.Crc) { var err = string.Format("Error while downloading Asset Bundle: CRC Mismatch. Provided {0}, calculated {1} from data. Will not load Asset Bundle.", reqOptions.Crc, m_Bundle.m_Crc); SetResult(null); OperationException = new Exception(err); InvokeCompletionEvent(); } if (!m_Bundle.m_IsLocal) { if (!string.IsNullOrEmpty(reqOptions.Hash)) { if (string.IsNullOrEmpty(m_Bundle.m_Hash) || m_Bundle.m_Hash != reqOptions.Hash) { Debug.LogWarningFormat("Mismatched hash in bundle {0}.", m_Bundle.Name); } //TODO: implement virtual cache that would persist between runs. //if(vCache.hashBundle(m_Bundle.Name, reqOptions.Hash)) // m_m_Bundle.IsLocal = true; } } } m_crcHashValidated = true; } m_TimeInLoadingState += unscaledDeltaTime; if (m_TimeInLoadingState > m_Bundle.m_Latency) { long localBytes = (long)Math.Ceiling(unscaledDeltaTime * localBandwidth); long remoteBytes = (long)Math.Ceiling(unscaledDeltaTime * remoteBandwidth); if (m_Bundle.LoadData(localBytes, remoteBytes)) { SetResult(m_Bundle); InvokeCompletionEvent(); } } } } bool LoadData(long localBytes, long remoteBytes) { if (m_IsLocal) { m_HeaderBytesLoaded += localBytes; if (m_HeaderBytesLoaded < m_HeaderSize) return false; m_HeaderBytesLoaded = m_HeaderSize; return true; } else { if (m_DataBytesLoaded < m_DataSize) { m_DataBytesLoaded += remoteBytes; if (m_DataBytesLoaded < m_DataSize) return false; m_DataBytesLoaded = m_DataSize; return false; } m_HeaderBytesLoaded += localBytes; if (m_HeaderBytesLoaded < m_HeaderSize) return false; m_HeaderBytesLoaded = m_HeaderSize; return true; } } internal bool Unload() { if (m_BundleLoadOperation == null) Debug.LogWarningFormat("Simulated assetbundle {0} is already unloaded.", m_Name); m_HeaderBytesLoaded = 0; m_BundleLoadOperation = null; return true; } internal VBAsyncOperation StartLoad(IResourceLocation location) { if (m_BundleLoadOperation != null) { if (m_BundleLoadOperation.IsDone) Debug.LogWarningFormat("Simulated assetbundle {0} is already loaded.", m_Name); else Debug.LogWarningFormat("Simulated assetbundle {0} is already loading.", m_Name); return m_BundleLoadOperation; } m_HeaderBytesLoaded = 0; return (m_BundleLoadOperation = new LoadAssetBundleOp(location, this)); } /// /// Load an asset via its location. The asset will actually be loaded via the AssetDatabase API. /// /// /// /// internal VBAsyncOperation LoadAssetAsync(ProvideHandle provideHandle, IResourceLocation location) { if (location == null) throw new ArgumentException("IResourceLocation location cannot be null."); if (m_BundleLoadOperation == null) return new VBAsyncOperation().StartCompleted(location, location, null, new ResourceManagerException("LoadAssetAsync called on unloaded bundle " + m_Name)); if (!m_BundleLoadOperation.IsDone) return new VBAsyncOperation().StartCompleted(location, location, null, new ResourceManagerException("LoadAssetAsync called on loading bundle " + m_Name)); VirtualAssetBundleEntry assetInfo; var assetPath = location.InternalId; if (ResourceManagerConfig.ExtractKeyAndSubKey(assetPath, out string mainPath, out string subKey)) assetPath = mainPath; //this needs to use the non translated internal id since that was how the table was built. if (!m_AssetMap.TryGetValue(assetPath, out assetInfo)) return new VBAsyncOperation().StartCompleted(location, location, null, new ResourceManagerException(string.Format("Unable to load asset {0} from simulated bundle {1}.", location.InternalId, Name))); var op = new LoadAssetOp(location, assetInfo, provideHandle); m_AssetLoadOperations.Add(op); return op; } internal void CountBandwidthUsage(ref long localCount, ref long remoteCount) { if (m_BundleLoadOperation != null && m_BundleLoadOperation.IsDone) { localCount += m_AssetLoadOperations.Count; return; } if (m_IsLocal) { localCount++; } else { if (m_DataBytesLoaded < m_DataSize) remoteCount++; else localCount++; } } interface IVirtualLoadable { bool Load(long localBandwidth, long remoteBandwidth, float unscaledDeltaTime); } // TODO: This is only needed internally. We can change this to not derive off of AsyncOperationBase and simplify the code class LoadAssetOp : VBAsyncOperation, IVirtualLoadable { long m_BytesLoaded; float m_LastUpdateTime; VirtualAssetBundleEntry m_AssetInfo; ProvideHandle m_provideHandle; public LoadAssetOp(IResourceLocation location, VirtualAssetBundleEntry assetInfo, ProvideHandle ph) { m_provideHandle = ph; Context = location; m_AssetInfo = assetInfo; m_LastUpdateTime = Time.realtimeSinceStartup; } public override bool WaitForCompletion() { //TODO: this needs to just wait on the resourcemanager update loop to ensure proper loading order while (!IsDone) { Load(k_SynchronousBytesPerSecond, k_SynchronousBytesPerSecond, .1f); System.Threading.Thread.Sleep(100); } return true; } public override float PercentComplete { get { return Mathf.Clamp01(m_BytesLoaded / (float)m_AssetInfo.Size); } } public bool Load(long localBandwidth, long remoteBandwidth, float unscaledDeltaTime) { if (IsDone) return false; var now = m_LastUpdateTime + unscaledDeltaTime; if (now > m_LastUpdateTime) { m_BytesLoaded += (long)Math.Ceiling((now - m_LastUpdateTime) * localBandwidth); m_LastUpdateTime = now; } if (m_BytesLoaded < m_AssetInfo.Size) return true; if (!(Context is IResourceLocation)) return false; var location = Context as IResourceLocation; var assetPath = m_AssetInfo.m_AssetPath; object result = null; var pt = m_provideHandle.Type; if (pt.IsArray) result = ResourceManagerConfig.CreateArrayResult(pt, AssetDatabaseProvider.LoadAssetsWithSubAssets(assetPath)); else if (pt.IsGenericType && typeof(IList<>) == pt.GetGenericTypeDefinition()) result = ResourceManagerConfig.CreateListResult(pt, AssetDatabaseProvider.LoadAssetsWithSubAssets(assetPath)); else { if (ResourceManagerConfig.ExtractKeyAndSubKey(location.InternalId, out string mainPath, out string subKey)) result = AssetDatabaseProvider.LoadAssetSubObject(assetPath, subKey, pt); else result = AssetDatabaseProvider.LoadAssetAtPath(assetPath, m_provideHandle); } SetResult(result); InvokeCompletionEvent(); return false; } } //return true until complete internal bool UpdateAsyncOperations(long localBandwidth, long remoteBandwidth, float unscaledDeltaTime) { if (m_BundleLoadOperation == null) return false; if (!m_BundleLoadOperation.IsDone) { m_BundleLoadOperation.Update(localBandwidth, remoteBandwidth, unscaledDeltaTime); return true; } foreach (var o in m_AssetLoadOperations) { if (!o.Load(localBandwidth, remoteBandwidth, unscaledDeltaTime)) { m_AssetLoadOperations.Remove(o); break; } } return m_AssetLoadOperations.Count > 0; } /// /// Implementation of IAssetBundleResource API /// /// Always returns null. public AssetBundle GetAssetBundle() { return null; } } } #endif