1 parent
1943996608
commit
f10b654953
34 files changed
+2099
-4576
No files matched your search
+70
-205
@@ -1,210 +1,75 @@
|
||||
# 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);
|
||||
```
|
||||
`versioninstaller.h`, `versioninstaller.cpp`
|
||||
|
||||
Ставит версию игры в `.minecraft`: описание версии, клиентский jar, библиотеки, индекс ресурсов,
|
||||
сами ресурсы и конфиг логирования. Одновременно ставится одна версия, остальные ждут в очереди.
|
||||
Всё работает в GUI-потоке на асинхронной сети.
|
||||
|
||||
Экземпляр держит [LauncherBackend](LauncherBackend.md). Ещё им пользуется
|
||||
[ModLoaderInstaller](ModLoaderInstaller.md): перед установкой модлоадера нужно поставить базовую
|
||||
версию, а после установки докачать библиотеки нового профиля.
|
||||
|
||||
## Интерфейс
|
||||
|
||||
- `install(gameDir, versionId)` — поставить в очередь. Если эта версия уже ставится или ждёт,
|
||||
ничего не происходит. Если id содержит слэши, `.` или `..`, сразу приходит `failed`.
|
||||
- `cancel()` — отменить текущую установку и очистить очередь.
|
||||
- `isRunning()`, `isQueued(id)`, `versionId()`.
|
||||
- Для панели прогресса: `stage()`, `currentFile()`, `bytesDone()`, `bytesTotal()`, `fraction()`
|
||||
(от 0 до 1; −1, если общий объём неизвестен).
|
||||
|
||||
Сигналы: `started(id)`, `progressChanged()` (не чаще раза в 100 мс), `finished(id)`,
|
||||
`failed(id, message)`, `canceled(id)`.
|
||||
|
||||
`started` испускается один раз на всю серию установок из очереди. `finished` испускается для
|
||||
каждой версии.
|
||||
|
||||
## Как идёт установка
|
||||
|
||||
1. **Описание версии.** У [VersionManifestService](VersionManifestService.md) берётся ссылка на
|
||||
`<id>.json`, файл скачивается, sha1 сверяется с манифестом, и файл сохраняется в
|
||||
`versions/<id>/`. Если версии в манифесте нет (профиль модлоадера) или сеть недоступна, берётся
|
||||
файл, который уже лежит на диске. Если нет и его, установка падает.
|
||||
2. **Родитель.** Если в описании есть `inheritsFrom`, а родителя на диске нет, текущая версия
|
||||
уступает место: в начало очереди встают сначала родитель, потом она сама. Глубина цепочки — до
|
||||
10 звеньев.
|
||||
3. **Список файлов.** `VersionLoader::load()` сводит цепочку, и из результата составляется список:
|
||||
клиентский jar, все библиотеки, индекс ресурсов и конфиг логов. Ссылки не по `https` и
|
||||
библиотеки без адреса пропускаются с предупреждением.
|
||||
4. **Загрузка.** До 8 файлов одновременно. Каждый пишется потоком в `QSaveFile`, sha1 считается на
|
||||
ходу. При временной ошибке (таймаут, обрыв, HTTP 429 или 5xx) и при несовпадении хеша файл
|
||||
ставится обратно в начало очереди с паузой 400 мс, потом 800 мс. После третьей неудачи
|
||||
установка падает. Другие ошибки сразу её прерывают. Таймаут на файл 30 секунд.
|
||||
5. **Ресурсы.** Как только индекс ресурсов на диске, он разворачивается в задачи на загрузку из
|
||||
`resources.download.minecraft.net/<xx>/<hash>`. До этого момента в общем объёме учитывается
|
||||
`totalSize` из описания версии, а потом он заменяется точной суммой.
|
||||
6. **Старые ресурсы.** У версий до 1.6 (`virtual`, `map_to_resources`, `assets` = `legacy` или
|
||||
`pre-1.6`) игра ищет ресурсы не в `assets/objects`, а в обычной папке с именами файлов. Они
|
||||
копируются в `assets/virtual/legacy/` или в `resources/` по 64 файла за такт таймера, чтобы
|
||||
окно не подвисало.
|
||||
7. **Проверка.** `GameLauncher::missingFiles()`. Если чего-то нет, это ошибка.
|
||||
|
||||
## Уже скачанные файлы
|
||||
|
||||
Существующий файл считается целым, если его размер совпадает с ожидаемым (или размер неизвестен).
|
||||
Хеш не пересчитывается: перехеширование сотен мегабайт ресурсов при каждой установке обходится
|
||||
дороже, чем редкий битый файл. Для будущей кнопки «Проверить файлы» есть флаг `m_verifyExisting`,
|
||||
сейчас он всегда `false`.
|
||||
|
||||
Пропуск файлов идёт пачками по 256 за такт. Иначе установка версии, у которой уже есть все
|
||||
ресурсы, повесила бы окно на перебор десятков тысяч записей.
|
||||
|
||||
## Ошибки и отмена
|
||||
|
||||
При ошибке все активные загрузки обрываются, недописанные файлы откатываются (`QSaveFile`), а
|
||||
**очередь очищается**: остальные версии из неё тоже не поставятся. К тексту ошибки добавляются до
|
||||
трёх накопленных предупреждений.
|
||||
|
||||
Отмена обрывает активные запросы и ждёт, пока все они вернутся. Только после этого приходит
|
||||
`canceled`. Пока отмена не закончилась, `install()` отвечает отказом.
|
||||
|
||||
Деструктор блокирует сигналы и отменяет установку.
|
||||
|
||||
---
|
||||
|
||||
|
||||
Reference in new issue
Block a user