using System; using BrewMonster; using Cysharp.Threading.Tasks; using UnityEngine; using UnityEngine.Networking; namespace BrewMonster.Scripts { /// /// Bootstrap: gọi server lấy contentVersion + (tuỳ chọn) assetsBaseUrl, gắn URL rewrite trước khi Addressables init, /// rồi nếu lần đầu hoặc contentVersion khác đã lưu thì cập nhật catalog và tải toàn bộ entry gắn label. /// First run / version bump: call server for contentVersion + optional assetsBaseUrl, apply URL rewrite before Addressables init, /// then if first launch or stored version differs, update catalogs and download all entries under the configured label. /// [DefaultExecutionOrder(-2000)] public class GameContentBootstrap : MonoBehaviour { const string PrefsFirstSyncDone = "PW_GameContent_FirstRemoteSyncDone"; const string PrefsLastContentVersion = "PW_GameContent_LastContentVersion"; /// /// When non-null, waits on this before . /// static UniTaskCompletionSource s_addressablesInitGate; [Header("Version API")] [Tooltip("GET JSON: { \"contentVersion\": \"12\", \"assetsBaseUrl\": \"https://cdn/.../\" }")] [SerializeField] string _versionEndpointUrl; [SerializeField] int _requestTimeoutSeconds = 20; [Header("Addressables")] [Tooltip("If true, blocks Addressables init until version HTTP + optional URL rewrite finish (recommended).")] [SerializeField] bool _holdAddressablesInitUntilVersionChecked = true; [Tooltip("Prefix baked into remote InternalIds at build (Remote.LoadPath). When server sends assetsBaseUrl, replace this prefix.")] [SerializeField] string _bakedRemoteUrlPrefixForRewrite; [Tooltip("Label applied in Addressables Groups to every remote asset that must download on first run / content update.")] [SerializeField] string _remoteBulkDownloadLabel = "RemoteContent"; [Header("Editor")] [SerializeField] bool _skipRemoteCallInEditor; [SerializeField] string _editorFakeContentVersion = "editor"; public event Action Finished; public static UniTask WaitForPreAddressablesSetupIfAnyAsync() { if (s_addressablesInitGate == null) return UniTask.CompletedTask; return s_addressablesInitGate.Task; } void Awake() { if (_holdAddressablesInitUntilVersionChecked) s_addressablesInitGate ??= new UniTaskCompletionSource(); } void Start() { RunAsync().Forget(); } void OnDestroy() { ReleaseGateIfPending(); } void OnApplicationQuit() { ReleaseGateIfPending(); } static void ReleaseGateIfPending() { if (s_addressablesInitGate != null) { s_addressablesInitGate.TrySetResult(); s_addressablesInitGate = null; } } async UniTaskVoid RunAsync() { var result = await RunInternalAsync(); Finished?.Invoke(result); } public async UniTask RunInternalAsync() { try { var dto = await FetchVersionDtoAsync(); if (!dto.Ok) { AllowAddressablesInit(); return new BootstrapResult(false, false, dto.Error, null); } if (!string.IsNullOrWhiteSpace(dto.AssetsBaseUrl) && !string.IsNullOrWhiteSpace(_bakedRemoteUrlPrefixForRewrite)) { AddressablesRuntimeUrlRewriter.InstallPrefixRewrite( _bakedRemoteUrlPrefixForRewrite, dto.AssetsBaseUrl); } AllowAddressablesInit(); await AddressablesCatalogUpdater.EnsureInitializedAsync(); bool firstRun = PlayerPrefs.GetInt(PrefsFirstSyncDone, 0) == 0; string localVersion = PlayerPrefs.GetString(PrefsLastContentVersion, string.Empty); bool needContentWork = firstRun || !string.Equals(localVersion, dto.ContentVersion ?? string.Empty, StringComparison.Ordinal); if (!needContentWork) { BMLogger.Log("[Cuong] GameContentBootstrap: contentVersion unchanged, skip catalog/bulk download."); return new BootstrapResult(true, false, null, dto.ContentVersion); } var catalog = await AddressablesCatalogUpdater.CheckAndUpdateCatalogsIfNeededAsync(false); if (!catalog.Success) return new BootstrapResult(false, true, catalog.Error?.Message ?? "Catalog update failed", dto.ContentVersion); if (string.IsNullOrWhiteSpace(_remoteBulkDownloadLabel)) { BMLogger.LogError("[Cuong] GameContentBootstrap: _remoteBulkDownloadLabel is empty; skipping bulk download."); return new BootstrapResult(false, true, "remoteBulkDownloadLabel empty", dto.ContentVersion); } bool dl = await AddressablesCatalogUpdater.DownloadDependenciesAsync(_remoteBulkDownloadLabel.Trim(), true); if (!dl) return new BootstrapResult(false, true, "DownloadDependencies failed", dto.ContentVersion); PlayerPrefs.SetString(PrefsLastContentVersion, dto.ContentVersion); PlayerPrefs.SetInt(PrefsFirstSyncDone, 1); PlayerPrefs.Save(); BMLogger.Log($"[Cuong] GameContentBootstrap: content sync OK, version={dto.ContentVersion}"); return new BootstrapResult(true, true, null, dto.ContentVersion); } catch (Exception e) { BMLogger.LogError($"[Cuong] GameContentBootstrap: {e.Message}"); AllowAddressablesInit(); return new BootstrapResult(false, true, e.Message, null); } } static void AllowAddressablesInit() { if (s_addressablesInitGate != null) { s_addressablesInitGate.TrySetResult(); s_addressablesInitGate = null; } } async UniTask FetchVersionDtoAsync() { #if UNITY_EDITOR if (_skipRemoteCallInEditor) { return new VersionDtoResult(true, _editorFakeContentVersion?.Trim() ?? "editor", null, null); } #endif if (string.IsNullOrWhiteSpace(_versionEndpointUrl)) { BMLogger.LogWarning("[Cuong] GameContentBootstrap: _versionEndpointUrl empty; using empty contentVersion (no remote check)."); return new VersionDtoResult(true, string.Empty, null, null); } using var req = UnityWebRequest.Get(_versionEndpointUrl); req.timeout = Mathf.Max(5, _requestTimeoutSeconds); await req.SendWebRequest().ToUniTask(); if (req.result != UnityWebRequest.Result.Success) return new VersionDtoResult(false, null, null, req.error); string json = req.downloadHandler?.text; if (string.IsNullOrWhiteSpace(json)) return new VersionDtoResult(false, null, null, "Empty response body"); try { var parsed = JsonUtility.FromJson(json); if (parsed == null || string.IsNullOrWhiteSpace(parsed.contentVersion)) return new VersionDtoResult(false, null, null, "JSON missing contentVersion"); return new VersionDtoResult(true, parsed.contentVersion.Trim(), parsed.assetsBaseUrl?.Trim(), null); } catch (Exception e) { return new VersionDtoResult(false, null, null, $"JSON parse: {e.Message}"); } } readonly struct VersionDtoResult { public VersionDtoResult(bool ok, string contentVersion, string assetsBaseUrl, string error) { Ok = ok; ContentVersion = contentVersion; AssetsBaseUrl = assetsBaseUrl; Error = error; } public bool Ok { get; } public string ContentVersion { get; } public string AssetsBaseUrl { get; } public string Error { get; } } [Serializable] class VersionResponseJson { public string contentVersion; public string assetsBaseUrl; } public readonly struct BootstrapResult { public BootstrapResult(bool success, bool didContentWork, string errorMessage, string serverContentVersion) { Success = success; DidContentWork = didContentWork; ErrorMessage = errorMessage; ServerContentVersion = serverContentVersion; } public bool Success { get; } /// True if catalog/bulk download ran this session. public bool DidContentWork { get; } public string ErrorMessage { get; } public string ServerContentVersion { get; } } } }