using System; using System.Collections; using UnityEngine; using UnityEngine.AddressableAssets; using UnityEngine.AddressableAssets.ResourceLocators; using UnityEngine.Networking; using UnityEngine.ResourceManagement.AsyncOperations; using UnityEngine.UI; using TMPro; public class BootstrapUpdater : MonoBehaviour { public const string ContentReadyPrefsKey = "Ota.ContentReady"; [Header("Host")] [Tooltip("Apache folder that serves version.json and the Addressables catalog. Example: https://kkapi.playpoolstudios.com/ota")] [SerializeField] string otaBaseUrl = "https://kkapi.playpoolstudios.com/ota"; [Tooltip("Opened when version.json has no storeUrl. Same role as the login screen Telegram link.")] [SerializeField] string fallbackStoreUrl = "https://t.me/+dfUhy4gmGXk0ZDRl"; [Header("Content")] [SerializeField] string introSceneAddress = AddressableSceneLoader.IntroScene; [SerializeField] string gameContentLabel = "game-content"; [Header("UI")] [SerializeField] TMP_Text statusText; [SerializeField] Slider progressSlider; [SerializeField] TMP_Text progressText; [SerializeField] GameObject retryButton; [SerializeField] GameObject updateButton; string pendingStoreUrl = ""; bool updateRunning; void Start() { SetRetryVisible(false); SetUpdateVisible(false); SetProgress(0f, ""); if (!updateRunning) StartCoroutine(RunUpdate()); } public void Retry() { if (updateRunning) return; SetRetryVisible(false); SetUpdateVisible(false); StartCoroutine(RunUpdate()); } public void OpenStore() { string url = string.IsNullOrWhiteSpace(pendingStoreUrl) ? fallbackStoreUrl : pendingStoreUrl; if (!string.IsNullOrWhiteSpace(url)) Application.OpenURL(url.Trim()); } IEnumerator RunUpdate() { updateRunning = true; SetStatus("Checking for updates"); OtaVersionInfo version = null; bool versionFetched = false; yield return FetchVersion(info => { version = info; versionFetched = info != null; }); if (!versionFetched) { #if UNITY_EDITOR Debug.LogWarning("OTA version.json was not reachable. Editor play continues into " + introSceneAddress + "."); #else if (PlayerPrefs.GetInt(ContentReadyPrefsKey, 0) != 1) { Fail("Check your connection and try again."); yield break; } SetStatus("Offline. Loading saved game"); #endif } else if (OtaVersionInfo.IsClientBelowRequiredVersion(version.minPlayerVersion)) { pendingStoreUrl = version.storeUrl; string message = string.IsNullOrWhiteSpace(version.message) ? "A new version is required." : version.message.Trim(); SetStatus(message); SetUpdateVisible(true); updateRunning = false; yield break; } AsyncOperationHandle init = Addressables.InitializeAsync(false); yield return init; bool initOk = init.IsValid() && init.Status == AsyncOperationStatus.Succeeded; if (init.IsValid()) Addressables.Release(init); if (!initOk) { Fail("Could not start content update."); yield break; } AsyncOperationHandle> check = Addressables.CheckForCatalogUpdates(false); yield return check; if (check.Status == AsyncOperationStatus.Succeeded && check.Result != null && check.Result.Count > 0) { SetStatus("Updating catalog"); AsyncOperationHandle> catalogs = Addressables.UpdateCatalogs(check.Result, false); yield return catalogs; if (catalogs.Status != AsyncOperationStatus.Succeeded) { Addressables.Release(catalogs); Addressables.Release(check); Fail("Could not update the content catalog."); yield break; } Addressables.Release(catalogs); } Addressables.Release(check); long downloadBytes = 0; AsyncOperationHandle sizeHandle = Addressables.GetDownloadSizeAsync(gameContentLabel); yield return sizeHandle; if (sizeHandle.Status == AsyncOperationStatus.Succeeded) downloadBytes = sizeHandle.Result; Addressables.Release(sizeHandle); if (downloadBytes > 0) { AsyncOperationHandle download = Addressables.DownloadDependenciesAsync(gameContentLabel, false); while (!download.IsDone) { DownloadStatus status = download.GetDownloadStatus(); float percent = status.TotalBytes > 0 ? (float)status.DownloadedBytes / status.TotalBytes : download.PercentComplete; SetProgress(percent, FormatMegabytes(status.DownloadedBytes) + " / " + FormatMegabytes(status.TotalBytes)); SetStatus("Downloading update " + Mathf.RoundToInt(percent * 100f) + "%"); yield return null; } if (download.Status != AsyncOperationStatus.Succeeded) { Addressables.Release(download); Fail("Download failed. Check your connection and try again."); yield break; } Addressables.Release(download); } PlayerPrefs.SetInt(ContentReadyPrefsKey, 1); PlayerPrefs.Save(); SetStatus("Loading"); SetProgress(1f, ""); yield return AddressableSceneLoader.LoadRoutine(introSceneAddress); updateRunning = false; } IEnumerator FetchVersion(Action onResult) { string baseUrl = (otaBaseUrl ?? "").Trim().TrimEnd('/'); if (string.IsNullOrEmpty(baseUrl)) { onResult?.Invoke(null); yield break; } string url = baseUrl + "/version.json"; using (var request = UnityWebRequest.Get(url)) { request.timeout = 20; yield return request.SendWebRequest(); if (request.result != UnityWebRequest.Result.Success) { onResult?.Invoke(null); yield break; } try { onResult?.Invoke(JsonUtility.FromJson(request.downloadHandler.text)); } catch (Exception e) { Debug.LogWarning("OTA version.json parse failed: " + e.Message); onResult?.Invoke(null); } } } void Fail(string message) { SetStatus(message); SetRetryVisible(true); updateRunning = false; } void SetStatus(string message) { if (statusText != null) statusText.text = message; } void SetProgress(float normalized, string label) { if (progressSlider != null) { progressSlider.gameObject.SetActive(true); progressSlider.value = Mathf.Clamp01(normalized); } if (progressText != null) progressText.text = label ?? ""; } void SetRetryVisible(bool visible) { if (retryButton != null) retryButton.SetActive(visible); } void SetUpdateVisible(bool visible) { if (updateButton != null) updateButton.SetActive(visible); } static string FormatMegabytes(long bytes) { float mb = bytes / (1024f * 1024f); return mb.ToString("0.0", System.Globalization.CultureInfo.InvariantCulture) + " MB"; } }