using System; using System.Collections; using System.Collections.Generic; using UnityEngine; using UnityEngine.Networking; using UnityEngine.SceneManagement; using UnityEngine.Profiling; namespace CustomAssetBundleSystem { /// /// Custom AssetBundle manager - replaces Unity Addressables /// Full manual control, no Unity 6 bugs, lightweight and fast /// public class RemoteAssetBundleManager : MonoBehaviour { private static RemoteAssetBundleManager _instance; public static RemoteAssetBundleManager Instance { get { if (_instance == null) { var go = new GameObject("[RemoteAssetBundleManager]"); _instance = go.AddComponent(); DontDestroyOnLoad(go); } return _instance; } } // Configuration [Header("Remote Catalog Settings")] public string catalogUrl = "https://assetbundle.uncaptured.co/catalog.json"; public int downloadTimeout = 900; // seconds (15 minutes) for large bundles public int maxRetries = 3; [Tooltip("If progress does not change for this many seconds, abort and retry.")] public float downloadStallTimeout = 45f; [Tooltip("Base delay between retries (multiplied by attempt index).")] public float retryBackoffSeconds = 2f; [Header("Memory Management")] [Tooltip("Unloads the scene bundle and its dependencies once the scene is loaded to free GPU/CPU memory (recommended on mobile).")] public bool unloadSceneBundlesAfterLoad = true; [Tooltip("Calls Resources.UnloadUnusedAssets and GC.Collect before and after heavy scene loads.")] public bool runMemoryCleanupAroundSceneLoad = true; [Tooltip("Logs runtime memory usage after cleanup steps.")] public bool logMemoryStats = false; // State private RemoteAssetCatalog _catalog; private Dictionary _loadedBundles = new Dictionary(); private Dictionary _activeDownloads = new Dictionary(); private bool _isInitialized = false; // Events public event Action OnCatalogLoaded; public event Action OnBundleDownloadProgress; public event Action OnBundleLoaded; void Awake() { if (_instance != null && _instance != this) { Destroy(gameObject); return; } _instance = this; DontDestroyOnLoad(gameObject); } #region Initialization /// /// Initialize the system by downloading the remote catalog /// public IEnumerator InitializeAsync() { if (_isInitialized) { Debug.Log("[AssetBundleManager] Already initialized"); yield break; } Debug.Log($"[AssetBundleManager] Downloading catalog from: {catalogUrl}"); bool success = false; string errorMessage = ""; for (int attempt = 1; attempt <= maxRetries; attempt++) { using (UnityWebRequest www = UnityWebRequest.Get(catalogUrl)) { www.timeout = downloadTimeout; yield return www.SendWebRequest(); if (www.result == UnityWebRequest.Result.Success) { try { string jsonText = www.downloadHandler.text; _catalog = JsonUtility.FromJson(jsonText); if (_catalog != null && _catalog.bundles != null) { Debug.Log($"[AssetBundleManager] ✓ Catalog loaded successfully!"); Debug.Log($" Version: {_catalog.version}"); Debug.Log($" Base URL: {_catalog.baseUrl}"); Debug.Log($" Bundles: {_catalog.bundles.Count}"); _isInitialized = true; success = true; break; } else { errorMessage = "Catalog JSON is invalid or empty"; } } catch (Exception ex) { errorMessage = $"JSON parsing failed: {ex.Message}"; Debug.LogError($"[AssetBundleManager] {errorMessage}"); } } else { errorMessage = $"Download failed: {www.error}"; Debug.LogWarning($"[AssetBundleManager] Attempt {attempt}/{maxRetries}: {errorMessage}"); if (attempt < maxRetries) { yield return new WaitForSeconds(2f); // Wait before retry } } } } OnCatalogLoaded?.Invoke(success, errorMessage); if (!success) { Debug.LogError($"[AssetBundleManager] ✗ Failed to load catalog after {maxRetries} attempts: {errorMessage}"); } } public bool IsInitialized => _isInitialized; #endregion #region Bundle Loading /// /// Load an AssetBundle by name (with dependencies) /// public IEnumerator LoadBundleAsync(string bundleName, Action onComplete = null) { if (!_isInitialized) { Debug.LogError("[AssetBundleManager] Not initialized! Call InitializeAsync() first."); onComplete?.Invoke(null); yield break; } // Check if already loaded if (_loadedBundles.TryGetValue(bundleName, out AssetBundle cachedBundle)) { Debug.Log($"[AssetBundleManager] Bundle '{bundleName}' already loaded (using cached)"); onComplete?.Invoke(cachedBundle); yield break; } // Find bundle entry in catalog var bundleEntry = _catalog.FindBundleByName(bundleName); if (bundleEntry == null) { Debug.LogError($"[AssetBundleManager] Bundle '{bundleName}' not found in catalog!"); onComplete?.Invoke(null); yield break; } // Load dependencies first if (bundleEntry.dependencies != null && bundleEntry.dependencies.Count > 0) { Debug.Log($"[AssetBundleManager] Loading {bundleEntry.dependencies.Count} dependencies for '{bundleName}'..."); foreach (var depName in bundleEntry.dependencies) { if (!_loadedBundles.ContainsKey(depName)) { bool depLoaded = false; yield return LoadBundleAsync(depName, (bundle) => { depLoaded = bundle != null; }); if (!depLoaded) { Debug.LogError($"[AssetBundleManager] Failed to load dependency '{depName}' for '{bundleName}'"); onComplete?.Invoke(null); yield break; } } } } // Download and load the bundle WITH CACHING string bundleUrl = bundleEntry.GetFullUrl(_catalog.baseUrl); // Check if bundle is already cached bool isCached = Caching.IsVersionCached(bundleUrl, Hash128.Parse(bundleEntry.hash)); if (isCached) { Debug.Log($"[AssetBundleManager] ✓ Bundle '{bundleName}' found in cache (INSTANT LOAD)"); } else { Debug.Log($"[AssetBundleManager] Downloading bundle: {bundleUrl}"); Debug.Log($" Size: {bundleEntry.size / 1024f / 1024f:F2} MB"); } AssetBundle downloadedBundle = null; string lastError = null; for (int attempt = 1; attempt <= maxRetries; attempt++) { if (Application.internetReachability == NetworkReachability.NotReachable) { lastError = "No internet connection detected"; Debug.LogWarning($"[AssetBundleManager] Internet not reachable while downloading '{bundleName}'. Attempt {attempt}/{maxRetries}."); yield return new WaitForSeconds(retryBackoffSeconds * attempt); continue; } using (UnityWebRequest www = UnityWebRequestAssetBundle.GetAssetBundle(bundleUrl, Hash128.Parse(bundleEntry.hash), 0)) { www.timeout = downloadTimeout; www.disposeDownloadHandlerOnDispose = true; var operation = www.SendWebRequest(); float lastProgress = -1f; float stalledTimer = 0f; // Report download progress (only shows progress if downloading) while (!operation.isDone) { float progress = Mathf.Clamp01(www.downloadProgress); if (!isCached) { OnBundleDownloadProgress?.Invoke(bundleName, progress); } if (!isCached && downloadStallTimeout > 0f) { if (Mathf.Approximately(progress, lastProgress)) { stalledTimer += Time.unscaledDeltaTime; if (stalledTimer >= downloadStallTimeout) { Debug.LogWarning($"[AssetBundleManager] Download stalled for '{bundleName}' (progress {progress:P0}) after {stalledTimer:F1}s. Aborting attempt {attempt}/{maxRetries}."); www.Abort(); break; } } else { lastProgress = progress; stalledTimer = 0f; } } yield return null; } if (www.result == UnityWebRequest.Result.Success) { downloadedBundle = DownloadHandlerAssetBundle.GetContent(www); if (downloadedBundle != null) { _loadedBundles[bundleName] = downloadedBundle; if (isCached) { Debug.Log($"[AssetBundleManager] ✓ Bundle '{bundleName}' loaded from cache!"); } else { Debug.Log($"[AssetBundleManager] ✓ Bundle '{bundleName}' downloaded and cached!"); Debug.Log($" Cached location: {Caching.defaultCache.path}"); } OnBundleLoaded?.Invoke(bundleName, true); onComplete?.Invoke(downloadedBundle); yield break; } lastError = "Bundle extraction returned null"; Debug.LogError($"[AssetBundleManager] ✗ Failed to extract bundle from download: {bundleName}"); } else { lastError = string.IsNullOrEmpty(www.error) ? www.result.ToString() : www.error; Debug.LogWarning($"[AssetBundleManager] Attempt {attempt}/{maxRetries} failed for '{bundleName}': {lastError}"); } } if (attempt < maxRetries) { float delay = retryBackoffSeconds * attempt; Debug.Log($"[AssetBundleManager] Retrying '{bundleName}' in {delay:F1}s..."); yield return new WaitForSeconds(delay); } } Debug.LogError($"[AssetBundleManager] ✗ Failed to download bundle '{bundleName}': {lastError}"); OnBundleLoaded?.Invoke(bundleName, false); onComplete?.Invoke(null); yield break; } /// /// Load a scene from a remote AssetBundle by scene name /// public IEnumerator LoadSceneAsync(string sceneName, Action onComplete = null) { if (!_isInitialized) { Debug.LogError("[AssetBundleManager] Not initialized! Call InitializeAsync() first."); onComplete?.Invoke(false); yield break; } // Find which bundle contains this scene var bundleEntry = _catalog.FindBundleByScene(sceneName); if (bundleEntry == null) { Debug.LogError($"[AssetBundleManager] Scene '{sceneName}' not found in any bundle!"); onComplete?.Invoke(false); yield break; } Debug.Log($"[AssetBundleManager] Scene '{sceneName}' found in bundle '{bundleEntry.bundleName}'"); var dependencyEntries = _catalog.GetAllDependencies(bundleEntry); if (runMemoryCleanupAroundSceneLoad) { yield return RunMemoryCleanupAsync($"[AssetBundleManager] Pre-load memory cleanup before '{sceneName}'"); } // Load the bundle (and dependencies) bool bundleLoaded = false; yield return LoadBundleAsync(bundleEntry.bundleName, (bundle) => { bundleLoaded = bundle != null; }); if (!bundleLoaded) { Debug.LogError($"[AssetBundleManager] Failed to load bundle for scene '{sceneName}'"); onComplete?.Invoke(false); yield break; } // CRITICAL FIX: Load scene in SINGLE mode to avoid Unity bug with additive loading // The bug causes scene loading to hang at 0.2% when using Additive mode from AssetBundles Debug.Log($"[AssetBundleManager] Loading scene '{sceneName}' from bundle (SINGLE mode to avoid Unity hang bug)..."); // Extra instrumentation: timestamp, loaded scenes, memory Debug.Log($"[AssetBundleManager] StartTime: {DateTime.UtcNow:O}"); Debug.Log($"[AssetBundleManager] Current loaded scenes: {SceneManager.sceneCount}"); for (int si = 0; si < SceneManager.sceneCount; si++) { var s = SceneManager.GetSceneAt(si); Debug.Log($"[AssetBundleManager] Scene[{si}] name={s.name} isLoaded={s.isLoaded}"); } var sceneLoadOperation = SceneManager.LoadSceneAsync(sceneName, LoadSceneMode.Single); if (sceneLoadOperation == null) { Debug.LogError($"[AssetBundleManager] Failed to start scene load: {sceneName}"); onComplete?.Invoke(false); yield break; } // Allow scene to activate immediately (no manual activation needed) sceneLoadOperation.allowSceneActivation = true; // Wait for scene to load with timeout protection and stall detection float timeout = 30f; // 30 second timeout float elapsed = 0f; // Track progress samples to detect stalls (common symptom: stuck at ~0.2) float lastProgress = -1f; float progressUnchangedTime = 0f; const float stallThresholdSeconds = 8f; // if progress unchanged for this long, treat as stall bool attemptedRetry = false; while (!sceneLoadOperation.isDone && elapsed < timeout) { float progress = sceneLoadOperation.progress; // 0..1 // Log only when progress changes or at periodic intervals to avoid log spam if (Mathf.Approximately(progress, lastProgress)) { progressUnchangedTime += Time.deltaTime; } else { lastProgress = progress; progressUnchangedTime = 0f; Debug.Log($"[AssetBundleManager] Scene load progress: {progress * 100:F1}% (elapsed {elapsed:F2}s)"); } // If we detect a stall near 0.2 (historical hang), attempt a single retry if (!attemptedRetry && progress <= 0.21f && progressUnchangedTime > stallThresholdSeconds) { Debug.LogWarning($"[AssetBundleManager] Detected scene-load stall at {progress * 100:F1}% for {progressUnchangedTime:F1}s. Attempting one retry..."); // Attempt to unload bundle and retry once after small delay try { string retryBundle = bundleEntry.bundleName; if (_loadedBundles.ContainsKey(retryBundle)) { Debug.Log($"[AssetBundleManager] Unloading bundle '{retryBundle}' to attempt retry..."); UnloadBundle(retryBundle, true); } } catch (Exception ex) { Debug.LogException(ex); } // wait a short moment and restart the load operation yield return new WaitForSeconds(0.5f); attemptedRetry = true; elapsed = 0f; lastProgress = -1f; progressUnchangedTime = 0f; Debug.Log("[AssetBundleManager] Starting retry of SceneManager.LoadSceneAsync..."); sceneLoadOperation = SceneManager.LoadSceneAsync(sceneName, LoadSceneMode.Single); if (sceneLoadOperation == null) { Debug.LogError($"[AssetBundleManager] Retry failed to start scene load: {sceneName}"); onComplete?.Invoke(false); yield break; } sceneLoadOperation.allowSceneActivation = true; } elapsed += Time.deltaTime; yield return null; } if (elapsed >= timeout && !sceneLoadOperation.isDone) { Debug.LogError($"[AssetBundleManager] Scene load timed out after {timeout} seconds! progress={sceneLoadOperation.progress * 100:F1}% attemptedRetry={attemptedRetry}"); // dump some diagnostic info Debug.Log($"[AssetBundleManager] Loaded bundles: {string.Join(",", GetLoadedBundleNames())}"); Debug.Log($"[AssetBundleManager] Memory stats: {GetMemoryStats()}"); onComplete?.Invoke(false); yield break; } Debug.Log($"[AssetBundleManager] ✓ Scene '{sceneName}' loaded successfully in {elapsed:F2} seconds! attemptedRetry={attemptedRetry}"); if (unloadSceneBundlesAfterLoad) { yield return null; // ensure the scene has a frame to settle before we release bundles ReleaseSceneBundles(bundleEntry, dependencyEntries); } if (runMemoryCleanupAroundSceneLoad) { yield return RunMemoryCleanupAsync($"[AssetBundleManager] Post-load memory cleanup after '{sceneName}'"); } onComplete?.Invoke(true); } #endregion #region Cache Management /// /// Check if a bundle is cached on disk /// public bool IsBundleCached(string bundleName) { if (!_isInitialized || _catalog == null) return false; var bundleEntry = _catalog.FindBundleByName(bundleName); if (bundleEntry == null) return false; string bundleUrl = bundleEntry.GetFullUrl(_catalog.baseUrl); return Caching.IsVersionCached(bundleUrl, Hash128.Parse(bundleEntry.hash)); } /// /// Get cache information /// public void GetCacheInfo() { Debug.Log($"[AssetBundleManager] Cache Info:"); Debug.Log($" Cache path: {Caching.defaultCache.path}"); Debug.Log($" Cache space used: {Caching.defaultCache.spaceOccupied / 1024f / 1024f:F2} MB"); Debug.Log($" Max cache size: {Caching.defaultCache.maximumAvailableStorageSpace / 1024f / 1024f / 1024f:F2} GB"); Debug.Log($" Caching ready: {Caching.ready}"); } /// /// Clear all cached bundles (forces re-download on next load) /// public bool ClearCache() { bool success = Caching.ClearCache(); if (success) { Debug.Log($"[AssetBundleManager] ✓ Cache cleared successfully!"); } else { Debug.LogError($"[AssetBundleManager] ✗ Failed to clear cache"); } return success; } /// /// Clear only old versions of cached bundles (keeps current version) /// public void CleanupOldCachedVersions() { bool success = Caching.ClearAllCachedVersions(Caching.defaultCache.path); if (success) { Debug.Log($"[AssetBundleManager] ✓ Cleaned up old cached versions"); } else { Debug.LogWarning($"[AssetBundleManager] Failed to cleanup old cached versions"); } } #endregion #region Memory Management /// /// Unload a specific bundle /// public void UnloadBundle(string bundleName, bool unloadAllLoadedObjects = false) { if (_loadedBundles.TryGetValue(bundleName, out AssetBundle bundle)) { bundle.Unload(unloadAllLoadedObjects); _loadedBundles.Remove(bundleName); Debug.Log($"[AssetBundleManager] Unloaded bundle: {bundleName}"); } } /// /// Unload all bundles /// public void UnloadAllBundles(bool unloadAllLoadedObjects = false) { foreach (var kvp in _loadedBundles) { kvp.Value.Unload(unloadAllLoadedObjects); } _loadedBundles.Clear(); Debug.Log($"[AssetBundleManager] Unloaded all bundles"); } /// /// Get memory usage stats /// public string GetMemoryStats() { long totalMemory = 0; foreach (var kvp in _loadedBundles) { // AssetBundle doesn't expose size directly, but we can estimate totalMemory += kvp.Value.GetAllAssetNames().Length * 1024; // Rough estimate } return $"Loaded Bundles: {_loadedBundles.Count}, Estimated Memory: {totalMemory / 1024f / 1024f:F2} MB"; } private void ReleaseSceneBundles(RemoteAssetCatalog.BundleEntry rootBundle, List dependencyEntries) { if (rootBundle == null) return; var bundlesToRelease = new List { rootBundle }; if (dependencyEntries != null) { bundlesToRelease.AddRange(dependencyEntries); } foreach (var entry in bundlesToRelease) { if (entry == null) continue; if (_loadedBundles.ContainsKey(entry.bundleName)) { Debug.Log($"[AssetBundleManager] Releasing bundle '{entry.bundleName}' after scene load"); UnloadBundle(entry.bundleName, false); } } } private IEnumerator RunMemoryCleanupAsync(string reason) { Debug.Log(reason); // Give the UI a frame to update before we start releasing memory heavy objects. yield return null; yield return Resources.UnloadUnusedAssets(); GC.Collect(); if (logMemoryStats) { Debug.Log($"[AssetBundleManager] Runtime memory usage: {GetRuntimeMemoryStats()}"); } } private string GetRuntimeMemoryStats() { long allocated = Profiler.GetTotalAllocatedMemoryLong(); long reserved = Profiler.GetTotalReservedMemoryLong(); long unused = Profiler.GetTotalUnusedReservedMemoryLong(); return $"Allocated: {allocated / (1024f * 1024f):F1} MB | Reserved: {reserved / (1024f * 1024f):F1} MB | Unused Reserved: {unused / (1024f * 1024f):F1} MB | Bundles Cached: {GetMemoryStats()}"; } #endregion #region Catalog Info /// /// Get the loaded catalog (null if not initialized) /// public RemoteAssetCatalog GetCatalog() => _catalog; /// /// Check if a bundle is loaded /// public bool IsBundleLoaded(string bundleName) => _loadedBundles.ContainsKey(bundleName); /// /// Get list of all loaded bundle names /// public List GetLoadedBundleNames() => new List(_loadedBundles.Keys); #endregion void OnDestroy() { UnloadAllBundles(true); } } }