# Mirror

### Mirror sur Edgegap

Ce guide vous aidera à créer un serveur headless sur Edgegap pour un projet Unity en utilisant [Mirror](https://mirror-networking.com/) comme solution réseau.

Ce guide utilisera le projet d'exemple open-source `Tanks`, qui est déjà disponible dans votre exemple Mirror, emplacement Assets/Mirror/Examples/Tanks.

L'exemple final peut être trouvé sur notre [GitHub](https://github.com/edgegap/netcode-sample-unity-mirror).

### Construire le serveur de jeu

Une fois votre jeu prêt, rendez-vous sur l'onglet `Build` de l'éditeur Unity, sous `File -> Build Settings` dans les menus supérieurs. Assurez-vous de sélectionner les bons paramètres selon votre version d'Unity.

* Avant la version 2021.2 :
  * Définir `Target Platform` sur `Linux`;
  * Définir `Architecture` sur `x86_64`;
  * Cochez l'option `Server Build` .
* Sinon :
  * Définir `Platform` sur `Dedicated Server`;
  * Définir `Target Platform` sur `Linux`.

Ensuite appuyez sur build et sélectionnez un nouveau dossier vide nommé `linux_server` comme destination de fichier. Transférez `linux_server` le dossier dans un second dossier vide, qui sera appelé `[SERVER BUILD]` dans ce document.

### Containerisation du serveur de jeu dédié

Nous allons créer une image docker contenant le serveur de jeu dédié dans cette partie. Vous pourriez également être intéressé par la lecture de [Unity Server in Docker](https://docs.edgegap.com/docs.edgegap.com-fr/docs/sample-projects/unity-netcodes/broken-reference).

Si vous avez besoin de plus d'informations sur Docker avec Edgegap, veuillez vous référer à [cette](https://docs.edgegap.com/docs.edgegap.com-fr/docs/tools-and-integrations/container/docker) documentation.

#### Dockerfile

```
FROM ubuntu:bionic
MAINTAINER <author_detail>

ARG debian_frontend=noninteractive
ARG docker_version=17.06.0-ce

RUN apt-get update && \
    apt-get install -y libglu1 xvfb libxcursor1 ca-certificates && \
    apt-get clean && \
    update-ca-certificates

EXPOSE 3389/TCP
EXPOSE [GAME PORT]/TCP
EXPOSE [GAME PORT]/UDP

COPY linux_server/ /root/linux_server/
COPY boot.sh /boot.sh

WORKDIR /root/
ENTRYPOINT ["/bin/bash", "/boot.sh"]
```

Notez le port que vous utilisez pour les communications réseau, appelé `[GAME PORT]`. Par défaut, le port utilisé est `7777`. Vous pouvez trouver cette information dans l'éditeur Unity, sur le `NetworkManager` objet de jeu, dans le `Transport` composant.

* Copiez les lignes ci-dessus et collez-les dans votre Dockerfile, placé à l'intérieur de `[SERVER BUILD]`. Modifiez les `[GAME PORT]` espaces réservés pour votre port de jeu.

Avoir le `[GAME PORT]` ouvert à la fois en TCP et UDP vous permet d'utiliser le transport que vous préférez dans le composant `NetworkManager` Mirror. Enfin, créez un fichier nommé `boot.sh` à la racine du `[SERVER BUILD]` dossier. Il sera exécuté lors du démarrage de l'image dans un conteneur.

* Copiez les deux lignes suivantes, assurez-vous de remplacer les `[YOUR GAME]` espaces réservés par le nom du fichier généré.

```
xvfb-run --auto-servernum --server-args='-screen 0 640X480X24:32' /root/build/[YOUR GAME].x86_64 -batchmode -nographics
```

À ce stade, vous devriez avoir la hiérarchie suivante :

> * `Dossier [SERVER BUILD]` > > - `Dockerfile` > > - `boot.sh` > > - `linux_server` dossier > > > - Fichiers générés par Unity

* Ouvrez une invite de commande dans le `[SERVER BUILD]` dossier, et exécutez les commandes Docker suivantes :

{% hint style="warning" %}
Pour les utilisateurs CPU ARM (Mac M1, M2, etc.), ajoutez `--platform linux/amd64`  option à votre commande de build.
{% endhint %}

#### Utilisation sous Linux

```bash
# construire l'image
docker build . -t <IMAGE_NAME>:<IMAGE_VERSION>

# connexion, un prompt demandera le mot de passe
docker login -u '<REGISTRY_USERNAME>' <REGISTRY_URL>

# ajouter un autre tag à votre image correspondant au registre
docker image tag <IMAGE_NAME>:<IMAGE_VERSION> <REGISTRY_URL>/<PROJECT_NAME>/<IMAGE_NAME>:<IMAGE_VERSION>

# pousser l'image
docker push <REGISTRY_URL>/<PROJECT_NAME>/<IMAGE_NAME>:<IMAGE_VERSION>
```

#### Utilisation de cmd

```bash
# construire l'image
docker build . -t <IMAGE_NAME>:<IMAGE_VERSION>

# connexion, un prompt demandera le mot de passe
docker login -u <REGISTRY_USERNAME> <REGISTRY_URL>

# ajouter un autre tag à votre image correspondant au registre
docker image tag <IMAGE_NAME>:<IMAGE_VERSION> <REGISTRY_URL>/<PROJECT_NAME>/<IMAGE_NAME>:<IMAGE_VERSION>

# pousser l'image
docker push <REGISTRY_URL>/<PROJECT_NAME>/<IMAGE_NAME>:<IMAGE_VERSION>
```

#### Utilisation de Powershell

```bash
# construire l'image
docker build . -t <IMAGE_NAME>:<IMAGE_VERSION>

# connexion, un prompt demandera le mot de passe
docker login -u '<REGISTRY_USERNAME>' <REGISTRY_URL>

# ajouter un autre tag à votre image correspondant au registre
docker image tag <IMAGE_NAME>:<IMAGE_VERSION> <REGISTRY_URL>/<PROJECT_NAME>/<IMAGE_NAME>:<IMAGE_VERSION>

# pousser l'image
docker push <REGISTRY_URL>/<PROJECT_NAME>/<IMAGE_NAME>:<IMAGE_VERSION>
```

Après ces commandes, vous devriez être en mesure de voir votre image téléchargée sur le site Edgegap si vous utilisez le registre de conteneurs Edgegap. Voir [ce doc](https://docs.edgegap.com/docs.edgegap.com-fr/learn/advanced-features/edgegap-container-registry) si vous souhaitez utiliser le registre Edgegap. Vous pouvez également utiliser un autre registre privé.

### Déployer sur Edgegap

Naviguez vers la page `Applications & Games` du site web. Cliquez sur le bouton `Create New` en haut à droite pour accéder au formulaire d'application. Voici les champs et comment les remplir correctement :

* Nom de l'application : Peut être n'importe quel nom distinctif que vous souhaitez utiliser pour reconnaître facilement votre application parmi d'autres.
* Image : Peut être n'importe quelle image spécifique que vous souhaitez utiliser pour reconnaître facilement votre application parmi d'autres.
* Nom de version : Vous pouvez utiliser un nom de version pour décrire la portée de la version que vous déployez. Des exemples peuvent être « demo », « production », « v1 », « v2 »
* Conteneur :
  * Registry : « \[URL] », où \[URL] est la valeur des identifiants que vous pouvez afficher sur la page du Container Repository.
  * Image repository : « \[PROJECT]/\[YOUR GAME] », où \[PROJECT] et \[YOUR GAME] sont les valeurs que vous avez utilisées précédemment lors du push de l'image docker.
  * Tag : « \[TAG] », où \[TAG] est la valeur que vous avez utilisée précédemment lors du push de l'image docker.
  * Cochez « Using a private repository »
  * Nom d'utilisateur du registre privé : « \[USERNAME] », où \[USERNAME] est la valeur de vos identifiants.
  * Jeton du registre privé : « \[TOKEN] », où \[TOKEN] est la valeur de vos identifiants.
  * Requirements : Laisser tel quel.
  * Ports :
    * Cliquez sur le lien `+ Add port` pour ajouter un nouveau port, et ajoutez les entrées suivantes :
      * `[GAME PORT]` - `TCP/UDP` - désactiver les Vérifications
      * 3389 - TCP - désactiver les Vérifications

Une fois votre application créée, vous pouvez appuyer sur le bouton `Deploy` pour procéder au déploiement de votre serveur de jeu. Choisissez la région où vous souhaitez déployer, et entrez le nombre de joueurs aléatoires que vous souhaitez générer selon votre jeu. Vérifiez que tout fonctionne correctement en vérifiant les éléments suivants :

* Le statut le plus récent doit être défini sur `Ready`.
* Dans l'onglet `Port Mapping` vous devriez voir le port que vous avez défini dans le formulaire de création de l'application :

<figure><img src="https://3008966946-files.gitbook.io/~/files/v0/b/gitbook-x-prod.appspot.com/o/spaces%2FsR0dHSFv9ymoC0DO5G8J%2Fuploads%2Fgit-blob-583d6356cdf560e89fc338bde5c125a81c77a9ee%2Fmirror-app-creation.png?alt=media" alt=""><figcaption></figcaption></figure>

### Ajouter un HUD d'exemple dans votre application cliente

* Définissez la valeur `Port` du composant `Transport` du `NetworkManager` sur le port externe défini dans l'onglet `Port Mapping` de votre déploiement.

Dans cet exemple, le port a été défini sur `31887`. Cela dépend principalement du jeu que vous développez et sera très probablement défini de manière programmatique dans la base de code du jeu.

* Définissez la valeur de `Network Address` du `Network Manager` sur l' `Host`de votre déploiement. Cette URL peut être trouvée dans le `Deployment Summary` sur le tableau de bord ou via l'API.

Dans cet exemple, l'adresse a été définie sur `0ace560706a5.pr.edgegap.net`. Encore une fois, cette valeur sera très probablement définie de manière programmatique lors de la communication du client avec le serveur maître/API responsable du processus de matchmaking.

Avec les bonnes informations, vous devriez pouvoir vous connecter normalement au serveur de jeu et commencer à jouer immédiatement.

Vous avez maintenant un projet Mirror prêt à être déployé à la demande !

Avec des déploiements basés sur les sièges, il est possible d'utiliser Mirror pour créer un système qui supprime automatiquement les sessions Edgegap orphelines une fois qu'un joueur se déconnecte du serveur, en utilisant `NetworkManager` fonctions de rappel, et un `NetworkBehaviour` script attaché au prefab du joueur qui utilise une fonction RPC (Remote Procedure Call) et une fonction Command.

Lorsque le serveur démarre, le `NetworkManager` récupère la liste des `session IDs` liés à son déploiement depuis l'API Edgegap et les stocke. Ensuite, lorsqu'un nouveau joueur se connecte au serveur, une fonction côté client est initiée via `RPC` qui enverra l' `adresse IP du joueur` au serveur via une `command`. Avec l'IP du joueur, le serveur vérifie s'il existe une IP correspondante dans les données de chaque session ; Le serveur obtient les données de session en utilisant son ID mis en cache via l'API Edgegap. Si une correspondance est trouvée, le `session ID` est mappé à la `NetworkConnectionToClient`.

du joueur. Comme de nouvelles sessions peuvent être ajoutées après le démarrage du serveur, la liste des session IDs est mise à jour et les nouvelles sessions sont vérifiées si une correspondance n'a pas pu être trouvée la première fois.

Enfin, une fois qu'un joueur se déconnecte du serveur, le serveur utilise l' `NetworkConnectionToClient` du joueur pour récupérer leur `session ID`associée, puis utilise l'API Edgegap pour supprimer cette session. Cela libère une socket dans le déploiement pour qu'un nouveau joueur puisse rejoindre.

#### PlayerNetworkBehaviour

```cs
public class PlayerNetworkBehaviour : NetworkBehaviour
{
    [ClientRpc]
    public void RpcSendClientIpAddress()
    {
        StartCoroutine(FetchPublicIpAndSendToServer());
    }

    private IEnumerator FetchPublicIpAndSendToServer(" alt=""><figcaption></figcaption></figure>
    {
        UnityWebRequest request = UnityWebRequest.Get("https://api.ipify.org?format=json");
        yield return request.SendWebRequest();

        if (request.result == UnityWebRequest.Result.Success)
        {
            string responseText = request.downloadHandler.text;
            string clientIp = JObject.Parse(responseText)["ip"].ToString();
            CmdSendIpAddressToServer(clientIp);
        }
        else
        {
            Debug.LogError("Failed to fetch public IP address.");
        }
    }

    [Command]
    public void CmdSendIpAddressToServer(string clientIp)
    {
        NetworkConnectionToClient conn = connectionToClient;
        if (conn != null)
        {
            CustomNetworkManager.Instance.StoreClientIpAddress(conn, clientIp);
        }
        else
        {
            Debug.LogError("Connection not found.");
        }
    }
}
```

#### CustomNetworkManager

```cs
public class CustomNetworkManager : NetworkManager
{
    private static readonly string edgegapUrl = "https://api.edgegap.com/v1";
    private string apiToken;
    private string deploymentId;
    private Dictionary<NetworkConnectionToClient, string> connectionToSessionIdMap = new Dictionary<NetworkConnectionToClient, string>();
    private Dictionary<NetworkConnectionToClient, bool> mappingInProgress = new Dictionary<NetworkConnectionToClient, bool>();

    private ArrayList currentDeploymentSessions = new();
    private ArrayList newSessions = new();

    public static CustomNetworkManager Instance;

    public override void Awake()
    {
        base.Awake();
        Instance = this;
    }

    public override void OnStartServer()
    {
        base.OnStartServer();
        deploymentId = System.Environment.GetEnvironmentVariable("ARBITRIUM_REQUEST_ID");
        apiToken = System.Environment.GetEnvironmentVariable("API_TOKEN");
        if (string.IsNullOrEmpty(apiToken))
        {
            Debug.LogError("Edgegap API token not found in environment variables.");
        }

        StartCoroutine(GetNewSessionList());
    }

    public override void OnServerAddPlayer(NetworkConnectionToClient conn)
    {
        base.OnServerAddPlayer(conn);
        Debug.Log($"Adding player for connection: {conn.connectionId}");

        // Récupérer le PlayerNetworkBehaviour depuis l'objet joueur
        PlayerNetworkBehaviour playerNetworkBehaviour = conn.identity.GetComponent<PlayerNetworkBehaviour>();
        playerNetworkBehaviour.RpcSendClientIpAddress();
    }

    public void StoreClientIpAddress(NetworkConnectionToClient conn, string clientIp)
    {
        mappingInProgress[conn] = true; // Définir le mappage en cours sur true
        Debug.Log($"Stored IP address for connection {conn.connectionId}: {clientIp}");

        // Commencer à mapper la session à la connexion
        StartCoroutine(CheckCachedSessions(conn, clientIp));
    }

    private IEnumerator GetNewSessionList()
    {
        newSessions.Clear();
        string url = $"{edgegapUrl}/status/{deploymentId}";
        UnityWebRequest request = UnityWebRequest.Get(url);
        request.SetRequestHeader("Authorization", apiToken);

        yield return request.SendWebRequest();

        if (request.result == UnityWebRequest.Result.Success)
        {
            string responseText = request.downloadHandler.text;
            Debug.Log("Deployment Status Response: " + responseText);

            var json = JObject.Parse(responseText);
            var sessions = json["sessions"] as JArray;

            foreach (var session in sessions)
            {
                string sessionId = session["session_id"]?.ToString();

                if (!currentDeploymentSessions.Contains(sessionId))
                {
                    newSessions.Add(sessionId);
                    currentDeploymentSessions.Add(sessionId);
                }
            }

            Debug.Log("New session list initialized");
        }
    }

    private IEnumerator CheckCachedSessions(NetworkConnectionToClient conn, string clientAddress)
    {
        foreach (string sessionId in currentDeploymentSessions)
        {
            yield return GetSessionAndMap(conn, sessionId, clientAddress);

            if (connectionToSessionIdMap.ContainsKey(conn))
            {
                break;
            }
        }

        if (!connectionToSessionIdMap.ContainsKey(conn))
        {
            Debug.Log("Could not map player with current cached sessions");
            yield return GetNewSessionList();

            foreach (string sessionId in newSessions)
            {
                yield return GetSessionAndMap(conn, sessionId, clientAddress);

                if (connectionToSessionIdMap.ContainsKey(conn))
                {
                    break;
                }
            }
        }
    }

    private IEnumerator GetSessionAndMap(NetworkConnectionToClient conn, string sessionId, string clientAddress)
    {
        Debug.Log($"Fetching session details for session ID: {sessionId}");
        string url = $"{edgegapUrl}/session/{sessionId}";
        UnityWebRequest request = UnityWebRequest.Get(url);
        request.SetRequestHeader("Authorization", apiToken);

        yield return request.SendWebRequest();

        if (request.result == UnityWebRequest.Result.Success)
        {
            Debug.Log("Session fetched successfully.");
            string responseText = request.downloadHandler.text;
            Debug.Log("Session Response: " + responseText);

            var session = JObject.Parse(responseText);
            var sessionUsers = session["session_users"] as JArray;

            if (sessionUsers != null && sessionUsers.Count > 0)
            {
                Debug.Log($"Found {sessionUsers.Count} users in session.");
                foreach (var user in sessionUsers)
                {
                    string playerIp = user["ip"]?.ToString();
                    Debug.Log($"Player IP: {playerIp}, Connection IP: {clientAddress}");
                    if (playerIp == clientAddress)
                    {
                        connectionToSessionIdMap[conn] = sessionId;
                        Debug.Log($"Mapped session ID {sessionId} to connection {conn.connectionId} with IP {playerIp}");

                        // Journalisation de confirmation supplémentaire
                        if (connectionToSessionIdMap.ContainsKey(conn))
                        {
                            Debug.Log($"Session ID {sessionId} successfully stored for connection {conn.connectionId} with IP {playerIp}");
                        }
                        else
                        {
                            Debug.LogError($"Failed to store session ID {sessionId} for connection {conn.connectionId} with IP {playerIp}");
                        }

                        break;
                    }
                }
            }
            else
            {
                Debug.LogError("No users found in session.");
            }
        }
        else
        {
            Debug.LogError($"Error fetching session: {request.error}");
            Debug.LogError($"Response Code: {request.responseCode}");
            Debug.LogError($"Response Text: {request.downloadHandler.text}");
        }

        mappingInProgress[conn] = false; // Définir le mappage en cours sur false une fois terminé
    }

    public override void OnServerDisconnect(NetworkConnectionToClient conn)
    {
        base.OnServerDisconnect(conn);
        Debug.Log($"Server disconnected client: {conn.connectionId}");

        StartCoroutine(HandleDisconnect(conn));
    }

    private IEnumerator HandleDisconnect(NetworkConnectionToClient conn)
    {
        while (mappingInProgress.ContainsKey(conn) && mappingInProgress[conn])
        {
            Debug.Log($"Waiting for session mapping to complete for connection {conn.connectionId}...");
            yield return new WaitForSeconds(0.1f);
        }

        if (connectionToSessionIdMap.TryGetValue(conn, out string sessionId))
        {
            Debug.Log($"Deleting session ID: {sessionId}");
            StartCoroutine(DeleteSession(sessionId));
            connectionToSessionIdMap.Remove(conn);
            mappingInProgress.Remove(conn);
            currentDeploymentSessions.Remove(sessionId);
        }
        else
        {
            Debug.LogWarning("No session ID found for this connection.");
        }
    }

    private IEnumerator DeleteSession(string sessionId)
    {
        Debug.Log($"Sending request to delete session ID: {sessionId}");
        string url = $"{edgegapUrl}/session/{sessionId}";
        UnityWebRequest request = UnityWebRequest.Delete(url);
        request.SetRequestHeader("Authorization", apiToken);

        yield return request.SendWebRequest();

        if (request.result == UnityWebRequest.Result.Success)
        {
            Debug.Log("Session deleted successfully.");
        }
        else
        {
            Debug.LogError($"Error deleting session: {request.error}");
            Debug.LogError($"Response Code: {request.responseCode}");
            Debug.LogError($"Response Text: {request.downloadHandler.text}");
        }
    }

}
```

#### Fonctionnalités optionnelles

Ce script peut être étendu avec des fonctionnalités optionnelles et indépendantes qui aident à gérer les sessions dans des cas spécifiques.

Par exemple, un `délai d'attente` peut être défini pour supprimer `sessions de sièges vides` après un délai configurable `suite à l'initialisation complète du serveur`. C'est une fonctionnalité utile dans le cas d'un joueur qui abandonne avant que son ticket de matchmaking soit résolu, ce qui créerait une session vide une fois qu'un match est trouvé. Si le joueur n'envoie jamais son adresse IP pour être mappée à l'ID de session avant l'expiration du délai, alors cette session est supprimée pour libérer une socket.

Une autre fonctionnalité peut être de `déconnecter les joueurs inactifs` après une longue période afin de libérer des sockets. Une fois connecté au serveur, le client doit `envoyer un message de heartbeat minimaliste` au serveur `toutes les quelques secondes`, sinon il est déconnecté si trop de `heartbeats sont manqués d'affilée`. À la fois le temps entre les heartbeats et le nombre maximum de messages pouvant être manqués d'affilée peuvent être configurés, en fonction des besoins du jeu. Dans cet exemple, un heartbeat est envoyé toutes les quelques secondes si le tank se déplace sur la carte ou tire.

#### PlayerNetworkBehaviour - Gestion des sessions de sièges

```cs
    public class PlayerNetworkBehaviour : NetworkBehaviour
    {
        private bool enableInactiveTimeout;
        private float clientTimeSinceLastHeartbeat = 0;
        private float secondsBetweenHeartbeats;

        [ClientRpc]
        public void RpcSendClientIpAddress()
        {
            StartCoroutine(FetchPublicIpAndSendToServer());
            secondsBetweenHeartbeats = CustomNetworkManager.Instance.GetSecondsBetweenHeartbeats();
            enableInactiveTimeout = CustomNetworkManager.Instance.IsEnableInactiveTimeout();
            clientTimeSinceLastHeartbeat = secondsBetweenHeartbeats;
        }

        ...

        private void Update()
        {
            if (isLocalPlayer && enableInactiveTimeout)
            {
                float horizontal = Input.GetAxis("Horizontal");
                float vertical = Input.GetAxis("Vertical");

                if (clientTimeSinceLastHeartbeat >= secondsBetweenHeartbeats && (Input.GetKeyDown(KeyCode.Space) || horizontal != 0 || vertical != 0))
                {
                    CmdSendHeartbeatTimeToServer(NetworkTime.predictedTime);
                    clientTimeSinceLastHeartbeat = 0;
                }

                clientTimeSinceLastHeartbeat += Time.deltaTime;
            }
        }

        [Command]
        public void CmdSendHeartbeatTimeToServer(double time)
        {
            NetworkConnectionToClient conn = connectionToClient;
            if (conn != null)
            {
                CustomNetworkManager.Instance.StoreClientHeartbeatTime(conn, time);
            }
            else
            {
                Debug.LogError("Connection not found.");
            }
        }
    }
```

#### CustomNetworkManager - Gestion des sessions de sièges

```cs
public class CustomNetworkManager : NetworkManager
{
    ...
    private bool listInProgress = false;

    [Header("Empty Session Timeout")]
    [SerializeField] private bool enableEmptyTimeout;
    [SerializeField] private float deleteAfterSeconds = 15f;

    [Header("Inactive Player Timeout")]
    [SerializeField] private bool enableInactiveTimeout;
    [SerializeField] private int maxMissedHeartbeats = 3;
    [SerializeField] private float secondsBetweenHeartbeats = 3f;
    private Dictionary<NetworkConnectionToClient, double> connectionToLastHeartbeatMap = new();

    ...

    public override void OnStartServer()
    {
        base.OnStartServer();
        deploymentId = Environment.GetEnvironmentVariable("ARBITRIUM_REQUEST_ID");
        apiToken = Environment.GetEnvironmentVariable("API_TOKEN");
        if (string.IsNullOrEmpty(apiToken))
        {
            Debug.LogError("Edgegap API token not found in environment variables.");
        }
        else
        {
            Debug.Log("Edgegap API token found: " + apiToken);
        }

        StartCoroutine(InitiateSessionManage());
    }

    private IEnumerator InitiateSessionManage()
    {
        yield return new WaitForSeconds(0.5f);

        StartCoroutine(GetNewSessionList());

        if (enableEmptyTimeout)
        {
            StartCoroutine(StartServerInitSessionsTimeout());
        }
    }

    ...

    private IEnumerator GetNewSessionList()
    {
        listInProgress = true;
        newSessions.Clear();
        string url = $"{edgegapUrl}/status/{deploymentId}";
        UnityWebRequest request = UnityWebRequest.Get(url);
        request.SetRequestHeader("Authorization", apiToken);

        yield return request.SendWebRequest();

        if (request.result == UnityWebRequest.Result.Success)
        {
            string responseText = request.downloadHandler.text;
            Debug.Log("Deployment Status Response: " + responseText);

            var json = JObject.Parse(responseText);
            var sessions = json["sessions"] as JArray;

            foreach (var session in sessions)
            {
                string sessionId = session["session_id"]?.ToString();

                if (!currentDeploymentSessions.Contains(sessionId))
                {
                    newSessions.Add(sessionId);
                    currentDeploymentSessions.Add(sessionId);
                }
            }

            Debug.Log("New session list initialized");
        }
        else
        {
            Debug.LogError($"Error fetching session list: {request.error}");
            Debug.LogError($"Response Code: {request.responseCode}");
            Debug.LogError($"Response Text: {request.downloadHandler.text}");
        }

        listInProgress = false;
    }

    ...

    private IEnumerator GetSessionAndMap(NetworkConnectionToClient conn, string sessionId, string clientAddress)
    {
        Debug.Log($"Fetching session details for session ID: {sessionId}");
        string url = $"{edgegapUrl}/session/{sessionId}";
        UnityWebRequest request = UnityWebRequest.Get(url);
        request.SetRequestHeader("Authorization", apiToken);

        yield return request.SendWebRequest();

        if (request.result == UnityWebRequest.Result.Success)
        {
            Debug.Log("Session fetched successfully.");
            string responseText = request.downloadHandler.text;
            Debug.Log("Session Response: " + responseText);

            var session = JObject.Parse(responseText);
            var sessionUsers = session["session_users"] as JArray;

            if (sessionUsers != null && sessionUsers.Count > 0)
            {
                Debug.Log($"Found {sessionUsers.Count} users in session.");
                foreach (var user in sessionUsers)
                {
                    string playerIp = user["ip"]?.ToString();
                    Debug.Log($"Player IP: {playerIp}, Connection IP: {clientAddress}");
                    if (playerIp == clientAddress)
                    {
                        connectionToSessionIdMap[conn] = sessionId;
                        Debug.Log($"Mapped session ID {sessionId} to connection {conn.connectionId} with IP {playerIp}");

                        if (enableInactiveTimeout)
                        {
                            connectionToLastHeartbeatMap[conn] = conn.lastMessageTime;
                            StartCoroutine(CheckClientHeartbeat(conn));
                        }

                        // Journalisation de confirmation supplémentaire
                        if (connectionToSessionIdMap.ContainsKey(conn))
                        {
                            Debug.Log($"Session ID {sessionId} successfully stored for connection {conn.connectionId} with IP {playerIp}");
                        }
                        else
                        {
                            Debug.LogError($"Failed to store session ID {sessionId} for connection {conn.connectionId} with IP {playerIp}");
                        }

                        break;
                    }
                }
            }
            else
            {
                Debug.LogError("No users found in session.");
            }
        }
        else
        {
            Debug.LogError($"Error fetching session: {request.error}");
            Debug.LogError($"Response Code: {request.responseCode}");
            Debug.LogError($"Response Text: {request.downloadHandler.text}");
        }

        mappingInProgress[conn] = false; // Définir le mappage en cours sur false une fois terminé
    }

    ...

    #region EmptySessionTimeout

    private IEnumerator StartServerInitSessionsTimeout()
    {
        while (listInProgress)
        {
            Debug.Log($"Waiting to retrieve session list...");
            yield return new WaitForSeconds(0.1f);
        }

        foreach (string sessionId in currentDeploymentSessions)
        {
            StartCoroutine(WaitForConnectionTimeout(sessionId));
        }
    }

    private IEnumerator WaitForConnectionTimeout(string sessionID)
    {
        Debug.Log($"Checking for empty timeout on session {sessionID}");
        bool delete = false;
        DateTime timeout = DateTime.Now.AddSeconds(deleteAfterSeconds);

        while (!connectionToSessionIdMap.ContainsValue(sessionID))
        {
            yield return new WaitForSeconds(0.1f);
            if (DateTime.Now >= timeout)
            {
                delete = true;
                break;
            }
        }

        if (delete && !connectionToSessionIdMap.ContainsValue(sessionID))
        {
            Debug.Log($"No connection initiated by client after {deleteAfterSeconds}s, deleting session {sessionID}");
            StartCoroutine(DeleteSession(sessionID));
            currentDeploymentSessions.Remove(sessionID);
            newSessions.Remove(sessionID);
        }
    }

    #endregion

    #region InactivePlayerTimeout

    private IEnumerator CheckClientHeartbeat(NetworkConnectionToClient conn)
    {
        int counter = 0;

        while (counter < maxMissedHeartbeats && connectionToSessionIdMap.ContainsKey(conn))
        {
            double lastHeartbeatDiff = NetworkTime.localTime - connectionToLastHeartbeatMap[conn];

            if (lastHeartbeatDiff >= secondsBetweenHeartbeats)
            {
                counter += 1;

                Debug.Log($"Connection {conn.connectionId} has missed {counter} heartbeat(s) in a row");
            }
            else
            {
                counter = 0;
                Debug.Log($"Reset heartbeat counter for connection {conn.connectionId}");
            }

            yield return new WaitForSeconds(secondsBetweenHeartbeats);
        }

        if (counter >= maxMissedHeartbeats && conn.isReady)
        {
            Debug.Log($"Connection {conn.connectionId} inactive for too long, disconnecting");
            conn.Disconnect();
            connectionToLastHeartbeatMap.Remove(conn);
        }
    }

    public void StoreClientHeartbeatTime(NetworkConnectionToClient conn, double time)
    {
        connectionToLastHeartbeatMap[conn] = time;
    }

    public float GetSecondsBetweenHeartbeats() => secondsBetweenHeartbeats;

    public bool IsEnableInactiveTimeout() => enableInactiveTimeout;

    #endregion
}
```
