Last Updated: 11/15/2022, 11:31:21 AM

# Armada

# Quick Reference

References
using AccelByte.Server; 
Log in with Credentials
AccelByteServerPlugin.GetDedicatedServer().LoginWithClientCredentials(
result =>
{
    if (result.IsError)
    {
        Debug.Log($"Server login failed");
    }
    else
    {
        Debug.Log("Server login successful");
    }
});
Register Local DS to DSM
AccelByteServerPlugin.GetDedicatedServerManager().RegisterLocalServer(ip, portNumber, name, registerResult =>
{
    if (registerResult.IsError)
    {
        Debug.Log("Register Local Server to DSM failed");
    }
    else
    {
        Debug.Log("Register Local Server to DSM successful");
    }
});
Get Session ID to DSM
AccelByteServerPlugin.GetDedicatedServerManager().GetSessionId(dsmResult =>
{
    if (dsmResult.IsError)
    {
        Debug.Log("Failed Get Session Id");
    }
    else
    {
        Debug.Log("Successfully Get Session Id");
    }
});
Query Session Status to Matchmaking
AccelByteServerPlugin.GetMatchmaking().QuerySessionStatus(dsmResult.Value.session_id, 
queryResult =>
{
    if (queryResult.IsError)
    {
        Debug.Log("Failed Query Session Status");
    }
    else
    {
        Debug.Log("Successfully Query Session Status");
    }
});
Deregister Local Server
AccelByteServerPlugin.GetDedicatedServerManager().DeregisterLocalServer(result => 
{
    if (result.IsError)
    {
        Debug.Log("Failed Deregister Local Server");
    }
    else
    {
        Debug.Log("Successfully Deregister Local Server");
    }
});
Shutdown Server
AccelByteServerPlugin.GetDedicatedServerManager().ShutdownServer(true, 
result => 
{
    if (result.IsError)
    {
        Debug.Log("Failed Shutdown Server");
    }
    else
    {
        Debug.Log("Successfully Shutdown Server");
    }
});

# Quickstart Guide

In this tutorial, you will learn how to integrate Armada with your game and test it on your local PC or in the AccelByte Server by uploading the server image. This guide assumes that you have already implemented the Lobby (opens new window), Friends (opens new window), Party (opens new window), and Matchmaking (opens new window) services.

Since server implementation with Armada can vary for each game, you can familiarize yourself with other concepts and classes in the ServerModels.cs file inside the plugin SDK.

Before continuing, ensure that you have configured your AccelByteServerSDKConfig.json. Follow this tutorial (opens new window) to configure the server SDK.

We will start by adding simple Armada logic into the game.

  1. Create a new script called ArmadaHandler.cs.
  2. Add the following AccelByte library to the top of the script:
using AccelByte.Server; 
  1. Create the Login logic for the Dedicated Server using client credentials. In the script, add the following code:
// Function called to Login by client credentials for the server.
public void LoginServer(int port, bool isLocal)
{
    AccelByteServerPlugin.GetDedicatedServer().LoginWithClientCredentials(result =>
    {
        if (result.IsError)
        {
            // If we error, grab the Error Code and Message to print in the Log
            Debug.Log($"Server login failed : {result.Error.Code}: {result.Error.Message}");
        }
        else
        {
            Debug.Log("Server login successful");
            // Some actions
        }
    });
}
  1. Now you can begin to integrate Armada into your game with the local Dedicated Server. You will need to register your local Dedicated Server to the Dedicated Server Manager (DSM). This will allow your Dedicated Server to be tracked by AccelByte’s DSM.

Log in with your client credentials and replace the actions comments as per the following code:

// Function called to Log in by client credentials for the server.
public static void LoginServer(int port, bool isLocal)
{
    ...
        else
        {
            Debug.Log("Server login successful");
            if (isLocal)
            {
                // Set local IP, server name, port
                string ip = "127.0.0.1";
                string name = $"localds-{DeviceProvider.GetFromSystemInfo().DeviceId}";
                uint portNumber = Convert.ToUInt32(port);
 
                // Register Local Server to DSM
AccelByteServerPlugin.GetDedicatedServerManager().RegisterLocalServer(ip, portNumber, name, registerResult =>
                {
                    if (registerResult.IsError)
                    {
                        Debug.Log("Register Local Server to DSM failed");
                    }
                    else
                    {
                        Debug.Log("Register Local Server to DSM successful");
                    }
                });
            }
 
        }
    });
}
  1. Once completed, you can now test your script to ensure it can log in and register your local Dedicated Server to the DSM. Call this function from somewhere else in MonoBehaviour.
