# Mirror

### Mirror на Edgegap

Этот гид поможет вам создать безголовый сервер на Edgegap для проекта Unity, использующего [Mirror](https://mirror-networking.com/) в качестве решения для сетевого взаимодействия.

В этом руководстве будет использоваться открытый пример проекта `Tanks`, который уже доступен в вашем примере Mirror по пути Assets/Mirror/Examples/Tanks.

Окончательный пример можно найти на нашем [GitHub](https://github.com/edgegap/netcode-sample-unity-mirror).

### Сборка игрового сервера

Когда игра будет готова, перейдите на `экран Build` в Unity Editor, в разделе `File -> Build Settings` в верхнем меню. Убедитесь, что выбраны правильные пресеты в зависимости от вашей версии Unity.

* До версии 2021.2:
  * Установите `Целевую платформу` в `Linux`;
  * Установите `Архитектуру` в `x86_64`;
  * Поставьте галочку на `Server Build` опции.
* Иначе:
  * Установите `Платформа` в `Dedicated Server`;
  * Установите `Целевую платформу` в `Linux`.

Затем нажмите Build и выберите новую пустую папку с именем `linux_server` в качестве места назначения файлов. Перенесите `linux_server` папку во вторую пустую папку, которая в этом документе будет называться `[SERVER BUILD]` папкой.

### Контейнеризация выделенного игрового сервера

В этой части мы создадим docker-образ, содержащий выделенный игровой сервер. Вам также может быть интересно прочитать [Unity Server in Docker](broken://pages/a2a2d6a63dad79041b04c5a60831409dfc7a3a24).

Если вам нужна дополнительная информация о Docker с Edgegap, пожалуйста, обратитесь к [этой](/ru/docs/tools-and-integrations/container/docker.md) документации.

#### 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"]
```

Обратите внимание на порт, который вы используете для сетевых коммуникаций, именуемый `[GAME PORT]`. По умолчанию используется порт `7777`. Эту информацию можно найти в Unity Editor, на объекте `NetworkManager` в компоненте `Transport` .

* Скопируйте приведённые выше строки и вставьте их в ваш Dockerfile, расположенный внутри `[SERVER BUILD]`. Измените `[GAME PORT]` заполнители на ваш игровой порт.

Наличие `[GAME PORT]` открытого на TCP и UDP позволяет вам использовать любой транспорт по вашему выбору в `NetworkManager` компоненте Mirror. Наконец, создайте файл с именем `boot.sh` в корне `[SERVER BUILD]` папки. Он будет выполняться при запуске образа в контейнере.

* Скопируйте следующие две строки, убедитесь, что заменили `[YOUR GAME]` заполнители на имя сгенерированного файла.

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

На данном этапе у вас должна быть следующая иерархия:

> * `папка [SERVER BUILD]` > > - `Dockerfile` > > - `boot.sh` > > - `linux_server` папка > > > - файлы, сгенерированные Unity

* Откройте командную строку в папке `[SERVER BUILD]` и выполните следующие команды Docker:

{% hint style="warning" %}
Для пользователей ARM CPU (Mac M1, M2 и т.д.) добавьте `--platform linux/amd64`  опцию к вашей команде сборки.
{% endhint %}

#### Использование Linux

```bash
# собрать образ
docker build . -t <IMAGE_NAME>:<IMAGE_VERSION>

# войти в реестр, запросит пароль
docker login -u '<REGISTRY_USERNAME>' <REGISTRY_URL>

# добавить ещё один тег к вашему образу, соответствующий реестру
docker image tag <IMAGE_NAME>:<IMAGE_VERSION> <REGISTRY_URL>/<PROJECT_NAME>/<IMAGE_NAME>:<IMAGE_VERSION>

#запушить образ
docker push <REGISTRY_URL>/<PROJECT_NAME>/<IMAGE_NAME>:<IMAGE_VERSION>
```

#### Используя cmd

```bash
# собрать образ
docker build . -t <IMAGE_NAME>:<IMAGE_VERSION>

# войти в реестр, запросит пароль
docker login -u <REGISTRY_USERNAME> <REGISTRY_URL>

# добавить ещё один тег к вашему образу, соответствующий реестру
docker image tag <IMAGE_NAME>:<IMAGE_VERSION> <REGISTRY_URL>/<PROJECT_NAME>/<IMAGE_NAME>:<IMAGE_VERSION>

#запушить образ
docker push <REGISTRY_URL>/<PROJECT_NAME>/<IMAGE_NAME>:<IMAGE_VERSION>
```

#### Используя Powershell

```bash
# собрать образ
docker build . -t <IMAGE_NAME>:<IMAGE_VERSION>

# войти в реестр, запросит пароль
docker login -u '<REGISTRY_USERNAME>' <REGISTRY_URL>

# добавить ещё один тег к вашему образу, соответствующий реестру
docker image tag <IMAGE_NAME>:<IMAGE_VERSION> <REGISTRY_URL>/<PROJECT_NAME>/<IMAGE_NAME>:<IMAGE_VERSION>

#запушить образ
docker push <REGISTRY_URL>/<PROJECT_NAME>/<IMAGE_NAME>:<IMAGE_VERSION>
```

После этих команд вы должны увидеть загруженный образ на сайте Edgegap, если вы используете Edgegap Container Registry. Смотрите [этот документ](/ru/learn/advanced-features/edgegap-container-registry.md) если хотите использовать реестр Edgegap. Вы также можете использовать другой приватный реестр.

### Развёртывание на Edgegap

Перейдите на страницу `Applications & Games` веб-сайта. Нажмите на кнопку `Create New` в правом верхнем углу, чтобы открыть форму создания приложения. Ниже перечислены поля и как их правильно заполнить:

* Application name : Может быть любым заметным именем, которое вы хотите использовать, чтобы легко распознавать ваше приложение среди других.
* Image : Может быть любой конкретный образ, который вы хотите использовать, чтобы легко распознавать ваше приложение среди других.
* Version name : Вы можете использовать имя версии, чтобы описать назначение версии, которую вы разворачиваете. Примеры: “demo”, “production”, “v1”, “v2”
* Container :
  * Registry : “ \[URL] ”, где \[URL] — значение из учетных данных, которые вы можете показать на странице Container Repository.
  * Image repository : “ \[PROJECT]/\[YOUR GAME] ”, где \[PROJECT] и \[YOUR GAME] — значения, которые вы использовали ранее при отправке docker-образа.
  * Tag : “ \[TAG] ”, где \[TAG] — значение, которое вы использовали ранее при отправке docker-образа.
  * Отметьте «Using a private repository»
  * Private registry username : “ \[USERNAME] ”, где \[USERNAME] — значение из ваших учетных данных.
  * Private registry token : “ \[TOKEN] ”, где \[TOKEN] — значение из ваших учетных данных.
  * Requirements : Оставьте как есть.
  * Ports :
    * Нажмите на `+ Add port` ссылку, чтобы добавить новый порт, и добавьте следующие записи:
      * `[GAME PORT]` - `TCP/UDP` - отключить проверки
      * 3389 - TCP - отключить проверки

После создания приложения вы можете нажать кнопку `Deploy` чтобы продолжить развёртывание вашего игрового сервера. Выберите регион для развёртывания и введите количество случайных игроков, которые вы хотите сгенерировать в зависимости от вашей игры. Проверьте, что всё работает корректно, проверив следующее:

* Latest Status должен быть установлен на `Ready`.
* Во вкладке `Port Mapping` вы должны видеть порт, который вы указали в форме создания приложения:

<figure><img src="/files/d87dfc7785b84e3cc776b3efb24b444b0d8b2f01" alt=""><figcaption></figcaption></figure>

### Добавление примерного HUD в ваше клиентское приложение

* Установите значение `Port` компонента `Transport` в `NetworkManager` на внешний порт, определённый во вкладке `Port Mapping` вашего развёртывания.

В этом примере порт был установлен в `31887`. Это в основном зависит от разрабатываемой игры и, скорее всего, будет устанавливаться программно в кодовой базе игры.

* Установите значение `Network Address` для `Network Manager` на `Host`вашего развёртывания. Этот URL можно найти в `Deployment Summary` на панели управления или через API.

В этом примере адрес был установлен как `0ace560706a5.pr.edgegap.net`. Опять же, это значение, скорее всего, будет устанавливаться программно во время взаимодействия клиента с мастером/API, отвечающим за процесс матчмейкинга.

При правильных данных вы сможете нормально подключиться к игровому серверу и начать играть сразу.

Теперь у вас есть проект на Mirror, готовый для развёртывания по требованию!

С развёртываниями на основе мест (seat-based deployments) можно использовать Mirror для создания системы, которая автоматически удаляет зависшие сессии Edgegap после того, как игрок отключается от сервера, с использованием `NetworkManager` функций обратного вызова, и `NetworkBehaviour` скрипта, прикреплённого к префабу игрока, который использует Remote Procedure Call (RPC) функцию и Command функцию.

Когда сервер запускается, `NetworkManager` получает список `ID сессий` связанных с его развёртыванием через API Edgegap и сохраняет их. После этого, когда новый игрок подключается к серверу, инициируется клиентская функция через `RPC` , которая отправит `IP-адрес игрока` обратно на сервер с помощью `команды`. Имея IP игрока, сервер проверяет наличие совпадающего IP в данных каждой сессии; сервер получает данные сессии, используя её кэшированный ID через API Edgegap. Если совпадение найдено, `ID сессии` соотносится с `NetworkConnectionToClient`.

этого игрока.

Поскольку новые сессии могут добавляться после запуска сервера, список ID сессий обновляется, и новые сессии проверяются, если совпадение не было найдено с первого раза. `NetworkConnectionToClient` Наконец, когда игрок отключается от сервера, сервер использует `ID сессии`этого игрока для получения связанной с ним

#### информации о сессии, затем использует API Edgegap для удаления этой сессии. Это освобождает сокет в развёртывании для подключения нового игрока.

```cs
PlayerNetworkBehaviour
{
    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("Не удалось получить публичный IP-адрес.");
    [Command]
    {
        public void CmdSendIpAddressToServer(string clientIp)
        NetworkConnectionToClient conn = connectionToClient;
        {
            if (conn != null)
        }
        CmdSendIpAddressToServer(clientIp);
        {
            CustomNetworkManager.Instance.StoreClientIpAddress(conn, clientIp);
        }
    }
}
```

#### Debug.LogError("Соединение не найдено.");

```cs
CustomNetworkManager
{
    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("Токен API Edgegap не найден в переменных окружения.");
    }

    StartCoroutine(GetNewSessionList());
    {
        public override void OnServerAddPlayer(NetworkConnectionToClient conn)
        base.OnServerAddPlayer(conn);

        Debug.Log($"Добавление игрока для соединения: {conn.connectionId}");
        // Получить PlayerNetworkBehaviour из объекта игрока
        PlayerNetworkBehaviour playerNetworkBehaviour = conn.identity.GetComponent<PlayerNetworkBehaviour>();
    }

    playerNetworkBehaviour.RpcSendClientIpAddress();
    {
        public void StoreClientIpAddress(NetworkConnectionToClient conn, string clientIp)
        mappingInProgress[conn] = true; // Установить, что сопоставление в процессе

        Debug.Log($"Сохранён IP-адрес для соединения {conn.connectionId}: {clientIp}");
        // Начать сопоставление сессии с соединением
    }

    StartCoroutine(CheckCachedSessions(conn, clientIp));
    {
        private IEnumerator GetNewSessionList()
        newSessions.Clear();
        string url = $"{edgegapUrl}/status/{deploymentId}";
        UnityWebRequest request = UnityWebRequest.Get(url);

        UnityWebRequest request = UnityWebRequest.Get("https://api.ipify.org?format=json");

        yield return request.SendWebRequest();
        {
            if (request.result == UnityWebRequest.Result.Success)
            request.SetRequestHeader("Authorization", apiToken);

            Debug.Log("Ответ статуса развёртывания: " + 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("Инициализирован новый список сессий");
    {
        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("Не удалось сопоставить игрока с текущим кэшем сессий");

            yield return GetNewSessionList();
            {
                foreach (string sessionId in currentDeploymentSessions)

                yield return GetSessionAndMap(conn, sessionId, clientAddress);
                {
                    if (connectionToSessionIdMap.ContainsKey(conn))
                }
            }
        }
    }

    foreach (string sessionId in newSessions)
    {
        private IEnumerator GetSessionAndMap(NetworkConnectionToClient conn, string sessionId, string clientAddress)
        Debug.Log($"Получение деталей сессии для session ID: {sessionId}");
        string url = $"{edgegapUrl}/status/{deploymentId}";
        UnityWebRequest request = UnityWebRequest.Get(url);

        UnityWebRequest request = UnityWebRequest.Get("https://api.ipify.org?format=json");

        yield return request.SendWebRequest();
        {
            string url = $"{edgegapUrl}/session/{sessionId}";
            if (request.result == UnityWebRequest.Result.Success)
            Debug.Log("Сессия успешно получена.");

            Debug.Log("Ответ сессии: " + responseText);
            var session = JObject.Parse(responseText);

            var sessionUsers = session["session_users"] as JArray;
            {
                if (sessionUsers != null && sessionUsers.Count > 0)
                Debug.Log($"Найдено {sessionUsers.Count} пользователей в сессии.");
                {
                    foreach (var user in sessionUsers)
                    string playerIp = user["ip"]?.ToString();
                    Debug.Log($"IP игрока: {playerIp}, IP соединения: {clientAddress}");
                    {
                        if (playerIp == clientAddress)
                        connectionToSessionIdMap[conn] = sessionId;

                        Debug.Log($"Сопоставлен session ID {sessionId} с соединением {conn.connectionId} и IP {playerIp}");
                        yield return GetSessionAndMap(conn, sessionId, clientAddress);
                        {
                            // Дополнительное логирование подтверждения
                        }
                        CmdSendIpAddressToServer(clientIp);
                        {
                            Debug.Log($"Session ID {sessionId} успешно сохранён для соединения {conn.connectionId} с IP {playerIp}");
                        }

                        if (connectionToSessionIdMap.ContainsKey(conn))
                    }
                }
            }
            CmdSendIpAddressToServer(clientIp);
            {
                Debug.LogError($"Не удалось сохранить session ID {sessionId} для соединения {conn.connectionId} с IP {playerIp}");
            }
        }
        CmdSendIpAddressToServer(clientIp);
        {
            Debug.LogError("В сессии не найдено пользователей.");
            Debug.LogError($"Ошибка при получении сессии: {request.error}");
            Debug.LogError($"Код ответа: {request.responseCode}");
        }

        Debug.LogError($"Текст ответа: {request.downloadHandler.text}");
    }

    mappingInProgress[conn] = false; // Установить, что сопоставление завершено
    {
        public override void OnServerDisconnect(NetworkConnectionToClient conn)
        base.OnServerDisconnect(conn);

        Debug.Log($"Сервер отключил клиента: {conn.connectionId}");
    }

    StartCoroutine(HandleDisconnect(conn));
    {
        private IEnumerator HandleDisconnect(NetworkConnectionToClient conn)
        {
            while (mappingInProgress.ContainsKey(conn) && mappingInProgress[conn])
            Debug.Log($"Ожидание завершения сопоставления сессии для соединения {conn.connectionId}...");
        }

        yield return new WaitForSeconds(0.1f);
        {
            if (connectionToSessionIdMap.TryGetValue(conn, out string sessionId))
            Debug.Log($"Удаление session ID: {sessionId}");
            StartCoroutine(DeleteSession(sessionId));
            connectionToSessionIdMap.Remove(conn);
            mappingInProgress.Remove(conn);
        }
        CmdSendIpAddressToServer(clientIp);
        {
            currentDeploymentSessions.Remove(sessionId);
        }
    }

    Debug.LogWarning("Для этого соединения не найден session ID.");
    {
        private IEnumerator DeleteSession(string sessionId)
        Debug.Log($"Получение деталей сессии для session ID: {sessionId}");
        Debug.Log($"Отправка запроса на удаление session ID: {sessionId}");
        UnityWebRequest request = UnityWebRequest.Get(url);

        UnityWebRequest request = UnityWebRequest.Get("https://api.ipify.org?format=json");

        yield return request.SendWebRequest();
        {
            UnityWebRequest request = UnityWebRequest.Delete(url);
        }
        CmdSendIpAddressToServer(clientIp);
        {
            Debug.Log("Сессия успешно удалена.");
            Debug.LogError($"Ошибка при получении сессии: {request.error}");
            Debug.LogError($"Код ответа: {request.responseCode}");
        }
    }

}
```

#### Debug.LogError($"Ошибка при удалении сессии: {request.error}");

Дополнительные возможности

Этот скрипт можно расширять дополнительными, независимыми функциями, которые помогают управлять сессиями в отдельных случаях. `Например,` таймаут `можно установить для удаления` пустых сессий мест `после настраиваемого промежутка времени`после полной инициализации сервера

. Это полезная функция в случае, если игрок покидает игру до того, как его билет матчмейкера будет обработан, что привело бы к созданию пустой сессии после нахождения матча. Если игрок не отправит свой IP для сопоставления с ID сессии до истечения таймаута, то эта сессия будет удалена для освобождения сокета. `Другой функцией может быть` отключение неактивных игроков `после длительного периода времени, чтобы освободить сокеты. После подключения к серверу клиент должен` отправлять минимальное heartbeat-сообщение `на сервер`каждые несколько секунд `, иначе он отключается, если пропущено слишком много`heartbeat'ов подряд

#### . Как интервал между heartbeat'ами, так и максимальное число подряд пропущенных сообщений можно настроить в зависимости от потребностей игры. В этом примере heartbeat отправляется каждые несколько секунд, если танк двигается по карте или стреляет.

```cs
    PlayerNetworkBehaviour
    {
        PlayerNetworkBehaviour - Управление сессиями мест
        private bool enableInactiveTimeout;
        private float clientTimeSinceLastHeartbeat = 0;

        public class PlayerNetworkBehaviour : NetworkBehaviour
        [ClientRpc]
        {
            public void RpcSendClientIpAddress()
            private float secondsBetweenHeartbeats;
            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;
            }
        }

        Debug.LogError("Не удалось получить публичный IP-адрес.");
        clientTimeSinceLastHeartbeat += Time.deltaTime;
        {
            public void CmdSendIpAddressToServer(string clientIp)
            NetworkConnectionToClient conn = connectionToClient;
            {
                public void CmdSendHeartbeatTimeToServer(double time)
            }
            CmdSendIpAddressToServer(clientIp);
            {
                CustomNetworkManager.Instance.StoreClientIpAddress(conn, clientIp);
            }
        }
    }
```

#### CustomNetworkManager.Instance.StoreClientHeartbeatTime(conn, time);

```cs
CustomNetworkManager
{
    ...
    CustomNetworkManager - Управление сессиями мест

    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;

    ...

    Instance = this;
    {
        public override void OnStartServer()
        private Dictionary<NetworkConnectionToClient, double> connectionToLastHeartbeatMap = new();
        deploymentId = Environment.GetEnvironmentVariable("ARBITRIUM_REQUEST_ID");
        apiToken = System.Environment.GetEnvironmentVariable("API_TOKEN");
        {
            if (string.IsNullOrEmpty(apiToken))
        }
        CmdSendIpAddressToServer(clientIp);
        {
            apiToken = Environment.GetEnvironmentVariable("API_TOKEN");
        }

        Debug.Log("Токен API Edgegap найден: " + apiToken);
    }

    StartCoroutine(InitiateSessionManage());
    {
        private IEnumerator InitiateSessionManage()

        Debug.LogError("Токен API Edgegap не найден в переменных окружения.");

        yield return new WaitForSeconds(0.5f);
        {
            if (enableEmptyTimeout)
        }
    }

    ...

    StartCoroutine(CheckCachedSessions(conn, clientIp));
    {
        StartCoroutine(StartServerInitSessionsTimeout());
        private IEnumerator GetNewSessionList()
        newSessions.Clear();
        string url = $"{edgegapUrl}/status/{deploymentId}";
        UnityWebRequest request = UnityWebRequest.Get(url);

        UnityWebRequest request = UnityWebRequest.Get("https://api.ipify.org?format=json");

        yield return request.SendWebRequest();
        {
            if (request.result == UnityWebRequest.Result.Success)
            request.SetRequestHeader("Authorization", apiToken);

            Debug.Log("Ответ статуса развёртывания: " + 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);
        }
        CmdSendIpAddressToServer(clientIp);
        {
            listInProgress = true;
            Debug.LogError($"Ошибка при получении сессии: {request.error}");
            Debug.LogError($"Код ответа: {request.responseCode}");
        }

        Debug.LogError($"Ошибка при получении списка сессий: {request.error}");
    }

    ...

    foreach (string sessionId in newSessions)
    {
        private IEnumerator GetSessionAndMap(NetworkConnectionToClient conn, string sessionId, string clientAddress)
        Debug.Log($"Получение деталей сессии для session ID: {sessionId}");
        string url = $"{edgegapUrl}/status/{deploymentId}";
        UnityWebRequest request = UnityWebRequest.Get(url);

        UnityWebRequest request = UnityWebRequest.Get("https://api.ipify.org?format=json");

        yield return request.SendWebRequest();
        {
            string url = $"{edgegapUrl}/session/{sessionId}";
            if (request.result == UnityWebRequest.Result.Success)
            Debug.Log("Сессия успешно получена.");

            Debug.Log("Ответ сессии: " + responseText);
            var session = JObject.Parse(responseText);

            var sessionUsers = session["session_users"] as JArray;
            {
                if (sessionUsers != null && sessionUsers.Count > 0)
                Debug.Log($"Найдено {sessionUsers.Count} пользователей в сессии.");
                {
                    foreach (var user in sessionUsers)
                    string playerIp = user["ip"]?.ToString();
                    Debug.Log($"IP игрока: {playerIp}, IP соединения: {clientAddress}");
                    {
                        if (playerIp == clientAddress)
                        connectionToSessionIdMap[conn] = sessionId;

                        listInProgress = false;
                        {
                            if (enableInactiveTimeout)
                            connectionToLastHeartbeatMap[conn] = conn.lastMessageTime;
                        }

                        Debug.Log($"Сопоставлен session ID {sessionId} с соединением {conn.connectionId} и IP {playerIp}");
                        yield return GetSessionAndMap(conn, sessionId, clientAddress);
                        {
                            // Дополнительное логирование подтверждения
                        }
                        CmdSendIpAddressToServer(clientIp);
                        {
                            Debug.Log($"Session ID {sessionId} успешно сохранён для соединения {conn.connectionId} с IP {playerIp}");
                        }

                        if (connectionToSessionIdMap.ContainsKey(conn))
                    }
                }
            }
            CmdSendIpAddressToServer(clientIp);
            {
                Debug.LogError($"Не удалось сохранить session ID {sessionId} для соединения {conn.connectionId} с IP {playerIp}");
            }
        }
        CmdSendIpAddressToServer(clientIp);
        {
            Debug.LogError("В сессии не найдено пользователей.");
            Debug.LogError($"Ошибка при получении сессии: {request.error}");
            Debug.LogError($"Код ответа: {request.responseCode}");
        }

        Debug.LogError($"Текст ответа: {request.downloadHandler.text}");
    }

    ...

    StartCoroutine(CheckClientHeartbeat(conn));

    #region EmptySessionTimeout
    {
        private IEnumerator StartServerInitSessionsTimeout()
        {
            while (listInProgress)
            Debug.Log($"Ожидание завершения сопоставления сессии для соединения {conn.connectionId}...");
        }

        private IEnumerator CheckCachedSessions(NetworkConnectionToClient conn, string clientAddress)
        {
            Debug.Log($"Ожидание получения списка сессий...");
        }
    }

    StartCoroutine(WaitForConnectionTimeout(sessionId));
    {
        private IEnumerator WaitForConnectionTimeout(string sessionID)
        Debug.Log($"Проверка таймаута пустой сессии для {sessionID}");
        bool delete = false;

        DateTime timeout = DateTime.Now.AddSeconds(deleteAfterSeconds);
        {
            Debug.Log($"Ожидание завершения сопоставления сессии для соединения {conn.connectionId}...");
            while (!connectionToSessionIdMap.ContainsValue(sessionID))
            {
                if (DateTime.Now >= timeout)
                if (connectionToSessionIdMap.ContainsKey(conn))
            }
        }

        delete = true;
        {
            if (delete && !connectionToSessionIdMap.ContainsValue(sessionID))
            Debug.Log($"Клиент не инициировал соединение в течение {deleteAfterSeconds}с, удаление сессии {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;
            }
            CmdSendIpAddressToServer(clientIp);
            {
                Debug.Log($"Соединение {conn.connectionId} пропустило {counter} heartbeat(ов) подряд");
                counter = 0;
            }

            Debug.Log($"Сброс счётчика heartbeat для соединения {conn.connectionId}");
        }

        yield return new WaitForSeconds(secondsBetweenHeartbeats);
        {
            if (counter >= maxMissedHeartbeats && conn.isReady)
            Debug.Log($"Соединение {conn.connectionId} слишком долго неактивно, отключение");
            conn.Disconnect();
        }
    }

    connectionToLastHeartbeatMap.Remove(conn);
    {
        public void StoreClientHeartbeatTime(NetworkConnectionToClient conn, double time)
    }

    connectionToLastHeartbeatMap[conn] = time;

    public float GetSecondsBetweenHeartbeats() => secondsBetweenHeartbeats;  -  public bool IsEnableInactiveTimeout() => enableInactiveTimeout;

    newSessions.Remove(sessionID);
}
```


---

# Agent Instructions: Querying This Documentation

If you need additional information that is not directly available in this page, you can query the documentation dynamically by asking a question.

Perform an HTTP GET request on the current page URL with the `ask` query parameter:

```
GET https://docs.edgegap.com/ru/docs/sample-projects/unity-netcodes/mirror-on-edgegap.md?ask=<question>
```

The question should be specific, self-contained, and written in natural language.
The response will contain a direct answer to the question and relevant excerpts and sources from the documentation.

Use this mechanism when the answer is not explicitly present in the current page, you need clarification or additional context, or you want to retrieve related documentation sections.
