# 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<ActiveDownload>`, а файл внутри каждой — как
`std::unique_ptr<QSaveFile>`: незавершённая запись отменяется вместе с уничтожением объекта, и
испорченный файл не попадает на место назначения.

## Потокобезопасность

Только поток 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);
```

---

При создании этого документа использовался ИИ.