void Start()
{
#if UNITY_SERVER
   LoginServer("7777", false);
#endif
}

Build and run the project as a server build. Your server log will read Server login successful and Register Local Server to DSM successful if the action has been completed successfully. You can also check in the Admin Portal to see if your local Dedicated Server has successfully registered in the DSM.

TROUBLESHOOTING

If you encounter a Server login failed: message, check your login credentials, API URLs, or the other configs in your AccelByteServerSDKConfig.json file.

  1. Using the Armada Service, you can get match information by calling get Session ID from the DSM and use Session ID to get match information from the Matchmaking service.
public void GetMatchInfo()
{
    // Get session id/ match id from DSM
    AccelByteServerPlugin.GetDedicatedServerManager().GetSessionId(dsmResult =>
    {
        if (dsmResult.IsError)
        {
            Debug.Log("Failed Get Session Id");
        }
        else
        {
            Debug.Log("Successfully Get Session Id");
 
            // Query Session Status to get match info from Matchmaking
            AccelByteServerPlugin.GetMatchmaking().QuerySessionStatus(dsmResult.Value.session_id, queryResult =>
            {
                if (queryResult.IsError)
                {
                    Debug.Log("Failed Query Session Status");
                }
                else
                {
                    Debug.Log("Successfully Query Session Status. The game mode is: " + queryResult.Value.game_mode);
                }
            });
        }
    });
}
  1. After a game has finished, you will need to deregister the local Dedicated Server from the DSM. Use the following code to create a Deregister Local Dedicated Server script.
public void UnregisterServer(bool isLocal)
{
    if (isLocal)
    {
        // Deregister Local Server to DSM
AccelByteServerPlugin.GetDedicatedServerManager().DeregisterLocalServer(result => 
        {
            if (result.IsError)
            {
                Debug.Log("Failed Deregister Local Server");
            }
            else
            {
                Debug.Log("Successfully Deregister Local Server");
            }
        });
    }
}
  1. Use the following code to attach this script when the local Dedicated Server needs to be closed:
void OnApplicationQuit()
{
#if UNITY_SERVER
   UnregisterServer(false);
#endif
}
  1. Now that you have set up all the required local Dedicated Server functions, you can begin to integrate Armada with the Dedicated Server. For the Login, use the same logic you created in Step 3. To register the Dedicated Server to the DSM so it can be spawned by Armada, you must modify the register logic as per the following:
// Function called to Login by client credentials for the server.
public void LoginServer(int port, bool isLocal)
{
    ...
        else
        {
            Debug.Log("Server login successful");
            
            if (!isLocal)
            {
            // Register Server to DSM
            AccelByteServerPlugin.GetDedicatedServerManager().RegisterServer(port, registerResult =>
            {
                if (registerResult.IsError)
                {
                    Debug.Log("Register Server to DSM failed");
                }
                else
                {
                    Debug.Log("Register Server to DSM successful");
                }
            });
        }
    });
}
  1. To obtain match information, use the same logic from Step 6.

  2. Create a Deregister or Shutdown Dedicated Server from Dedicated Server Manager script by modifying the following in the deregistration script that you have already created in Step 7:

public static void UnregisterServer(bool isLocal)
{
    if (!isLocal)
    {
        // Shutdown Server to DSM
            AccelByteServerPlugin.GetDedicatedServerManager().ShutdownServer(true, result => 
            {
                if (result.IsError)
                {
                    Debug.Log("Failed Shutdown Server");
                }
                else
                {
                    Debug.Log("Successfully Shutdown Server");
                }
            });
    }
}
  1. For testing purposes, modify the boolean that you used in the Start() function to test your local servers.
void Start()
{
#if UNITY_SERVER
   LoginServer("7777", true);
#endif
}

Modify the boolean in the OnApplicationQuit() function.

void OnApplicationQuit()
{
#if UNITY_SERVER
   UnregisterServer(true);
#endif
}

Now you can build your server and upload the image into the AccelByte Server. Follow this guide (opens new window) to upload the image to the AccelByte Server. Once completed, your server should be automatically created when the client gets a DS Updated notification event from the Matchmaking service.

NOTE

  • You can also configure the timeout for the Dedicated Server in the Admin Portal by following this guide (opens new window).
  • We recommend uploading your server image to the AccelByte Server with the Linux Server build.

