using System; using BrewMonster; using Cysharp.Threading.Tasks; using UnityEngine; namespace BrewMonster.Scripts { /// /// Bootstrap: lấy contentVersion + (tuỳ chọn) assetsBaseUrl qua server hoặc hardcode, 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: resolve version + optional assetsBaseUrl (server or hardcoded), 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 source")] [Tooltip("True = gọi HTTP tới URL bên dưới. False = không request server, chỉ dùng hardcode (test / API chưa chốt).")] [SerializeField] bool _useServerForVersionInfo; [Header("Hardcoded (khi tắt server hoặc Editor skip remote)")] [Tooltip("Dùng khi _useServerForVersionInfo = false, hoặc Editor bật Skip Remote (ưu tiên field này nếu có).")] [SerializeField] string _hardcodedContentVersion = "0"; [Tooltip("Tuỳ chọn: base URL CDN mới (rewrite), để trống nếu không đổi URL runtime.")] [SerializeField] string _hardcodedAssetsBaseUrl; [Header("Version API (chỉ khi bật server)")] [Tooltip("URL đầy đủ (https://...). Server trả JSON: contentVersion + tuỳ chọn assetsBaseUrl (prefix CDN). / Full URL; server returns JSON: contentVersion + optional assetsBaseUrl (CDN prefix).")] [SerializeField] string _versionEndpointUrl; [Tooltip("Bật = POST JSON body bên dưới; tắt = GET (không body). Server kiểm tra body rồi trả cùng format JSON. / On = POST JSON body below; off = GET.")] [SerializeField] bool _usePostForVersionRequest; [Tooltip("Body POST (JSON). Để trống → gửi \"{}\". Ví dụ: {\"userId\":\"...\",\"platform\":\"Android\"}. / POST JSON body; empty sends \"{}\".")] [TextArea(2, 8)] [SerializeField] string _versionPostBody; [SerializeField] int _requestTimeoutSeconds = 20; [Header("Addressables")] [Tooltip("If true, blocks Addressables init until version resolve + optional URL rewrite finish (recommended).")] [SerializeField] bool _holdAddressablesInitUntilVersionChecked = true; [Tooltip("Prefix baked into remote InternalIds at build (Remote.LoadPath). When assetsBaseUrl is set, 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")] [Tooltip("Editor: không gọi HTTP server; dùng hardcode (nếu có version) hoặc _editorFakeContentVersion.")] [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 { Debug.Log("[Cuong] GameContentBootstrap: Bắt đầu bootstrap Addressables..."); Debug.Log("[Cuong] GameContentBootstrap: Đang lấy contentVersion / assetsBaseUrl..."); var fetch = await ResolveVersionFetchAsync(); if (!fetch.Ok) { Debug.LogError($"[Cuong] GameContentBootstrap: Lỗi lấy version — {fetch.Error}"); AllowAddressablesInit(); return new BootstrapResult(false, false, fetch.Error, null); } Debug.Log($"[Cuong] GameContentBootstrap: Đã có contentVersion={fetch.ContentVersion}" + (string.IsNullOrWhiteSpace(fetch.AssetsBaseUrl) ? "" : $", assetsBaseUrl={fetch.AssetsBaseUrl}")); if (!string.IsNullOrWhiteSpace(fetch.AssetsBaseUrl) && !string.IsNullOrWhiteSpace(_bakedRemoteUrlPrefixForRewrite)) { AddressablesRuntimeUrlRewriter.InstallPrefixRewrite( _bakedRemoteUrlPrefixForRewrite, fetch.AssetsBaseUrl); Debug.Log("[Cuong] GameContentBootstrap: Đã gắn URL rewrite cho remote CDN."); } AllowAddressablesInit(); Debug.Log("[Cuong] GameContentBootstrap: Đang khởi tạo Addressables..."); await AddressablesCatalogUpdater.EnsureInitializedAsync(); Debug.Log("[Cuong] GameContentBootstrap: Addressables đã khởi tạo."); bool firstRun = PlayerPrefs.GetInt(PrefsFirstSyncDone, 0) == 0; string localVersion = PlayerPrefs.GetString(PrefsLastContentVersion, string.Empty); bool needContentWork = firstRun || !string.Equals(localVersion, fetch.ContentVersion ?? string.Empty, StringComparison.Ordinal); if (!needContentWork) { BMLogger.Log("[Cuong] GameContentBootstrap: contentVersion unchanged, skip catalog/bulk download."); Debug.Log("[Cuong] GameContentBootstrap: Hoàn tất — không cần tải catalog/bulk (version không đổi)."); return new BootstrapResult(true, false, null, fetch.ContentVersion); } Debug.Log($"[Cuong] GameContentBootstrap: Cần sync nội dung (firstRun={firstRun}, localVersion='{localVersion}', serverVersion='{fetch.ContentVersion}')."); Debug.Log("[Cuong] GameContentBootstrap: Đang kiểm tra / cập nhật catalog..."); var catalog = await AddressablesCatalogUpdater.CheckAndUpdateCatalogsIfNeededAsync(false); if (!catalog.Success) { Debug.LogError($"[Cuong] GameContentBootstrap: Catalog update thất bại — {catalog.Error?.Message}"); return new BootstrapResult(false, true, catalog.Error?.Message ?? "Catalog update failed", fetch.ContentVersion); } Debug.Log("[Cuong] GameContentBootstrap: Catalog OK."); if (string.IsNullOrWhiteSpace(_remoteBulkDownloadLabel)) { BMLogger.LogError("[Cuong] GameContentBootstrap: _remoteBulkDownloadLabel is empty; skipping bulk download."); Debug.LogError("[Cuong] GameContentBootstrap: _remoteBulkDownloadLabel trống — bỏ bulk download."); return new BootstrapResult(false, true, "remoteBulkDownloadLabel empty", fetch.ContentVersion); } Debug.Log($"[Cuong] GameContentBootstrap: Đang tải toàn bộ remote content (label={_remoteBulkDownloadLabel})..."); bool dl = await AddressablesCatalogUpdater.DownloadDependenciesAsync(_remoteBulkDownloadLabel.Trim(), true); if (!dl) { Debug.LogError("[Cuong] GameContentBootstrap: Tải remote content thất bại."); return new BootstrapResult(false, true, "DownloadDependencies failed", fetch.ContentVersion); } PlayerPrefs.SetString(PrefsLastContentVersion, fetch.ContentVersion); PlayerPrefs.SetInt(PrefsFirstSyncDone, 1); PlayerPrefs.Save(); BMLogger.Log($"[Cuong] GameContentBootstrap: content sync OK, version={fetch.ContentVersion}"); Debug.Log($"[Cuong] GameContentBootstrap: Bootstrap hoàn tất — đã tải xong hết nội dung (version={fetch.ContentVersion})."); return new BootstrapResult(true, true, null, fetch.ContentVersion); } catch (Exception e) { BMLogger.LogError($"[Cuong] GameContentBootstrap: {e.Message}"); Debug.LogError($"[Cuong] GameContentBootstrap: Exception — {e.Message}"); AllowAddressablesInit(); return new BootstrapResult(false, true, e.Message, null); } } static void AllowAddressablesInit() { if (s_addressablesInitGate != null) { s_addressablesInitGate.TrySetResult(); s_addressablesInitGate = null; } } /// /// Editor skip → hardcode / fake; không server → hardcode; bật server → (GET hoặc POST). /// async UniTask ResolveVersionFetchAsync() { #if UNITY_EDITOR if (_skipRemoteCallInEditor) { BMLogger.Log("[Cuong] GameContentBootstrap: Editor skip remote — no HTTP."); return BuildHardcodedOrEditorResult(); } #endif if (!_useServerForVersionInfo) { BMLogger.Log("[Cuong] GameContentBootstrap: server disabled — using hardcoded version / URL."); return BuildHardcodedResult(); } if (string.IsNullOrWhiteSpace(_versionEndpointUrl)) { BMLogger.LogWarning("[Cuong] GameContentBootstrap: server enabled but URL empty; falling back to hardcoded."); return BuildHardcodedResult(); } return await GameContentVersionServerClient.FetchAsync( _versionEndpointUrl, _requestTimeoutSeconds, _usePostForVersionRequest, _versionPostBody); } GameContentVersionFetchResult BuildHardcodedOrEditorResult() { if (!string.IsNullOrWhiteSpace(_hardcodedContentVersion)) { return new GameContentVersionFetchResult( true, _hardcodedContentVersion.Trim(), TrimOrNull(_hardcodedAssetsBaseUrl), null); } return new GameContentVersionFetchResult( true, _editorFakeContentVersion?.Trim() ?? "editor", TrimOrNull(_hardcodedAssetsBaseUrl), null); } GameContentVersionFetchResult BuildHardcodedResult() { if (string.IsNullOrWhiteSpace(_hardcodedContentVersion)) { BMLogger.LogError("[Cuong] GameContentBootstrap: _hardcodedContentVersion is empty."); return new GameContentVersionFetchResult(false, null, null, "hardcoded contentVersion empty"); } return new GameContentVersionFetchResult( true, _hardcodedContentVersion.Trim(), TrimOrNull(_hardcodedAssetsBaseUrl), null); } static string TrimOrNull(string s) { if (string.IsNullOrWhiteSpace(s)) return null; return s.Trim(); } 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; } } } }