Files
test/Assets/PerfectWorld/Scripts/Addressable/GameContentBootstrap.cs
T
2026-05-13 15:33:35 +07:00

245 lines
9.5 KiB
C#

using System;
using BrewMonster;
using Cysharp.Threading.Tasks;
using UnityEngine;
using UnityEngine.Networking;
namespace BrewMonster.Scripts
{
/// <summary>
/// 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.
/// </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 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<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
{
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<VersionDtoResult> 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<VersionResponseJson>(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; }
/// <summary>True if catalog/bulk download ran this session.</summary>
public bool DidContentWork { get; }
public string ErrorMessage { get; }
public string ServerContentVersion { get; }
}
}
}