Congratulations! You have now learnt how to use Armada!

Continue on for a step by step example of the code implementation.

# Step by Step Guide

Code Implementation
  1. Turn your ArmadaHandler.cs script into a wrapper by changing the class to static and removing the MonoBehaviour base class. This will make it easier to call the GameManager script from your own game later.
public static class ArmadaHandler
  1. Change the server login function into a static function.
public static void LoginServer(int port, bool isLocal) { … }
  1. Modify the register server script to be more flexible so it can be used as a local Dedicated Server or Dedicated Server in a single build.
public static void LoginServer(int port, bool isLocal)
{
    AccelByteServerPlugin.GetDedicatedServer().LoginWithClientCredentials(result =>
    {
        if (result.IsError)
        {
            // If we error, grab the Error Code and Message to print in the Log
            Debug.Log($"Server login failed : {result.Error.Code}: {result.Error.Message}");
        }
        else
        {
            Debug.Log("Server login successful");
 
            if (!isLocal)
            {
                // Register Server to DSM
                AccelByteServerPlugin.GetDedicatedServerManager().RegisterServer(port, registerResult =>
                {
                    if (registerResult.IsError)
                    {
                        Debug.Log("Register Server to DSM failed");
                    }
                    else
                    {
                        Debug.Log("Register Server to DSM successful");
                    }
                });
            }
            else
            {
                string ip = "127.0.0.1";
                string name = $"localds-{DeviceProvider.GetFromSystemInfo().DeviceId}";
                uint portNumber = Convert.ToUInt32(port);
 
                // Register Local Server to DSM
                AccelByteServerPlugin.GetDedicatedServerManager().RegisterLocalServer(ip, portNumber, name, registerResult =>
                {
                    if (registerResult.IsError)
                    {
                        Debug.Log("Register Local Server to DSM failed");
                    }
                    else
                    {
                        Debug.Log("Register Local Server to DSM successful");
                    }
                });
            }
        }
    });
}
  1. Once completed, you can use the Matchmaking information to check whether the connected client is allowed to enter the current server. To do this, create a GetPlayerInfo function that uses a callback. Attach this function to another script and wait until the asynchronous function has been completed.
public static void GetPlayerInfo(ResultCallback<MatchmakingResult> callback)
{
    // Get session id/ match id from DSM
    AccelByteServerPlugin.GetDedicatedServerManager().GetSessionId(dsmResult =>
    {
        if (dsmResult.IsError)
        {
            Debug.Log("Failed Get Session Id");
 
            callback.TryError(dsmResult.Error);
        }
        else
        {
            // Query Session Status to get match info from Matchmaking
            AccelByteServerPlugin.GetMatchmaking().QuerySessionStatus(dsmResult.Value.session_id, queryResult =>
            {
                if (queryResult.IsError)
                {
                    Debug.Log("Failed Query Session Status");
 
                    callback.TryError(queryResult.Error);
                }
                else
                {
                    // Return error if status is not matched
                    if (queryResult.Value.status != AccelByte.Models.MatchmakingStatus.matched)
                    {
                        Debug.Log("Matchmaking status is not matched");
 
                        // Return error callback
                        callback.TryError(queryResult.Error);
                    }
 
                    // Return success callback
                    callback.TryOk(queryResult.Value);
                }
            });
        }
    });
}
  1. Change the Unregister Dedicated Server function to a static function.
public static void UnregisterServer(bool isLocal) { … }
  1. Change the Unregister Server script to make it more flexible. This way, you won’t need to make a different build for your local Dedicated Server and Dedicated Server.
public static void UnregisterServer(bool isLocal)
{
    if (isLocal)
    {
        // Deregister Local Server to DSM
        AccelByteServerPlugin.GetDedicatedServerManager().DeregisterLocalServer(result => 
        {
            if (result.IsError)
            {
                Debug.Log("Failed Deregister Local Server");
            }
            else
            {
                Debug.Log("Successfully Deregister Local Server");
 
                Application.Quit();
            }
        });
    }
    else
    {
        // Shutdown Server to DSM
        AccelByteServerPlugin.GetDedicatedServerManager().ShutdownServer(true, result => 
        {
            if (result.IsError)
            {
                Debug.Log("Failed Shutdown Server");
            }
            else
            {
                Debug.Log("Successfully Shutdown Server");
 
                Application.Quit();
            }
        });
    }
}
  1. Once completed, you can begin to integrate Armada into your game. In the steps above, this tutorial has used GameplayManager.cs to handle the server script. Start by storing a boolean variable to determine whether the tests are local. Set the value to **false **to use the AccelByte Server as default.
