# VersionInstaller ## Обзор класса Установить версию Minecraft — значит положить в `.minecraft` пять групп файлов: описание версии, клиентский jar, библиотеки, индекс ресурсов и сами ресурсы. Последних — десятки тысяч мелких файлов. `VersionInstaller` делает это фоном, не блокируя интерфейс: складывает всё нужное в очередь загрузок, качает несколько файлов параллельно, пишет их потоком на диск и по ходу сообщает прогресс. Одна версия ставится за раз, остальные ждут в очереди. Класс также разворачивает цепочку наследования: если у версии есть `inheritsFrom`, родительская версия ставится перед ней. ## Место в проекте и зависимости Подключает [minecraftversion.h](minecraftversion.md) — по разобранной версии он и понимает, что качать. В конструктор принимает [VersionManifestService](VersionManifestService.md): оттуда берётся адрес описания версии. Экземпляр создаётся и принадлежит [LauncherBackend](LauncherBackend.md). На него же опирается [ModLoaderInstaller](ModLoaderInstaller.md) — профиль модлоадера ставится поверх установленной версии игры. Требования сборки: `Qt6::Core` (`QCryptographicHash`, `QSaveFile`, `QQueue`, `QTimer`) и `Qt6::Network` (`QNetworkAccessManager`, `QNetworkReply`). ## Иерархия и роль Наследует `QObject`: мета-объектная система, пять сигналов и владение по родителю. Объявлен виртуальный деструктор — класс владеет незавершёнными загрузками и обязан их закрыть. ## Публичные структуры ### DownloadTask Один файл, который нужно положить в `.minecraft`. | Поле | Тип | По умолчанию | Описание | |------|-----|--------------|----------| | `kind` | `DownloadTask::Kind` | `ClientJar` | Что это за файл | | `url` | `QUrl` | — | Откуда качать | | `path` | `QString` | — | Абсолютный путь назначения | | `sha1` | `QString` | — | Контрольная сумма; пустая означает «не проверять» | | `size` | `qint64` | `0` | Ожидаемый размер; `0` — неизвестен | | `label` | `QString` | — | Что показать в панели прогресса | | `attempts` | `int` | `0` | Сколько попыток уже сделано | #### Перечисление DownloadTask::Kind | Значение | Описание | |----------|----------| | `ClientJar` | Клиентский jar версии | | `Library` | Библиотека из `libraries/` | | `AssetIndex` | Индекс ресурсов | | `AssetObject` | Один файл ресурсов | | `LoggingConfig` | Конфигурация журналирования log4j | ### ActiveDownload Файл в процессе скачивания: задача, сетевой ответ, открытый `QSaveFile`, накапливаемая контрольная сумма и число принятых байт. Файлы пишутся потоком — держать десятки мегабайт в памяти незачем, а нескольких параллельных загрузок хватило бы на сотни. ## Публичные методы #### explicit VersionInstaller(VersionManifestService \*manifest, QObject \*parent = nullptr) Создаёт установщик поверх сервиса манифеста. Сервис не переходит во владение установщика и обязан пережить его. Конструктор помечен `explicit`. #### bool isRunning() const Идёт ли установка прямо сейчас. #### QString versionId() const Идентификатор версии, которая ставится в данный момент. #### QString stage() const Текущий этап установки словами — это же значение показывается в заголовке панели прогресса. #### QString currentFile() const Подпись файла, который качается сейчас. #### qint64 bytesDone() const Сколько байт уже получено, с учётом идущих загрузок. #### qint64 bytesTotal() const Ожидаемый общий объём. Растёт по ходу установки: полный размер ресурсов становится известен только после разбора их индекса. #### double fraction() const Доля выполнения от `0` до `1` либо `-1`, пока итоговый объём неизвестен. Значение `-1` панель прогресса показывает многоточием вместо процентов. #### void install(const QString &gameDir, const QString &versionId) Ставит версию в указанную папку игры. Если установка уже идёт, версия становится в очередь. Порядок работы: разрешение записи манифеста, загрузка описания версии, подготовка списка задач, скачивание, разворачивание индекса ресурсов и — для версий до 1.6 — раскладка ресурсов в плоскую папку, которую те версии умеют читать. #### bool isQueued(const QString &versionId) const Стоит ли версия в очереди на установку. Позволяет не ставить одну и ту же версию дважды. #### void cancel() Отменяет текущую установку и очищает очередь. Незавершённые файлы не остаются на диске: они пишутся через `QSaveFile` и фиксируются только целиком. ## Сигналы #### started(const QString &versionId) Установка версии началась. Обработчик показывает панель прогресса и выставляет признак занятости. #### progressChanged() Изменились числа прогресса. Испускается не чаще десяти раз в секунду: при тысячах мелких файлов сигнал на каждый принятый блок обошёлся бы дороже самой загрузки. Обработчик перечитывает `stage()`, `currentFile()`, `bytesDone()`, `bytesTotal()` и `fraction()`. #### finished(const QString &versionId) Версия установлена успешно. Обработчик убирает панель прогресса, обновляет список установленных версий и пересчитывает комплектность сборок. #### failed(const QString &versionId, const QString &message) Установка не удалась; в параметре — текст ошибки для пользователя. Часть файлов при этом может остаться на диске: повторная установка докачает недостающее. #### canceled(const QString &versionId) Установка отменена пользователем. В отличие от `failed`, ошибку показывать не нужно. ## Владение и время жизни Класс наследует `QObject` и принимает `parent` — родитель его и удалит. Указатель на `VersionManifestService`, переданный в конструктор, **не принадлежит** установщику: сервис создаётся раньше и живёт дольше. `QNetworkAccessManager` и оба таймера создаются в конструкторе с установщиком в роли родителя. Активные загрузки хранятся как `std::shared_ptr`, а файл внутри каждой — как `std::unique_ptr`: незавершённая запись отменяется вместе с уничтожением объекта, и испорченный файл не попадает на место назначения. ## Потокобезопасность Только поток GUI, как и остальной сетевой код лаунчера. Параллелизм здесь — не потоки, а несколько одновременных сетевых запросов в одном цикле событий. Раскладка ресурсов для старых версий выполняется порциями по таймеру, чтобы не занимать поток надолго. ## Взаимодействие с другими классами `LauncherBackend` вызывает `install()` при установке сборки и переправляет все пять сигналов в свойства, которые читает QML: панель прогресса главного окна показывает `stage()`, `fraction()` и байты, а `finished` обновляет список установленных версий. [ModLoaderInstaller](ModLoaderInstaller.md) держит ссылку на установщик: профиль модлоадера требует, чтобы базовая версия игры была уже на месте. Разбор описания версии идёт через `VersionLoader::load()` из [minecraftversion.h](minecraftversion.md). ## Внешнее взаимодействие **Сеть, исходящие запросы.** Класс качает файлы с серверов Mojang через `QNetworkAccessManager`. Направление одностороннее, протокол — HTTPS; описание версии и индекс ресурсов приходят как JSON, остальное — двоичными файлами. Каждый файл пишется потоком через `QSaveFile` с одновременным подсчётом sha1; несовпадение контрольной суммы считается неудачей загрузки. Неудачная задача повторяется — счётчик попыток хранится в самой задаче, — и только исчерпав попытки, приводит к сигналу `failed`. Существующие файлы сверяются только по размеру: перехеширование сотен мегабайт при каждом добавлении версии дороже, чем риск битого файла. Все сигналы приходят в поток GUI. ## Пример использования ```cpp auto *installer = new VersionInstaller(manifestService, this); connect(installer, &VersionInstaller::progressChanged, this, [this, installer] { emit downloadProgress(installer->fraction(), installer->currentFile()); }); connect(installer, &VersionInstaller::finished, this, &Backend::onVersionInstalled); connect(installer, &VersionInstaller::failed, this, [this](const QString &id, const QString &message) { emit launchError(tr("Не удалось установить %1: %2").arg(id, message)); }); if (!installer->isQueued(versionId)) installer->install(gameDir, versionId); ``` --- При создании этого документа использовался ИИ.