diff --git a/Assets/PerfectWorld/Scene/Bootstrap.unity b/Assets/PerfectWorld/Scene/Bootstrap.unity index 0d8f6e97cc..bc0fc63f29 100644 --- a/Assets/PerfectWorld/Scene/Bootstrap.unity +++ b/Assets/PerfectWorld/Scene/Bootstrap.unity @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:59a38344245b9d816e9ee3749664edf1ced3bb23a176c93562f0e7ef78ac22aa -size 311690 +oid sha256:118edff57ce13d46edc50acb1387bca1d79d66795958be304ef1e9d017cff634 +size 309966 diff --git a/Assets/PerfectWorld/Scene/LoadScene.unity b/Assets/PerfectWorld/Scene/LoadScene.unity new file mode 100644 index 0000000000..946cfa3490 --- /dev/null +++ b/Assets/PerfectWorld/Scene/LoadScene.unity @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:a5ca144887d8b4ad62b05ed4ee96ef2ff817460dd87d9a70a30ca6a1f750fe30 +size 5293 diff --git a/Assets/PerfectWorld/Scene/LoadScene.unity.meta b/Assets/PerfectWorld/Scene/LoadScene.unity.meta new file mode 100644 index 0000000000..3b2f468b92 --- /dev/null +++ b/Assets/PerfectWorld/Scene/LoadScene.unity.meta @@ -0,0 +1,7 @@ +fileFormatVersion: 2 +guid: 7231194cdbf312f4ea3bc0583146e93f +DefaultImporter: + externalObjects: {} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Assets/PerfectWorld/Scripts/Addressable/AddressableManager.cs b/Assets/PerfectWorld/Scripts/Addressable/AddressableManager.cs index 2d0c5e27f5..fa06c8a515 100644 --- a/Assets/PerfectWorld/Scripts/Addressable/AddressableManager.cs +++ b/Assets/PerfectWorld/Scripts/Addressable/AddressableManager.cs @@ -13,7 +13,10 @@ using UnityEngine.U2D; namespace BrewMonster.Scripts { - /// Runs after (-2000) so bootstrap Awake creates the gate first. + /// + /// Scene game (Bootstrap): chờ gate chỉ khi cùng scene. + /// Nếu đã chạy scene GameContentBootstrap trước → . + /// [DefaultExecutionOrder(-1990)] public class AddressableManager : MonoSingleton { @@ -85,20 +88,28 @@ namespace BrewMonster.Scripts /// async UniTaskVoid StartAddressablesInitAfterBootstrapGate() { - var gateState = GameContentBootstrap.GetGateDebugState(); - Debug.Log( - $"[Cuong] AddressableManager: Đang chờ GameContentBootstrap (version / URL rewrite)... | " + - $"id={GetInstanceID()} scene={SceneManager.GetActiveScene().name} gate={gateState}"); - - var waited = await WaitForBootstrapGateWithTimeoutAsync(); - if (!waited) + if (GameContentBootstrapSession.IsContentReady || AddressablesInitService.IsInitialized) { - Debug.LogWarning( - $"[Cuong] AddressableManager: Bootstrap gate timeout ({_bootstrapGateWaitTimeoutSeconds:F0}s) — " + - "InitializeAsync anyway. Check GameContentBootstrap lifecycle logs."); + Debug.Log( + $"[Cuong] AddressableManager: Content bootstrap đã chạy ở scene trước — init Addressables (scene={SceneManager.GetActiveScene().name})."); + } + else + { + var gateState = GameContentBootstrap.GetGateDebugState(); + Debug.Log( + $"[Cuong] AddressableManager: Đang chờ GameContentBootstrap (version / URL rewrite)... | " + + $"id={GetInstanceID()} scene={SceneManager.GetActiveScene().name} gate={gateState}"); + + var waited = await WaitForBootstrapGateWithTimeoutAsync(); + if (!waited) + { + Debug.LogWarning( + $"[Cuong] AddressableManager: Bootstrap gate timeout ({_bootstrapGateWaitTimeoutSeconds:F0}s) — " + + "InitializeAsync anyway. Nên dùng scene GameContentBootstrap riêng (index 0)."); + } } - Debug.Log("[Cuong] AddressableManager: Bootstrap gate xong — đang InitializeAsync Addressables..."); + Debug.Log("[Cuong] AddressableManager: Đang InitializeAsync Addressables..."); try { await AddressablesInitService.EnsureInitializedAsync(); diff --git a/Assets/PerfectWorld/Scripts/Addressable/AddressablesInitService.cs b/Assets/PerfectWorld/Scripts/Addressable/AddressablesInitService.cs index 9b449f590d..64502ef6fa 100644 --- a/Assets/PerfectWorld/Scripts/Addressable/AddressablesInitService.cs +++ b/Assets/PerfectWorld/Scripts/Addressable/AddressablesInitService.cs @@ -52,8 +52,12 @@ namespace BrewMonster.Scripts try { - await GameContentBootstrap.WaitForPreAddressablesSetupIfAnyAsync(); - Debug.Log("[Cuong] AddressablesInitService: Bootstrap gate OK — InitializeAsync..."); + if (!GameContentBootstrapSession.IsContentReady) + await GameContentBootstrap.WaitForPreAddressablesSetupIfAnyAsync(); + else + Debug.Log("[Cuong] AddressablesInitService: Content scene đã sync — bỏ chờ gate."); + + Debug.Log("[Cuong] AddressablesInitService: InitializeAsync..."); var handle = Addressables.InitializeAsync(); await handle.ToUniTask(); diff --git a/Assets/PerfectWorld/Scripts/Addressable/AddressablesInitService.cs.meta b/Assets/PerfectWorld/Scripts/Addressable/AddressablesInitService.cs.meta new file mode 100644 index 0000000000..e5e8526bdd --- /dev/null +++ b/Assets/PerfectWorld/Scripts/Addressable/AddressablesInitService.cs.meta @@ -0,0 +1,2 @@ +fileFormatVersion: 2 +guid: f5cbe16c40166b34c9a3f9e967dfc694 \ No newline at end of file diff --git a/Assets/PerfectWorld/Scripts/Addressable/GameContentBootstrap.cs b/Assets/PerfectWorld/Scripts/Addressable/GameContentBootstrap.cs index de2cdf60c3..507726ab38 100644 --- a/Assets/PerfectWorld/Scripts/Addressable/GameContentBootstrap.cs +++ b/Assets/PerfectWorld/Scripts/Addressable/GameContentBootstrap.cs @@ -8,10 +8,8 @@ using UnityEngine.SceneManagement; 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. + /// 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 @@ -87,6 +85,19 @@ namespace BrewMonster.Scripts [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ừ . @@ -110,6 +121,8 @@ namespace BrewMonster.Scripts void Awake() { + GameContentBootstrapSession.ResetForNewRun(); + s_bootstrapRunStarted = false; s_activeBootstrapInstanceId = GetInstanceID(); _holdAddressablesInitConfigured = _holdAddressablesInitUntilVersionChecked; LogLifecycle("Awake"); @@ -209,7 +222,32 @@ namespace BrewMonster.Scripts 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() diff --git a/Assets/PerfectWorld/Scripts/Addressable/GameContentBootstrapSession.cs b/Assets/PerfectWorld/Scripts/Addressable/GameContentBootstrapSession.cs new file mode 100644 index 0000000000..06f4fe231b --- /dev/null +++ b/Assets/PerfectWorld/Scripts/Addressable/GameContentBootstrapSession.cs @@ -0,0 +1,21 @@ +namespace BrewMonster.Scripts +{ + /// + /// Trạng thái phiên sau khi scene GameContentBootstrap hoàn tất (Addressables + catalog/bulk nếu cần). + /// Scene game (Bootstrap) đọc flag này — không cần component trong scene đó. + /// + public static class GameContentBootstrapSession + { + public static bool IsContentReady { get; private set; } + + public static void MarkContentReady() + { + IsContentReady = true; + } + + public static void ResetForNewRun() + { + IsContentReady = false; + } + } +} diff --git a/Assets/PerfectWorld/Scripts/Addressable/GameContentBootstrapSession.cs.meta b/Assets/PerfectWorld/Scripts/Addressable/GameContentBootstrapSession.cs.meta new file mode 100644 index 0000000000..3478e7f0fa --- /dev/null +++ b/Assets/PerfectWorld/Scripts/Addressable/GameContentBootstrapSession.cs.meta @@ -0,0 +1,2 @@ +fileFormatVersion: 2 +guid: 1c7e6838c2213a9478bf29bb3f248949 \ No newline at end of file diff --git a/Assets/PerfectWorld/Scripts/Editor/GameContentBootstrapSceneSetup.cs b/Assets/PerfectWorld/Scripts/Editor/GameContentBootstrapSceneSetup.cs new file mode 100644 index 0000000000..d46b021138 --- /dev/null +++ b/Assets/PerfectWorld/Scripts/Editor/GameContentBootstrapSceneSetup.cs @@ -0,0 +1,95 @@ +#if UNITY_EDITOR +using BrewMonster.Scripts; +using UnityEditor; +using UnityEditor.SceneManagement; +using UnityEngine; +using UnityEngine.SceneManagement; + +namespace BrewMonster.Editor +{ + /// + /// Tạo scene GameContentBootstrap và đưa lên đầu Build Settings (trước Bootstrap). + /// + public static class GameContentBootstrapSceneSetup + { + const string ContentScenePath = "Assets/PerfectWorld/Scene/GameContentBootstrap.unity"; + const string BootstrapScenePath = "Assets/PerfectWorld/Scene/Bootstrap.unity"; + + [MenuItem("Perfect World/Addressables/Setup Two-Scene Bootstrap")] + public static void SetupTwoSceneBootstrap() + { + EnsureContentBootstrapScene(); + RemoveGameContentBootstrapFromBootstrapScene(); + ReorderBuildSettings(); + AssetDatabase.SaveAssets(); + Debug.Log( + "[Cuong] Two-scene bootstrap: (0) GameContentBootstrap → (1) Bootstrap. " + + "Gỡ GameContentBootstrap khỏi Bootstrap.unity; cấu hình _nextSceneName trên component trong scene Content."); + } + + static void EnsureContentBootstrapScene() + { + if (System.IO.File.Exists(ContentScenePath)) + return; + + var scene = EditorSceneManager.NewScene(NewSceneSetup.DefaultGameObjects, NewSceneMode.Single); + var root = new GameObject("GameContentBootstrap"); + var bootstrap = root.AddComponent(); + + var so = new SerializedObject(bootstrap); + so.FindProperty("_loadNextSceneAfterSuccess").boolValue = true; + so.FindProperty("_nextSceneName").stringValue = "Bootstrap"; + so.ApplyModifiedPropertiesWithoutUndo(); + + EditorSceneManager.SaveScene(scene, ContentScenePath); + Debug.Log($"[Cuong] Created {ContentScenePath}"); + } + + static void RemoveGameContentBootstrapFromBootstrapScene() + { + if (!System.IO.File.Exists(BootstrapScenePath)) + { + Debug.LogWarning($"[Cuong] Missing {BootstrapScenePath}"); + return; + } + + var scene = EditorSceneManager.OpenScene(BootstrapScenePath, OpenSceneMode.Single); + var targets = Object.FindObjectsByType(FindObjectsInactive.Include, FindObjectsSortMode.None); + if (targets == null || targets.Length == 0) + { + Debug.Log("[Cuong] Bootstrap.unity: no GameContentBootstrap found (already clean)."); + return; + } + + foreach (var t in targets) + Object.DestroyImmediate(t.gameObject); + + EditorSceneManager.MarkSceneDirty(scene); + EditorSceneManager.SaveScene(scene); + } + + static void ReorderBuildSettings() + { + var scenes = new System.Collections.Generic.List(EditorBuildSettings.scenes); + EditorBuildSettingsScene contentEntry = null; + for (int i = scenes.Count - 1; i >= 0; i--) + { + if (scenes[i].path == ContentScenePath) + { + contentEntry = scenes[i]; + scenes.RemoveAt(i); + break; + } + } + + if (contentEntry == null) + contentEntry = new EditorBuildSettingsScene(ContentScenePath, true); + + contentEntry.enabled = true; + scenes.Insert(0, contentEntry); + EditorBuildSettings.scenes = scenes.ToArray(); + Debug.Log("[Cuong] Build Settings: GameContentBootstrap is index 0."); + } + } +} +#endif diff --git a/Assets/PerfectWorld/Scripts/Editor/GameContentBootstrapSceneSetup.cs.meta b/Assets/PerfectWorld/Scripts/Editor/GameContentBootstrapSceneSetup.cs.meta new file mode 100644 index 0000000000..efce950673 --- /dev/null +++ b/Assets/PerfectWorld/Scripts/Editor/GameContentBootstrapSceneSetup.cs.meta @@ -0,0 +1,2 @@ +fileFormatVersion: 2 +guid: 3a3ab2943a6c3ab44aee0633dc42a560 \ No newline at end of file