private bool isLocal = false;
  1. Remove the Start() function and replace it with the following function in GameplayManager.cs. This will determine whether your game is a local build for the server to log in.
internal void OnAccelByteServerStarted(int port)
{
    // Get the local command line argument for the local test
    isLocal = ConnectionHandler.GetLocalArgument();
 
    ArmadaHandler.LoginServer(port, isLocal);
}
  1. Override the OnStartServer() function from the WatchTimeNetworkManager.cs script:
public override void OnStartServer()
{
    base.OnStartServer();
 
    GameplayManager.OnAccelByteServerStarted(transport.ServerUri().Port);
    GameplayManager.OnServerStarted();
}
  1. Navigate back to GameplayManager.cs and add the following code to obtain the Session ID and query match information when a player tries to connect to the server:
void OnServerStartClient(NetworkConnection conn, ServerStartClientMessage msg)
{
    playerInfos.Add(conn, new PlayerInfo { playerId = msg.playerId, displayName = msg.displayName });
 
    PlayerInfo playerInfo = playerInfos[conn];
 
    ArmadaHandler.GetPlayerInfo(result => 
    {
        if (result.IsError) return;
 
        bool isPartyA = true;
        bool foundPlayer = false;
 
        // Get total player from game mode in result
        totalPlayers = result.Value.game_mode.ToGameMode().GetTotalPlayers();
 
        // Check if the user exists and assign the party
        foreach (var ally in result.Value.matching_allies)
        {
            foreach (var party in ally.matching_parties)
            {
                foreach (var user in party.party_members)
                {
                    if (user.user_id == playerInfo.playerId)
                    {
                        playerInfo.isPartyA = isPartyA;
 
                        foundPlayer = true;
                        break;
                    }
                }
 
                if (foundPlayer) break;
            }
 
            if (foundPlayer) break;
 
            isPartyA = !isPartyA;
        }
 
        // Remove player info if the player is not registered in the current match
        if (!foundPlayer)
        {
            playerInfos.Remove(conn);
            return;
        }
 
        totalPlayersConnected++;
 
        Debug.Log($"Total player Connected : {totalPlayersConnected}/{totalPlayers}");
 
        // Update player infos dictionary
        playerInfos[conn] = playerInfo;
 
        Debug.Log(string.Format("Player {0} is joining in the party {1}", playerInfo.displayName, playerInfo.isPartyA ? "A" : "B"));
 
        // Start the game if total players connected and total players are same
        if (totalPlayersConnected == totalPlayers)
        {
 
            foreach (NetworkConnection connection in playerInfos.Keys)
            {
                connection.Send(new ClientStartClientResponseMessage { });
            }
            if (isServer)
            {
                StartCoroutine(CountdownTimer());
            }
        }
    });
}
  1. Create an Unregister Server script to listen to a variable that holds the boolean value of local test in the OnApplicationQuit() function in GameplayManager.cs.
private void OnApplicationQuit()
{
#if UNITY_SERVER
    ArmadaHandler.UnregisterServer(isLocal);
#endif
}
  1. Add a timer to unregister the Dedicated Server after the game has finished.
IEnumerator CloseServer(int timeout = 30)
{
    Debug.Log("Start countdown to close server");
 
    for (int i = 0; i < timeout; i++)
    {
        yield return new WaitForSeconds(1.0f);
    }
 
    ArmadaHandler.UnregisterServer(isLocal);
}
  1. Call the Close Server countdown function in GameplayManager.cs when the game has finished.
void OnServerStopTimerMessage(NetworkConnection conn, ServerRequestStopTimerMessage msg)
{
    totalPlayersStop++;
 
    PlayerInfo playerInfo = playerInfos[conn];
    playerInfo.playerScoreTime = mainTime;
    playerInfos[conn] = playerInfo;
 
    Debug.Log($"Total player Stop: {totalPlayersStop}/{totalPlayers}");
 
    if (totalPlayersStop == totalPlayers)
    {
        StartCoroutine(CloseServer());
        OnServerStopGameplay();
    }
}
  1. Build your server and upload your server image to the AccelByte Server.

  2. Once completed, you can test your build. To test on your local PC, run the local server with the command line argument -local. You can either create a shortcut or a batch file to test local servers. If you want to create a batch file, you can use the following example: batch file.

@ECHO ON
 
