using System; using System.Collections; using UnityEngine; using Mirror; using UnityEngine.Networking; public class NetManager : NetworkManager { const float InitialReconnectDelaySeconds = 2f; const float ReopenTriggerDelaySeconds = 15f; [SerializeField] float reconnectIntervalSeconds = 15f; Coroutine reconnectCoroutine; bool reopenRequestedThisDisconnect; bool reopenRequestInFlight; [SerializeField] private CanvasGroup reconnectCanvasGroup; static bool s_IntentionalClientDisconnect; public static NetManager instance; public override void Awake() { base.Awake(); instance = this; Transport.active.OnClientError += OnTransportClientError; } void OnDestroy() { if (Transport.active != null) Transport.active.OnClientError -= OnTransportClientError; if (NetworkServer.active || NetworkClient.active) StopNetworkingForSceneChange(); } /// /// Fully tears down Mirror before loading another scene. Required for tutorial host mode; /// alone leaves the server running. /// public static void StopNetworkingForSceneChange() { MarkIntentionalClientDisconnect(); NetworkManager manager = singleton != null ? singleton : NetworkManager.singleton; if (manager != null) { if (NetworkServer.active && NetworkClient.active) manager.StopHost(); else if (NetworkClient.active) manager.StopClient(); else if (NetworkServer.active) manager.StopServer(); return; } if (NetworkClient.active) NetworkClient.Shutdown(); if (NetworkServer.active) NetworkServer.Shutdown(); } static void OnTransportClientError(TransportError error, string reason) { Logger.Log($"Transport client error ({error}): {reason}"); } public override void OnStartServer() { base.OnStartServer(); Application.logMessageReceived += ForwardMirrorServerLogToLogger; Logger.Log("Mirror server exception logging enabled"); } public override void OnServerError(NetworkConnectionToClient conn, TransportError error, string reason) { int connId = conn != null ? conn.connectionId : -1; Logger.Log($"Mirror server transport error (connId={connId}, {error}): {reason}"); base.OnServerError(conn, error, reason); } public override void OnServerTransportException(NetworkConnectionToClient conn, Exception exception) { int connId = conn != null ? conn.connectionId : -1; Logger.Log($"Mirror server transport exception (connId={connId}): {exception}"); base.OnServerTransportException(conn, exception); } public override void OnServerDisconnect(NetworkConnectionToClient conn) { if (conn != null && conn.identity != null) { var netPlayer = conn.identity.GetComponent(); if (netPlayer != null && GameManager.instance != null) GameManager.instance.OnDedicatedMatchPlayerDisconnected(netPlayer.myTeam); } if (conn != null) Logger.Log($"Mirror server: client disconnected (connId={conn.connectionId})"); base.OnServerDisconnect(conn); } static void ForwardMirrorServerLogToLogger(string logString, string stackTrace, LogType type) { if (!NetworkServer.active) return; if (!ShouldForwardMirrorServerLog(logString, stackTrace, type)) return; if (string.IsNullOrEmpty(stackTrace)) Logger.Log($"[Mirror/{type}] {logString}"); else Logger.Log($"[Mirror/{type}] {logString}\n{stackTrace}"); } static bool ShouldForwardMirrorServerLog(string message, string stackTrace, LogType type) { if (type == LogType.Exception) return ContainsMirrorNetworkContext(message, stackTrace); if (type != LogType.Error) return false; return message.Contains("Disconnecting connection:") || message.Contains("NetworkServer:") || message.Contains("because handling a message") || message.Contains("because reading a message") || message.Contains("failed to unpack and invoke") || message.Contains("was too short (messages should start with message id)"); } static bool ContainsMirrorNetworkContext(string message, string stackTrace) { return (message != null && (message.Contains("Mirror") || message.Contains("NetworkServer"))) || (stackTrace != null && stackTrace.Contains("Mirror")); } public static void MarkIntentionalClientDisconnect() { s_IntentionalClientDisconnect = true; } public override void OnApplicationQuit() { MarkIntentionalClientDisconnect(); StopReconnectLoop(); base.OnApplicationQuit(); } public override void OnStopServer() { Application.logMessageReceived -= ForwardMirrorServerLogToLogger; ReplayRecorder.StopAndFlush(); GameManager.ReportDedicatedMatchRoomClosed(); base.OnStopServer(); } public override void OnClientConnect() { base.OnClientConnect(); Logger.Log("Client connected"); reopenRequestedThisDisconnect = false; reopenRequestInFlight = false; StopReconnectLoop(); } public override void OnClientDisconnect() { base.OnClientDisconnect(); Logger.Log("Client disconnected"); // Mirror raises OnDisconnectedEvent before clearing ConnectState, so // NetworkClient.isConnected is still true here on involuntary drops. // Gate on policy flags only; teardown wait happens in the reconnect loop. if (CanAttemptReconnect()) { if (reconnectCoroutine == null) reconnectCoroutine = StartCoroutine(ReconnectLoopCoroutine()); else SetReconnectCanvasVisible(true); } else SetReconnectCanvasVisible(false); } public override void OnStopClient() { s_IntentionalClientDisconnect = false; base.OnStopClient(); } void StopReconnectLoop() { if (reconnectCoroutine == null) { SetReconnectCanvasVisible(false); return; } StopCoroutine(reconnectCoroutine); reconnectCoroutine = null; SetReconnectCanvasVisible(false); } void SetReconnectCanvasVisible(bool visible) { if (reconnectCanvasGroup == null) return; reconnectCanvasGroup.alpha = visible ? 1f : 0f; reconnectCanvasGroup.interactable = visible; reconnectCanvasGroup.blocksRaycasts = visible; } IEnumerator ReconnectLoopCoroutine() { SetReconnectCanvasVisible(true); reopenRequestedThisDisconnect = false; reopenRequestInFlight = false; // Wait until Mirror finishes OnClientDisconnectInternal (clears Connected // then Shutdown). Without this, ShouldReconnect() fails immediately. while (NetworkClient.isConnected || NetworkClient.active) yield return null; float reconnectStartedAt = Time.realtimeSinceStartup; float initialWaitEnd = Time.realtimeSinceStartup + InitialReconnectDelaySeconds; while (ShouldReconnect() && Time.realtimeSinceStartup < initialWaitEnd) yield return null; while (ShouldReconnect()) { if (!reopenRequestedThisDisconnect && !reopenRequestInFlight && Time.realtimeSinceStartup - reconnectStartedAt >= ReopenTriggerDelaySeconds) { reopenRequestedThisDisconnect = true; StartCoroutine(CoRequestReopenForCurrentPort()); } if (NetworkClient.active && !NetworkClient.isConnected) { StopClient(); yield return null; continue; } if (!NetworkClient.active) { Logger.Log("Reconnect: attempting StartClient"); StartClient(); } float waitEnd = Time.realtimeSinceStartup + reconnectIntervalSeconds; while (ShouldReconnect() && Time.realtimeSinceStartup < waitEnd) yield return null; } reconnectCoroutine = null; SetReconnectCanvasVisible(false); } /// /// Policy checks that are safe during Mirror's disconnect callback /// (when may still be true). /// bool CanAttemptReconnect() { if (s_IntentionalClientDisconnect) return false; if (NetworkServer.active) return false; if (GameManager.instance != null && GameManager.instance.isGameEnded) return false; return true; } bool ShouldReconnect() { if (!CanAttemptReconnect()) return false; if (NetworkClient.isConnected) return false; return true; } IEnumerator CoRequestReopenForCurrentPort() { reopenRequestInFlight = true; int port = Cupid.RoomPort; if (port <= 0) { Logger.Log("Reconnect: skipping /reopen because room port is invalid"); reopenRequestInFlight = false; yield break; } string baseUri = Cupid.CupidURI; string password = Cupid.SettingsPassword; if (string.IsNullOrWhiteSpace(baseUri) || string.IsNullOrEmpty(password)) { Logger.Log("Reconnect: skipping /reopen because Cupid URI/password is not available"); reopenRequestInFlight = false; yield break; } string query = "?password=" + UnityWebRequest.EscapeURL(password) + "&port=" + port; string[] endpointCandidates = { baseUri.TrimEnd('/') + "/internal/admin/reopen" + query, baseUri.TrimEnd('/') + "/reopen" + query }; bool success = false; for (int i = 0; i < endpointCandidates.Length; i++) { string url = endpointCandidates[i]; using (UnityWebRequest req = UnityWebRequest.Get(url)) { req.downloadHandler = new DownloadHandlerBuffer(); yield return req.SendWebRequest(); string body = req.downloadHandler != null ? req.downloadHandler.text : ""; if (req.result == UnityWebRequest.Result.Success && req.responseCode >= 200 && req.responseCode < 300) { Logger.Log("Reconnect: /reopen success on port " + port + ". Response: " + body); success = true; break; } Logger.Log("Reconnect: /reopen failed at " + url + " status " + req.responseCode + " error: " + req.error + " body: " + body); } } if (!success) Logger.Log("Reconnect: all /reopen endpoint attempts failed."); reopenRequestInFlight = false; } }