using System;
using System.Threading;
using BrewMonster;
using Cysharp.Threading.Tasks;
using UnityEngine;
using UnityEngine.SceneManagement;
namespace BrewMonster.Scripts
{
///
/// Scene riêng (khuyến nghị index 0 trong Build Settings): version + URL rewrite → Addressables init → catalog/bulk.
/// Khi xong, load scene game (, thường Bootstrap) — scene đó không cần component này.
///
[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;
static int s_activeBootstrapInstanceId;
static bool s_bootstrapRunStarted;
[Header("Debug")]
[Tooltip("Log lifecycle (Awake/Start/OnDestroy), gate state, scene name — dùng khi debug mobile.")]
[SerializeField]
bool _verboseLifecycleDebug = true;
[Tooltip("Nếu gate chưa mở sau N giây, tự release để AddressableManager không treo vĩnh viễn.")]
[SerializeField]
float _gateReleaseTimeoutSeconds = 45f;
[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";
[Header("Scene flow (dedicated content scene)")]
[Tooltip("Bật: sau khi sync content thành công, LoadScene Single sang scene game. Tắt: chỉ chạy sync (dùng khi gắn chung scene Bootstrap).")]
[SerializeField]
bool _loadNextSceneAfterSuccess = true;
[Tooltip("Tên scene trong Build Settings (vd Bootstrap). BootstrapSceneController trong scene đó sẽ load LoginScene.")]
[SerializeField]
string _nextSceneName = "Bootstrap";
[Tooltip("Khi sync thất bại, không load scene game (ở lại scene content để xử lý / retry).")]
[SerializeField]
bool _stayOnSceneWhenSyncFails = true;
public event Action Finished;
/// Trạng thái gate tĩnh — dùng log chẩn đoán từ .
public static string GetGateDebugState()
{
if (!_holdAddressablesInitConfigured)
return "holdAddressablesInit=false (no gate)";
if (s_addressablesInitGate == null)
return "gate=null (released or not created)";
return $"gate=pending, activeInstanceId={s_activeBootstrapInstanceId}, runStarted={s_bootstrapRunStarted}";
}
static bool _holdAddressablesInitConfigured = true;
public static UniTask WaitForPreAddressablesSetupIfAnyAsync(CancellationToken cancellationToken = default)
{
if (s_addressablesInitGate == null)
return UniTask.CompletedTask;
return s_addressablesInitGate.Task.AttachExternalCancellation(cancellationToken);
}
void Awake()
{
GameContentBootstrapSession.ResetForNewRun();
s_bootstrapRunStarted = false;
s_activeBootstrapInstanceId = GetInstanceID();
_holdAddressablesInitConfigured = _holdAddressablesInitUntilVersionChecked;
LogLifecycle("Awake");
if (_holdAddressablesInitUntilVersionChecked)
{
s_addressablesInitGate ??= new UniTaskCompletionSource();
GateTimeoutWatchdog().Forget();
}
else
LogLifecycle("Awake: holdAddressablesInit=false — AddressableManager will not wait on gate.");
TryStartBootstrapRun("Awake");
}
void Start()
{
LogLifecycle("Start");
TryStartBootstrapRun("Start");
}
void OnDestroy()
{
LogLifecycle("OnDestroy — releasing gate if still pending");
ReleaseGateIfPending("OnDestroy");
if (s_activeBootstrapInstanceId == GetInstanceID())
s_activeBootstrapInstanceId = 0;
}
void OnApplicationQuit()
{
LogLifecycle("OnApplicationQuit");
ReleaseGateIfPending("OnApplicationQuit");
}
void TryStartBootstrapRun(string caller)
{
if (!isActiveAndEnabled)
{
LogLifecycle($"{caller}: SKIP bootstrap — GameObject inactive or component disabled.");
return;
}
if (s_bootstrapRunStarted)
{
LogLifecycle($"{caller}: bootstrap already started (skip duplicate).");
return;
}
s_bootstrapRunStarted = true;
LogLifecycle($"{caller}: starting RunAsync.");
RunAsync().Forget();
}
async UniTaskVoid GateTimeoutWatchdog()
{
var timeout = Mathf.Max(5f, _gateReleaseTimeoutSeconds);
await UniTask.Delay(TimeSpan.FromSeconds(timeout));
if (s_addressablesInitGate == null)
return;
Debug.LogWarning(
$"[Cuong] GameContentBootstrap: Gate timeout ({timeout:F0}s) — releasing gate. " +
$"runStarted={s_bootstrapRunStarted}, scene={SceneManager.GetActiveScene().name}, {DescribeSelf()}");
AllowAddressablesInit("GateTimeoutWatchdog");
}
static void ReleaseGateIfPending(string reason)
{
if (s_addressablesInitGate == null)
return;
Debug.LogWarning(
$"[Cuong] GameContentBootstrap: ReleaseGate ({reason}) — gate opened without normal bootstrap completion.");
s_addressablesInitGate.TrySetResult();
s_addressablesInitGate = null;
}
void LogLifecycle(string phase)
{
if (!_verboseLifecycleDebug)
return;
Debug.Log(
$"[Cuong] GameContentBootstrap: {phase} | {DescribeSelf()} | gate={GetGateDebugState()} | " +
$"scene={SceneManager.GetActiveScene().name} frame={Time.frameCount}");
}
string DescribeSelf()
{
return
$"id={GetInstanceID()} active={gameObject.activeInHierarchy} enabled={enabled} " +
$"holdInit={_holdAddressablesInitUntilVersionChecked} server={_useServerForVersionInfo}";
}
async UniTaskVoid RunAsync()
{
var result = await RunInternalAsync();
if (result.Success)
GameContentBootstrapSession.MarkContentReady();
Finished?.Invoke(result);
await HandleSceneFlowAfterBootstrapAsync(result);
}
async UniTask HandleSceneFlowAfterBootstrapAsync(BootstrapResult result)
{
if (!result.Success)
{
if (_stayOnSceneWhenSyncFails)
Debug.LogError("[Cuong] GameContentBootstrap: Sync thất bại — ở lại scene content (không load game).");
return;
}
if (!_loadNextSceneAfterSuccess || string.IsNullOrWhiteSpace(_nextSceneName))
{
Debug.Log("[Cuong] GameContentBootstrap: Sync OK — không load scene tiếp (_loadNextSceneAfterSuccess tắt hoặc tên scene trống).");
return;
}
var next = _nextSceneName.Trim();
await UniTask.Yield();
Debug.Log($"[Cuong] GameContentBootstrap: Sync OK — LoadScene '{next}' (Single)...");
SceneManager.LoadScene(next, LoadSceneMode.Single);
}
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("version-fetch-failed");
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: URL rewrite | from={_bakedRemoteUrlPrefixForRewrite.Trim()} → to={fetch.AssetsBaseUrl.Trim()}");
}
else if (!string.IsNullOrWhiteSpace(fetch.AssetsBaseUrl) &&
string.IsNullOrWhiteSpace(_bakedRemoteUrlPrefixForRewrite))
{
Debug.LogWarning(
"[Cuong] GameContentBootstrap: assetsBaseUrl có nhưng _bakedRemoteUrlPrefixForRewrite trống — " +
"catalog/bundle vẫn dùng URL bake trong build (vd CDN cũ). Điền prefix URL lúc build.");
}
AllowAddressablesInit("version-and-url-ready");
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)
{
var errMsg = catalog.Error?.Message ?? "Catalog update failed";
Debug.LogError($"[Cuong] GameContentBootstrap: Catalog update thất bại — {errMsg}");
if (IsLikelyRemoteTlsOrNetworkError(errMsg))
{
Debug.LogError(
"[Cuong] GameContentBootstrap: Gợi ý — SSL CA / mạng trên mobile. " +
"Kiểm tra chuỗi chứng chỉ CDN; đảm bảo _bakedRemoteUrlPrefixForRewrite khớp host trong catalog build " +
"(vd https://prefect-world-asset....wcsapi.com/) và assetsBaseUrl trỏ CDN HTTPS hợp lệ. " +
"Không gọi Addressables.InitializeAsync trước bootstrap (AUIManager đã dùng AddressablesInitService).");
}
return new BootstrapResult(false, true, errMsg, 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);
}
var bulkLabel = _remoteBulkDownloadLabel.Trim();
var downloadBytes = await AddressablesCatalogUpdater.GetDownloadSizeBytesAsync(bulkLabel);
if (downloadBytes > 0)
{
Debug.Log(
$"[Cuong] GameContentBootstrap: Đang tải remote content (label={bulkLabel}), dung lượng cần tải ~{downloadBytes / (1024f * 1024f):F1} MB...");
}
else if (downloadBytes == 0)
{
Debug.Log(
$"[Cuong] GameContentBootstrap: Remote content (label={bulkLabel}) đã có trong cache — vẫn chạy DownloadDependencies để đồng bộ.");
}
else
{
Debug.Log(
$"[Cuong] GameContentBootstrap: Đang tải remote content (label={bulkLabel}) — không lấy được dung lượng trước khi tải.");
}
bool dl = await AddressablesCatalogUpdater.DownloadDependenciesAsync(bulkLabel);
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("exception");
return new BootstrapResult(false, true, e.Message, null);
}
}
static void AllowAddressablesInit(string reason)
{
if (s_addressablesInitGate == null)
{
Debug.Log($"[Cuong] GameContentBootstrap: AllowAddressablesInit({reason}) — gate already null.");
return;
}
Debug.Log($"[Cuong] GameContentBootstrap: AllowAddressablesInit({reason}) — opening gate for AddressableManager.");
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();
}
static bool IsLikelyRemoteTlsOrNetworkError(string message)
{
if (string.IsNullOrEmpty(message))
return false;
return message.IndexOf("SSL", StringComparison.OrdinalIgnoreCase) >= 0 ||
message.IndexOf("certificate", StringComparison.OrdinalIgnoreCase) >= 0 ||
message.IndexOf("ConnectionError", StringComparison.OrdinalIgnoreCase) >= 0 ||
message.IndexOf("unable to load from url", StringComparison.OrdinalIgnoreCase) >= 0;
}
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; }
}
}
}