start Server/Justice-Unity-Tutorial-Project.exe local

Congratulations! You have fully implemented the Armada Service and successfully installed the AccelByte Unity SDK and AccelByte Config file.

# Full Code

ArmadaHandler.cs
// Copyright (c) 2022 AccelByte Inc. All Rights Reserved.
// This is licensed software from AccelByte Inc, for limitations
// and restrictions contact your company contract manager.
 
using System;
using System.Collections.Generic;
using UnityEngine;
using AccelByte.Server;
using AccelByte.Api;
using AccelByte.Core;
using AccelByte.Models;
 
public static class ArmadaHandler
{
    /// <summary>
    /// Server login with the server client credentials and register DS to DSM
    /// </summary>
    /// <param name="port"> </param>
    /// <param name="isLocal"></param>
    public static void LoginServer(int port, bool isLocal)
    {
        AccelByteServerPlugin.GetDedicatedServer().LoginWithClientCredentials(result =>
        {
            if (result.IsError)
            {
                // If we error, grab the Error Code and Message to print in the Log
                Debug.Log($"Server login failed : {result.Error.Code}: {result.Error.Message}");
            }
            else
            {
                Debug.Log("Server login successful");
 
                if (!isLocal)
                {
                    // Register Server to DSM
                    AccelByteServerPlugin.GetDedicatedServerManager().RegisterServer(port, registerResult =>
                    {
                        if (registerResult.IsError)
                        {
                            Debug.Log("Register Server to DSM failed");
                        }
                        else
                        {
                            Debug.Log("Register Server to DSM successful");
                        }
                    });
                }
                else
                {
                    string ip = "127.0.0.1";
                    string name = $"localds-{DeviceProvider.GetFromSystemInfo().DeviceId}";
                    uint portNumber = Convert.ToUInt32(port);
 
                    // Register Local Server to DSM
                    AccelByteServerPlugin.GetDedicatedServerManager().RegisterLocalServer(ip, portNumber, name, registerResult =>
                    {
                        if (registerResult.IsError)
                        {
                            Debug.Log("Register Local Server to DSM failed");
                        }
                        else
                        {
                            Debug.Log("Register Local Server to DSM successful");
                        }
                    });
                }
            }
        });
    }
 
    /// <summary>
    /// Unregister DS from DSM and quit the app
    /// </summary>
    /// <param name="isLocal"> Unregister local DS if the value is true</param>
    public static void UnregisterServer(bool isLocal)
    {
        if (isLocal)
        {
            // Deregister Local Server to DSM
            AccelByteServerPlugin.GetDedicatedServerManager().DeregisterLocalServer(result => 
            {
                if (result.IsError)
                {
                    Debug.Log("Failed Deregister Local Server");
                }
                else
                {
                    Debug.Log("Successfully Deregister Local Server");
 
                    Application.Quit();
                }
            });
        }
        else
        {
            // Shutdown Server to DSM
            AccelByteServerPlugin.GetDedicatedServerManager().ShutdownServer(true, result => 
            {
                if (result.IsError)
                {
                    Debug.Log("Failed Shutdown Server");
                }
                else
                {
                    Debug.Log("Successfully Shutdown Server");
 
                    Application.Quit();
                }
            });
        }
    }
 
    /// <summary>
    /// DS queries match info from Matchmaking (MM)
    /// </summary>
    /// <param name="callback"> Return match info callback</param>
    public static void GetPlayerInfo(ResultCallback<MatchmakingResult> callback)
    {
        // Get session id/ match id from DSM
        AccelByteServerPlugin.GetDedicatedServerManager().GetSessionId(dsmResult =>
        {
            if (dsmResult.IsError)
            {
                Debug.Log("Failed Get Session Id");
 
                callback.TryError(dsmResult.Error);
            }
            else
            {
                // Query Session Status to get match info from Matchmaking
                AccelByteServerPlugin.GetMatchmaking().QuerySessionStatus(dsmResult.Value.session_id, queryResult =>
                {
                    if (queryResult.IsError)
                    {
                        Debug.Log("Failed Query Session Status");
 
                        callback.TryError(queryResult.Error);
                    }
                    else
                    {
                        // Return error if status is not matched
                        if (queryResult.Value.status != AccelByte.Models.MatchmakingStatus.matched)
                        {
                            Debug.Log("Matchmaking status is not matched");
 
                            // Return error callback
                            callback.TryError(queryResult.Error);
                        }
 
                        // Return success callback
                        callback.TryOk(queryResult.Value);
                    }
                });
            }
        });
    }
}
WatchTimeNetworkManager.cs
// Copyright (c) 2022 AccelByte Inc. All Rights Reserved.
// This is licensed software from AccelByte Inc, for limitations
// and restrictions contact your company contract manager.
 
