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