using System; using Unity.BossRoom.Infrastructure; using Unity.BossRoom.UnityServices.Lobbies; using Unity.Multiplayer.Samples.BossRoom; using Unity.Multiplayer.Samples.Utilities; using Unity.Netcode; using UnityEngine; using VContainer; namespace Unity.BossRoom.ConnectionManagement { /// /// Connection state corresponding to a listening host. Handles incoming client connections. When shutting down or /// being timed out, transitions to the Offline state. /// class HostingState : OnlineState { [Inject] LobbyServiceFacade m_LobbyServiceFacade; [Inject] IPublisher m_ConnectionEventPublisher; // used in ApprovalCheck. This is intended as a bit of light protection against DOS attacks that rely on sending silly big buffers of garbage. const int k_MaxConnectPayload = 1024; public override void Enter() { //The "BossRoom" server always advances to CharSelect immediately on start. Different games //may do this differently. SceneLoaderWrapper.Instance.LoadScene("CharSelect", useNetworkSceneManager: true); if (m_LobbyServiceFacade.CurrentUnityLobby != null) { m_LobbyServiceFacade.BeginTracking(); } } public override void Exit() { SessionManager.Instance.OnServerEnded(); } public override void OnClientConnected(ulong clientId) { var playerData = SessionManager.Instance.GetPlayerData(clientId); if (playerData != null) { m_ConnectionEventPublisher.Publish(new ConnectionEventMessage() { ConnectStatus = ConnectStatus.Success, PlayerName = playerData.Value.PlayerName }); } else { // This should not happen since player data is assigned during connection approval Debug.LogError($"No player data associated with client {clientId}"); var reason = JsonUtility.ToJson(ConnectStatus.GenericDisconnect); m_ConnectionManager.NetworkManager.DisconnectClient(clientId, reason); } } public override void OnClientDisconnect(ulong clientId) { if (clientId != m_ConnectionManager.NetworkManager.LocalClientId) { var playerId = SessionManager.Instance.GetPlayerId(clientId); if (playerId != null) { var sessionData = SessionManager.Instance.GetPlayerData(playerId); if (sessionData.HasValue) { m_ConnectionEventPublisher.Publish(new ConnectionEventMessage() { ConnectStatus = ConnectStatus.GenericDisconnect, PlayerName = sessionData.Value.PlayerName }); } SessionManager.Instance.DisconnectClient(clientId); } } } public override void OnUserRequestedShutdown() { var reason = JsonUtility.ToJson(ConnectStatus.HostEndedSession); for (var i = m_ConnectionManager.NetworkManager.ConnectedClientsIds.Count - 1; i >= 0; i--) { var id = m_ConnectionManager.NetworkManager.ConnectedClientsIds[i]; if (id != m_ConnectionManager.NetworkManager.LocalClientId) { m_ConnectionManager.NetworkManager.DisconnectClient(id, reason); } } m_ConnectionManager.ChangeState(m_ConnectionManager.m_Offline); } public override void OnServerStopped() { m_ConnectStatusPublisher.Publish(ConnectStatus.GenericDisconnect); m_ConnectionManager.ChangeState(m_ConnectionManager.m_Offline); } /// /// This logic plugs into the "ConnectionApprovalResponse" exposed by Netcode.NetworkManager. It is run every time a client connects to us. /// The complementary logic that runs when the client starts its connection can be found in ClientConnectingState. /// /// /// Multiple things can be done here, some asynchronously. For example, it could authenticate your user against an auth service like UGS' auth service. It can /// also send custom messages to connecting users before they receive their connection result (this is useful to set status messages client side /// when connection is refused, for example). /// Note on authentication: It's usually harder to justify having authentication in a client hosted game's connection approval. Since the host can't be trusted, /// clients shouldn't send it private authentication tokens you'd usually send to a dedicated server. /// /// The initial request contains, among other things, 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. /// Our response to the approval process. In case of connection refusal with custom return message, we delay using the Pending field. public override void ApprovalCheck(NetworkManager.ConnectionApprovalRequest request, NetworkManager.ConnectionApprovalResponse response) { var connectionData = request.Payload; var clientId = request.ClientNetworkId; if (connectionData.Length > k_MaxConnectPayload) { // If connectionData too high, deny immediately to avoid wasting time on the server. This is intended as // a bit of light protection against DOS attacks that rely on sending silly big buffers of garbage. response.Approved = false; return; } var payload = System.Text.Encoding.UTF8.GetString(connectionData); var connectionPayload = JsonUtility.FromJson(payload); // https://docs.unity3d.com/2020.2/Documentation/Manual/JSONSerialization.html var gameReturnStatus = GetConnectStatus(connectionPayload); if (gameReturnStatus == ConnectStatus.Success) { SessionManager.Instance.SetupConnectingPlayerSessionData(clientId, connectionPayload.playerId, new SessionPlayerData(clientId, connectionPayload.playerName, new NetworkGuid(), 0, true)); // connection approval will create a player object for you response.Approved = true; response.CreatePlayerObject = true; response.Position = Vector3.zero; response.Rotation = Quaternion.identity; return; } response.Approved = false; response.Reason = JsonUtility.ToJson(gameReturnStatus); if (m_LobbyServiceFacade.CurrentUnityLobby != null) { m_LobbyServiceFacade.RemovePlayerFromLobbyAsync(connectionPayload.playerId); } } ConnectStatus GetConnectStatus(ConnectionPayload connectionPayload) { if (m_ConnectionManager.NetworkManager.ConnectedClientsIds.Count >= m_ConnectionManager.MaxConnectedPlayers) { return ConnectStatus.ServerFull; } if (connectionPayload.isDebug != Debug.isDebugBuild) { return ConnectStatus.IncompatibleBuildType; } return SessionManager.Instance.IsDuplicateConnection(connectionPayload.playerId) ? ConnectStatus.LoggedInAgain : ConnectStatus.Success; } } }