using Mirror;
using System;
using UnityEngine;
 
public class WatchTimeNetworkManager : NetworkManager
{
    [SerializeField]
    private GameplayManager GameplayManager;
 
    public override void Start()
    {
        base.Start();
 
#if !UNITY_SERVER
        // Change ip and port based on DS info in the client
        networkAddress = ConnectionHandler.ip;
        gameObject.GetComponent<kcp2k.KcpTransport>().Port = ConnectionHandler.uPort;
 
        // Auto start the client connection
        StartClient();
#endif
    }
 
    #region Client System Callbacks
 
    /// <summary>
    /// Called on the client when connected to a server.
    /// <para>The default implementation of this function sets the client as ready and adds a player. Override the function to dictate what happens when the client connects.</para>
    /// </summary>
    /// <param name="conn">Connection to the server.</param>
    public override void OnClientConnect(NetworkConnection conn)
    {
        base.OnClientConnect(conn);
        GameplayManager.OnPlayerStarted();
    }
    #endregion
 
    #region Start & Stop Callbacks
 
    /// <summary>
    /// Called when a server is started - including when a host is started.
    /// <para>StartServer has multiple signatures, but they all cause this hook to be called.</para>
    /// </summary>
    public override void OnStartServer()
    {
        base.OnStartServer();
 
        GameplayManager.OnAccelByteServerStarted(transport.ServerUri().Port);
        GameplayManager.OnServerStarted();
    }
    #endregion
 
    /// <summary>
    /// Called when the server stop the client connections
    /// </summary>
    public override void OnStopClient()
    {
        base.OnStopClient();
        GameplayManager.OnPlayerDisconnected();
    }
}
GameplayManager.cs
// Copyright (c) 2022 AccelByte Inc. All Rights Reserved.
// This is licensed software from AccelByte Inc, for limitations
// and restrictions contact your company contract manager.
 
using System.Collections;
using System.Collections.Generic;
using System.Linq;
using UnityEngine;
using AccelByte.Api;
using Mirror;
 
public class GameplayManager : NetworkBehaviour
{
    [SerializeField]
    private GameplayInterface gameCanvas;
 
    private int totalPlayersConnected = 0;
    private int totalPlayersStop = 0;
    private int totalPlayers = 0;
 
    private bool isLocal = false;
 
    private double targetTime;
    private double mainTime;
 
    internal static readonly Dictionary<NetworkConnection, PlayerInfo> playerInfos = new Dictionary<NetworkConnection, PlayerInfo>();
 
    /// <summary>
    /// Called by server to login by credentials
    /// </summary>
    /// <param name="port"></param>
    internal void OnAccelByteServerStarted(int port)
    {
        // Get the local command line argument for the local test
        isLocal = ConnectionHandler.GetLocalArgument();
 
        ArmadaHandler.LoginServer(port, isLocal);
    }
 
    /// <summary>
    /// Called on Start Server
    /// </summary>
    internal void OnServerStarted()
    {
        if (!NetworkServer.active) return;
 
        NetworkServer.RegisterHandler<ServerStartClientMessage>(OnServerStartClient);
        NetworkServer.RegisterHandler<ServerRequestStopTimerMessage>(OnServerStopTimerMessage);
    }
 
    /// <summary>
    /// Called on Client connect to server
    /// </summary>
    internal void OnPlayerStarted()
    {
        if (!NetworkClient.active) return;
 
        NetworkClient.RegisterHandler<ClientStartClientResponseMessage>(OnStartClientResponse);
        NetworkClient.RegisterHandler<ClientUpdateCountdownTimerMessage>(OnUpdateCountdownTime);
        NetworkClient.RegisterHandler<ClientChangeToGameplayStateMessage>(OnChangeToGameplayState);
        NetworkClient.RegisterHandler<ClientUpdateMainTimeMessage>(OnUpdateMainTime);
        NetworkClient.RegisterHandler<ClientOnAllPlayerStopTime>(OnAllClientStopTime);
 
        // Current user's userId and displayName
        string userId = AccelBytePlugin.GetUser().Session.UserId;
        string displayName = LobbyHandler.Instance.partyHandler.partyMembers[userId];
 
        NetworkClient.connection.Send(new ServerStartClientMessage { playerId = userId, displayName = displayName });
 
        // Set the user id inside the gameplay interface player id. this to check after gameplay ended, the interface will know where their current player information by matching the player id
        gameCanvas.playerId = userId;
    }
 
