// Assets/_Project/Scripts/Net/SessionFlow.cs using Unity.Netcode; using UnityEngine; using UnityEngine.SceneManagement; namespace TD.Net { /// /// Bootstrap script — drop one on a GameObject in the MainMenu scene so it /// runs once when the app boots. Subscribes to NGO disconnect callbacks /// and routes the local peer back to the MainMenu scene whenever its /// connection ends (host left, server kicked it, transport error, etc.). /// /// /// Why this exists. Per Option A (see Project_Roadmap.md §1.7), /// host disconnect tears down the session for everyone. Each client needs /// to recover gracefully and return to its own main menu. Without this, /// a client would be stuck in the Lobby or Match scene with no working /// network connection after the host quits. /// /// Lifecycle. The NetworkManager itself is marked /// DontDestroyOnLoad by NGO once it spawns. This script is also marked /// DontDestroyOnLoad so it survives scene transitions and its subscription /// stays alive across MainMenu → Lobby → Match → back. /// /// Server side. When the host (server) shuts down, the /// OnServerStopped callback fires on the host too. For the host /// that's fine — they've initiated the shutdown deliberately (e.g., from /// the "Return to Main Menu" button) and being sent to MainMenu is what /// they wanted. /// public class SessionFlow : MonoBehaviour { // ----- Singleton -------------------------------------------------- public static SessionFlow Instance { get; private set; } private void Awake() { if (Instance != null && Instance != this) { Destroy(gameObject); return; } Instance = this; DontDestroyOnLoad(gameObject); } private void Start() { HookCallbacks(); } private void OnDestroy() { UnhookCallbacks(); if (Instance == this) Instance = null; } // ----- NGO callback wiring --------------------------------------- private bool callbacksHooked; private void HookCallbacks() { var nm = NetworkManager.Singleton; if (nm == null) { // NetworkManager may not be alive yet on the very first frame // depending on script execution order — retry next frame. Invoke(nameof(HookCallbacks), 0.1f); return; } if (callbacksHooked) return; nm.OnClientDisconnectCallback += HandleClientDisconnect; nm.OnServerStopped += HandleServerStopped; callbacksHooked = true; } private void UnhookCallbacks() { if (!callbacksHooked) return; var nm = NetworkManager.Singleton; if (nm != null) { nm.OnClientDisconnectCallback -= HandleClientDisconnect; nm.OnServerStopped -= HandleServerStopped; } callbacksHooked = false; } // ----- Disconnect handlers --------------------------------------- // Fires when ANY client disconnects on the server, and when the LOCAL // client gets disconnected on a client. We only care about the latter: // when the local peer loses its connection (host left, transport error, // explicit Shutdown call), route back to the main menu. private void HandleClientDisconnect(ulong clientId) { var nm = NetworkManager.Singleton; if (nm == null) return; // On the server, this fires when other clients disconnect — ignore // those, the server keeps running. We only act when the LOCAL // client's connection ends. if (clientId != nm.LocalClientId) return; ReturnToMainMenu(); } // Fires on the server when the server shuts down (including the host). // The host's own client also gets HandleClientDisconnect — but // OnServerStopped is the canonical "server is gone" signal and is // safer to act on for the host path. private void HandleServerStopped(bool isHost) { ReturnToMainMenu(); } // ----- Scene transition ------------------------------------------ // Returns the local peer to the MainMenu scene if they're not already // there. Uses Unity's local SceneManager (NOT NGO's networked scene // manager) because by the time this fires the network connection is // already gone. private static void ReturnToMainMenu() { if (SceneManager.GetActiveScene().name == SceneNames.MainMenu) return; Debug.Log("[SessionFlow] Connection ended — returning to main menu."); SceneManager.LoadScene(SceneNames.MainMenu); } } }