|
| 1 | +using System.Collections; |
| 2 | +using System.Collections.Generic; |
| 3 | +using Unity.Collections; |
| 4 | +using Unity.Multiplayer.Samples.BossRoom.Client; |
| 5 | +using Unity.Multiplayer.Samples.BossRoom.Shared.Infrastructure; |
| 6 | +using Unity.Multiplayer.Samples.BossRoom.Shared.Net.UnityServices.Lobbies; |
| 7 | +using Unity.Multiplayer.Samples.Utilities; |
| 8 | +using Unity.Netcode; |
| 9 | +using UnityEngine; |
| 10 | +using UnityEngine.SceneManagement; |
| 11 | + |
| 12 | +namespace Unity.Multiplayer.Samples.BossRoom.Server |
| 13 | +{ |
| 14 | + public class ServerConnectionManager : MonoBehaviour |
| 15 | + { |
| 16 | + [SerializeField] |
| 17 | + NetworkObject m_GameState; |
| 18 | + |
| 19 | + // used in ApprovalCheck. This is intended as a bit of light protection against DOS attacks that rely on sending silly big buffers of garbage. |
| 20 | + const int k_MaxConnectPayload = 1024; |
| 21 | + |
| 22 | + /// <summary> |
| 23 | + /// Keeps a list of what clients are in what scenes. |
| 24 | + /// </summary> |
| 25 | + Dictionary<ulong, int> m_ClientSceneMap = new Dictionary<ulong, int>(); |
| 26 | + |
| 27 | + /// <summary> |
| 28 | + /// The active server scene index. |
| 29 | + /// </summary> |
| 30 | + static int ServerScene => SceneManager.GetActiveScene().buildIndex; |
| 31 | + |
| 32 | + NetworkManager m_NetworkManager; |
| 33 | + LobbyServiceFacade m_LobbyServiceFacade; |
| 34 | + |
| 35 | + IPublisher<ConnectionEventMessage> m_ConnectionEventPublisher; |
| 36 | + |
| 37 | + [Inject] |
| 38 | + void InjectDependencies(LobbyServiceFacade lobbyServiceFacade, IPublisher<ConnectionEventMessage> connectionEventPublisher) |
| 39 | + { |
| 40 | + m_LobbyServiceFacade = lobbyServiceFacade; |
| 41 | + m_ConnectionEventPublisher = connectionEventPublisher; |
| 42 | + } |
| 43 | + |
| 44 | + void Start() |
| 45 | + { |
| 46 | + m_NetworkManager.ConnectionApprovalCallback += ApprovalCheck; |
| 47 | + m_NetworkManager.OnServerStarted += ServerStartedHandler; |
| 48 | + m_NetworkManager.OnClientDisconnectCallback += OnClientDisconnect; |
| 49 | + } |
| 50 | + |
| 51 | + void OnDestroy() |
| 52 | + { |
| 53 | + if (m_NetworkManager != null) |
| 54 | + { |
| 55 | + m_NetworkManager.ConnectionApprovalCallback -= ApprovalCheck; |
| 56 | + m_NetworkManager.OnServerStarted -= ServerStartedHandler; |
| 57 | + m_NetworkManager.OnClientDisconnectCallback -= OnClientDisconnect; |
| 58 | + } |
| 59 | + } |
| 60 | + |
| 61 | + /// <summary> |
| 62 | + /// Handles the case where NetworkManager has told us a client has disconnected. This includes ourselves, if we're the host, |
| 63 | + /// and the server is stopped." |
| 64 | + /// </summary> |
| 65 | + void OnClientDisconnect(ulong clientId) |
| 66 | + { |
| 67 | + m_ClientSceneMap.Remove(clientId); |
| 68 | + |
| 69 | + if (clientId == m_NetworkManager.LocalClientId) |
| 70 | + { |
| 71 | + //the ServerGameNetPortal may be initialized again, which will cause its OnNetworkSpawn to be called again. |
| 72 | + //Consequently we need to unregister anything we registered, when the NetworkManager is shutting down. |
| 73 | + m_NetworkManager.OnClientDisconnectCallback -= OnClientDisconnect; |
| 74 | + if (m_LobbyServiceFacade.CurrentUnityLobby != null) |
| 75 | + { |
| 76 | + m_LobbyServiceFacade.DeleteLobbyAsync(m_LobbyServiceFacade.CurrentUnityLobby.Id); |
| 77 | + } |
| 78 | + SessionManager<SessionPlayerData>.Instance.OnServerEnded(); |
| 79 | + } |
| 80 | + else |
| 81 | + { |
| 82 | + var playerId = SessionManager<SessionPlayerData>.Instance.GetPlayerId(clientId); |
| 83 | + if (playerId != null) |
| 84 | + { |
| 85 | + if (m_LobbyServiceFacade.CurrentUnityLobby != null) |
| 86 | + { |
| 87 | + m_LobbyServiceFacade.RemovePlayerFromLobbyAsync(playerId, m_LobbyServiceFacade.CurrentUnityLobby.Id); |
| 88 | + } |
| 89 | + |
| 90 | + var sessionData = SessionManager<SessionPlayerData>.Instance.GetPlayerData(playerId); |
| 91 | + if (sessionData.HasValue) |
| 92 | + { |
| 93 | + m_ConnectionEventPublisher.Publish(new ConnectionEventMessage() { ConnectStatus = ConnectStatus.GenericDisconnect, PlayerName = sessionData.Value.PlayerName }); |
| 94 | + } |
| 95 | + SessionManager<SessionPlayerData>.Instance.DisconnectClient(clientId); |
| 96 | + } |
| 97 | + } |
| 98 | + } |
| 99 | + |
| 100 | + public void OnClientSceneChanged(ulong clientId, int sceneIndex) |
| 101 | + { |
| 102 | + m_ClientSceneMap[clientId] = sceneIndex; |
| 103 | + } |
| 104 | + |
| 105 | + /// <summary> |
| 106 | + /// Handles the flow when a user has requested a disconnect via UI (which can be invoked on the Host, and thus must be |
| 107 | + /// handled in server code). |
| 108 | + /// </summary> |
| 109 | + public void OnUserDisconnectRequest() |
| 110 | + { |
| 111 | + if (m_NetworkManager.IsHost) |
| 112 | + { |
| 113 | + SendServerToAllClientsSetDisconnectReason(ConnectStatus.HostEndedSession); |
| 114 | + // Wait before shutting down to make sure clients receive that message before they are disconnected |
| 115 | + StartCoroutine(WaitToShutdown()); |
| 116 | + } |
| 117 | + Clear(); |
| 118 | + } |
| 119 | + |
| 120 | + void Clear() |
| 121 | + { |
| 122 | + //resets all our runtime state. |
| 123 | + m_ClientSceneMap.Clear(); |
| 124 | + } |
| 125 | + |
| 126 | + public bool AreAllClientsInServerScene() |
| 127 | + { |
| 128 | + foreach (var kvp in m_ClientSceneMap) |
| 129 | + { |
| 130 | + if (kvp.Value != ServerScene) { return false; } |
| 131 | + } |
| 132 | + |
| 133 | + return true; |
| 134 | + } |
| 135 | + |
| 136 | + /// <summary> |
| 137 | + /// This logic plugs into the "ConnectionApprovalCallback" exposed by Netcode.NetworkManager, and is run every time a client connects to us. |
| 138 | + /// See ClientGameNetPortal.StartClient for the complementary logic that runs when the client starts its connection. |
| 139 | + /// </summary> |
| 140 | + /// <remarks> |
| 141 | + /// Since our game doesn't have to interact with some third party authentication service to validate the identity of the new connection, our ApprovalCheck |
| 142 | + /// method is simple, and runs synchronously, invoking "callback" to signal approval at the end of the method. Netcode currently doesn't support the ability |
| 143 | + /// to send back more than a "true/false", which means we have to work a little harder to provide a useful error return to the client. To do that, we invoke a |
| 144 | + /// custom message in the same channel that Netcode uses for its connection callback. Since the delivery is NetworkDelivery.ReliableSequenced, we can be |
| 145 | + /// confident that our login result message will execute before any disconnect message. |
| 146 | + /// </remarks> |
| 147 | + /// <param name="connectionData">binary data passed into StartClient. In our case this is the client's GUID, which is a unique identifier for their install of the game that persists across app restarts. </param> |
| 148 | + /// <param name="clientId">This is the clientId that Netcode assigned us on login. It does not persist across multiple logins from the same client. </param> |
| 149 | + /// <param name="connectionApprovedCallback">The delegate we must invoke to signal that the connection was approved or not. </param> |
| 150 | + void ApprovalCheck(byte[] connectionData, ulong clientId, NetworkManager.ConnectionApprovedDelegate connectionApprovedCallback) |
| 151 | + { |
| 152 | + if (connectionData.Length > k_MaxConnectPayload) |
| 153 | + { |
| 154 | + // If connectionData too high, deny immediately to avoid wasting time on the server. This is intended as |
| 155 | + // a bit of light protection against DOS attacks that rely on sending silly big buffers of garbage. |
| 156 | + connectionApprovedCallback(false, 0, false, null, null); |
| 157 | + return; |
| 158 | + } |
| 159 | + |
| 160 | + // Approval check happens for Host too, but obviously we want it to be approved |
| 161 | + if (clientId == NetworkManager.Singleton.LocalClientId) |
| 162 | + { |
| 163 | + SessionManager<SessionPlayerData>.Instance.SetupConnectingPlayerSessionData(clientId, m_Portal.GetPlayerId(), |
| 164 | + new SessionPlayerData(clientId, m_Portal.PlayerName, m_Portal.AvatarRegistry.GetRandomAvatar().Guid.ToNetworkGuid(), 0, true)); |
| 165 | + |
| 166 | + connectionApprovedCallback(true, null, true, null, null); |
| 167 | + return; |
| 168 | + } |
| 169 | + |
| 170 | + var payload = System.Text.Encoding.UTF8.GetString(connectionData); |
| 171 | + var connectionPayload = JsonUtility.FromJson<ConnectionPayload>(payload); // https://docs.unity3d.com/2020.2/Documentation/Manual/JSONSerialization.html |
| 172 | + var gameReturnStatus = GetConnectStatus(connectionPayload); |
| 173 | + |
| 174 | + if (gameReturnStatus == ConnectStatus.Success) |
| 175 | + { |
| 176 | + SessionManager<SessionPlayerData>.Instance.SetupConnectingPlayerSessionData(clientId, connectionPayload.playerId, |
| 177 | + new SessionPlayerData(clientId, connectionPayload.playerName, m_Portal.AvatarRegistry.GetRandomAvatar().Guid.ToNetworkGuid(), 0, true)); |
| 178 | + |
| 179 | + //Populate our client scene map |
| 180 | + m_ClientSceneMap[clientId] = connectionPayload.clientScene; |
| 181 | + |
| 182 | + // connection approval will create a player object for you |
| 183 | + connectionApprovedCallback(true, null, true, Vector3.zero, Quaternion.identity); |
| 184 | + |
| 185 | + m_ConnectionEventPublisher.Publish(new ConnectionEventMessage() { ConnectStatus = ConnectStatus.Success, PlayerName = SessionManager<SessionPlayerData>.Instance.GetPlayerData(clientId)?.PlayerName }); |
| 186 | + } |
| 187 | + else |
| 188 | + { |
| 189 | + //TODO-FIXME:Netcode Issue #796. We should be able to send a reason and disconnect without a coroutine delay. |
| 190 | + //TODO:Netcode: In the future we expect Netcode to allow us to return more information as part of the |
| 191 | + //approval callback, so that we can provide more context on a reject. In the meantime we must provide |
| 192 | + //the extra information ourselves, and then wait a short time before manually close down the connection. |
| 193 | + SendServerToClientSetDisconnectReason(clientId, gameReturnStatus); |
| 194 | + StartCoroutine(WaitToDenyApproval(connectionApprovedCallback)); |
| 195 | + if (m_LobbyServiceFacade.CurrentUnityLobby != null) |
| 196 | + { |
| 197 | + m_LobbyServiceFacade.RemovePlayerFromLobbyAsync(connectionPayload.playerId, m_LobbyServiceFacade.CurrentUnityLobby.Id); |
| 198 | + } |
| 199 | + } |
| 200 | + } |
| 201 | + |
| 202 | + ConnectStatus GetConnectStatus(ConnectionPayload connectionPayload) |
| 203 | + { |
| 204 | + if (m_NetworkManager.ConnectedClientsIds.Count >= CharSelectData.k_MaxLobbyPlayers) |
| 205 | + { |
| 206 | + return ConnectStatus.ServerFull; |
| 207 | + } |
| 208 | + |
| 209 | + if (connectionPayload.isDebug != Debug.isDebugBuild) |
| 210 | + { |
| 211 | + return ConnectStatus.IncompatibleBuildType; |
| 212 | + } |
| 213 | + |
| 214 | + return SessionManager<SessionPlayerData>.Instance.IsDuplicateConnection(connectionPayload.playerId) ? |
| 215 | + ConnectStatus.LoggedInAgain : ConnectStatus.Success; |
| 216 | + } |
| 217 | + |
| 218 | + static IEnumerator WaitToDenyApproval(NetworkManager.ConnectionApprovedDelegate connectionApprovedCallback) |
| 219 | + { |
| 220 | + yield return new WaitForSeconds(0.5f); |
| 221 | + connectionApprovedCallback(false, 0, false, null, null); |
| 222 | + } |
| 223 | + |
| 224 | + IEnumerator WaitToShutdown() |
| 225 | + { |
| 226 | + yield return null; |
| 227 | + m_NetworkManager.Shutdown(); |
| 228 | + SessionManager<SessionPlayerData>.Instance.OnServerEnded(); |
| 229 | + } |
| 230 | + |
| 231 | + /// <summary> |
| 232 | + /// Sends a DisconnectReason to all connected clients. This should only be done on the server, prior to disconnecting the client. |
| 233 | + /// </summary> |
| 234 | + /// <param name="status"> The reason for the upcoming disconnect.</param> |
| 235 | + static void SendServerToAllClientsSetDisconnectReason(ConnectStatus status) |
| 236 | + { |
| 237 | + var writer = new FastBufferWriter(sizeof(ConnectStatus), Allocator.Temp); |
| 238 | + writer.WriteValueSafe(status); |
| 239 | + NetworkManager.Singleton.CustomMessagingManager.SendNamedMessageToAll(nameof(ClientGameNetPortal.ReceiveServerToClientSetDisconnectReason_CustomMessage), writer); |
| 240 | + } |
| 241 | + |
| 242 | + /// <summary> |
| 243 | + /// Sends a DisconnectReason to the indicated client. This should only be done on the server, prior to disconnecting the client. |
| 244 | + /// </summary> |
| 245 | + /// <param name="clientID"> id of the client to send to </param> |
| 246 | + /// <param name="status"> The reason for the upcoming disconnect.</param> |
| 247 | + static void SendServerToClientSetDisconnectReason(ulong clientID, ConnectStatus status) |
| 248 | + { |
| 249 | + var writer = new FastBufferWriter(sizeof(ConnectStatus), Allocator.Temp); |
| 250 | + writer.WriteValueSafe(status); |
| 251 | + NetworkManager.Singleton.CustomMessagingManager.SendNamedMessage(nameof(ClientGameNetPortal.ReceiveServerToClientSetDisconnectReason_CustomMessage), clientID, writer); |
| 252 | + } |
| 253 | + |
| 254 | + /// <summary> |
| 255 | + /// Called after the server is created- This is primarily meant for the host server to clean up or handle/set state as its starting up |
| 256 | + /// </summary> |
| 257 | + void ServerStartedHandler() |
| 258 | + { |
| 259 | + // server spawns game state |
| 260 | + var gameState = Instantiate(m_GameState); |
| 261 | + |
| 262 | + gameState.Spawn(); |
| 263 | + |
| 264 | + SceneLoaderWrapper.Instance.AddOnSceneEventCallback(); |
| 265 | + |
| 266 | + //The "BossRoom" server always advances to CharSelect immediately on start. Different games |
| 267 | + //may do this differently. |
| 268 | + SceneLoaderWrapper.Instance.LoadScene("CharSelect", useNetworkSceneManager: true); |
| 269 | + } |
| 270 | + } |
| 271 | +} |
0 commit comments