    /// <summary>
    /// Called on Client disconnect
    /// </summary>
    internal void OnPlayerDisconnected()
    {
        gameCanvas.ChangePanel(GameplayInterfaceState.None);
    } 
 
    /// <summary>
    /// Send message to server that player press the stop button
    /// </summary>
    public void RequestStopTime()
    {
        NetworkClient.connection.Send(new ServerRequestStopTimerMessage { });
    }
 
    /// <summary>
    /// Server set player's info
    /// </summary>
    /// <param name="conn"> player's network connection</param>
    /// <param name="msg"> message that contains player's info</param>
    void OnServerStartClient(NetworkConnection conn, ServerStartClientMessage msg)
    {
        playerInfos.Add(conn, new PlayerInfo { playerId = msg.playerId, displayName = msg.displayName });
 
        PlayerInfo playerInfo = playerInfos[conn];
 
        ArmadaHandler.GetPlayerInfo(result => 
        {
            if (result.IsError) return;
 
            bool isPartyA = true;
            bool foundPlayer = false;
 
            // Get total player from game mode in result
            totalPlayers = result.Value.game_mode.ToGameMode().GetTotalPlayers();
 
            // Check if the user exists and assign the party
            foreach (var ally in result.Value.matching_allies)
            {
                foreach (var party in ally.matching_parties)
                {
                    foreach (var user in party.party_members)
                    {
                        if (user.user_id == playerInfo.playerId)
                        {
                            playerInfo.isPartyA = isPartyA;
 
                            foundPlayer = true;
                            break;
                        }
                    }
 
                    if (foundPlayer) break;
                }
 
                if (foundPlayer) break;
 
                isPartyA = !isPartyA;
            }
 
            // Remove player info if the player is not registered in the current match
            if (!foundPlayer)
            {
                playerInfos.Remove(conn);
                return;
            }
 
            totalPlayersConnected++;
 
            Debug.Log($"Total player Connected : {totalPlayersConnected}/{totalPlayers}");
 
            // Update player infos dictionary
            playerInfos[conn] = playerInfo;
 
            Debug.Log(string.Format("Player {0} is joining in the party {1}", playerInfo.displayName, playerInfo.isPartyA ? "A" : "B"));
 
            // Start the game if total players connected and total players are same
            if (totalPlayersConnected == totalPlayers)
            {
 
                foreach (NetworkConnection connection in playerInfos.Keys)
                {
                    connection.Send(new ClientStartClientResponseMessage { });
                }
                if (isServer)
                {
                    StartCoroutine(CountdownTimer());
                }
            }
        });
    }
 
    /// <summary>
    /// Server set the player stop time
    /// </summary>
    /// <param name="conn"> player's network connection</param>
    /// <param name="msg"> server's message</param>
    void OnServerStopTimerMessage(NetworkConnection conn, ServerRequestStopTimerMessage msg)
    {
        totalPlayersStop++;
 
        PlayerInfo playerInfo = playerInfos[conn];
        playerInfo.playerScoreTime = mainTime;
        playerInfos[conn] = playerInfo;
 
        Debug.Log($"Total player Stop: {totalPlayersStop}/{totalPlayers}");
 
        if (totalPlayersStop == totalPlayers)
        {
            StartCoroutine(CloseServer());
            OnServerStopGameplay();
        }
    }
 
