336 lines
11 KiB
C#
336 lines
11 KiB
C#
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();
|
|
}
|
|
|
|
/// <summary>
|
|
/// Fully tears down Mirror before loading another scene. Required for tutorial host mode;
|
|
/// <see cref="NetworkManager.StopClient"/> alone leaves the server running.
|
|
/// </summary>
|
|
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)
|
|
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;
|
|
|
|
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);
|
|
}
|
|
|
|
/// <summary>
|
|
/// Policy checks that are safe during Mirror's disconnect callback
|
|
/// (when <see cref="NetworkClient.isConnected"/> may still be true).
|
|
/// </summary>
|
|
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;
|
|
}
|
|
|
|
}
|