245 lines
9.5 KiB
C#
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; }
|
|
}
|
|
}
|
|
}
|