    /// <summary>
    /// Server finish the round since all players have pressed the stop button
    /// </summary>
    void OnServerStopGameplay()
    {
        StopCoroutine("StopWatch");
 
        List<NetworkConnection> keysToUpdate = new List<NetworkConnection>();
        keysToUpdate.AddRange(playerInfos.Keys.ToArray());
 
        List<double> scores = new List<double>();
        for (int i = 0; i < keysToUpdate.Count; i++)
        {
            scores.Add(Mathf.Abs((float)(targetTime - playerInfos.Values.ToArray()[i].playerScoreTime)));
        }
 
        double currentHigherScore = 99999999.0f; // in this case the lower value is the winner
        for (int i = 0; i < scores.Count; i++)
        {
            if (scores[i] < currentHigherScore)
            {
                currentHigherScore = scores[i];
            }
        }
 
        int highscoreIndex = scores.FindIndex(x => x == currentHigherScore);
 
        for (int i = 0; i < keysToUpdate.Count; i++)
        {
            PlayerInfo playerInformation = playerInfos[keysToUpdate[i]];
            if (playerInformation.isPartyA == playerInfos[keysToUpdate[highscoreIndex]].isPartyA)
            {
                playerInformation.isWin = true;
            }
            else
            {
                playerInformation.isWin = false;
            }
            playerInfos[keysToUpdate[i]] = playerInformation;
        }
 
        foreach (NetworkConnection connection in playerInfos.Keys)
        {
            connection.Send(new ClientOnAllPlayerStopTime { allPlayerInfos = playerInfos.Values.ToArray(), targetTime = targetTime });
        }
    }
 
 
    /// <summary>
    /// Coroutine: Update loading countdown from 3 to 0
    /// </summary>
    /// <returns> wait for 1 second</returns>
    IEnumerator CountdownTimer()
    {
        for (int countdown = 3; countdown >= 0; countdown--)
        {
            foreach (NetworkConnection connection in playerInfos.Keys)
            {
                if (isServer)
                {
                    connection.Send(new ClientUpdateCountdownTimerMessage { time = countdown });
                }
            }
 
            yield return new WaitForSeconds(1.0f);
 
            if (countdown == 0)
            {
                // Set target time
 
                // random target time with range a to b seconds
                targetTime = Random.Range(3.0f, 9.0f);
 
                // send targetTime value to all client
                foreach (NetworkConnection connection in playerInfos.Keys)
                {
                    connection.Send(new ClientChangeToGameplayStateMessage { targetTime = targetTime });
                }
 
                StartCoroutine(MainTime());
            }
        }
    }
 
    /// <summary>
    /// Coroutine: Update current running mainTime
    /// </summary>
    /// <returns></returns>
    IEnumerator MainTime()
    {
        while (true)
        {
            mainTime += Time.deltaTime;
 
            foreach (NetworkConnection connection in playerInfos.Keys)
            {
                connection.Send(new ClientUpdateMainTimeMessage { mainTime = mainTime });
            }
 
            yield return null;
        }
    }
 
    /// <summary>
    /// Unregister server and close the server automatically after the time is timeout
    /// </summary>
    /// <param name="timeout"></param>
    /// <returns></returns>
    IEnumerator CloseServer(int timeout = 30)
    {
        Debug.Log("Start countdown to close server");
 
        for (int i = 0; i < timeout; i++)
        {
            yield return new WaitForSeconds(1.0f);
        }
 
        ArmadaHandler.UnregisterServer(isLocal);
    }
 
    /// <summary>
    /// On client start, change panel to ReadyPanel
    /// </summary>
    /// <param name="msg"> client's message</param>
    void OnStartClientResponse(ClientStartClientResponseMessage msg)
    {
        gameCanvas.ChangePanel(GameplayInterfaceState.Loading);
    }
 
    /// <summary>
    /// On loading countdown, update LoadingPanel's UI
    /// </summary>
    /// <param name="msg"></param>
    void OnUpdateCountdownTime(ClientUpdateCountdownTimerMessage msg)
    {
        gameCanvas.UpdateLoadingPanelUI(msg.time);
    }
 
    /// <summary>
    /// Change panel to GameplayPanel and start the game
    /// </summary>
    /// <param name="msg"></param>
    void OnChangeToGameplayState(ClientChangeToGameplayStateMessage msg)
    {
        gameCanvas.ChangePanel(GameplayInterfaceState.Gameplay);
        gameCanvas.UpdateTargetTimeUI(msg.targetTime);
    }
 
    /// <summary>
    /// On current mainTime update, update mainTime to its UI
    /// </summary>
    /// <param name="msg"></param>
    void OnUpdateMainTime(ClientUpdateMainTimeMessage msg)
    {
        gameCanvas.UpdateMainTimeUI(msg.mainTime);
    }
 
    /// <summary>
    /// On all players have pressed the stop button, finish the game
    /// </summary>
    /// <param name="msg"></param>
    void OnAllClientStopTime(ClientOnAllPlayerStopTime msg)
    {
        gameCanvas.ChangePanel(GameplayInterfaceState.Result);
        gameCanvas.UpdateResultPanelUI(msg.allPlayerInfos, msg.targetTime);
    }
 
    private void OnApplicationQuit()
    {
#if UNITY_SERVER
        ArmadaHandler.UnregisterServer(isLocal);
#endif
    }
}