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;
}
}