Files
test/Assets/PerfectWorld/Scripts/Addressable/GameContentBootstrap.cs
T
2026-05-20 17:23:08 +07:00

309 lines
13 KiB
C#

using System;
using BrewMonster;
using Cysharp.Threading.Tasks;
using UnityEngine;
namespace BrewMonster.Scripts
{
/// <summary>
/// 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.
/// </summary>
[DefaultExecutionOrder(-2000)]
public class GameContentBootstrap : MonoBehaviour
{
const string PrefsFirstSyncDone = "PW_GameContent_FirstRemoteSyncDone";
const string PrefsLastContentVersion = "PW_GameContent_LastContentVersion";
/// <summary>
/// When non-null, <see cref="AddressableManager"/> waits on this before <see cref="UnityEngine.AddressableAssets.Addressables.InitializeAsync"/>.
/// </summary>
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<BootstrapResult> 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<BootstrapResult> 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;
}
}
/// <summary>
/// Editor skip → hardcode / fake; không server → hardcode; bật server → <see cref="GameContentVersionServerClient.FetchAsync"/> (GET hoặc POST).
/// </summary>
async UniTask<GameContentVersionFetchResult> 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; }
/// <summary>True if catalog/bulk download ran this session.</summary>
public bool DidContentWork { get; }
public string ErrorMessage { get; }
public string ServerContentVersion { get; }
}
}
}