# SeasonalBuildService

## Обзор класса

Кроме сборок, которые пользователь собирает сам, лаунчер умеет ставить готовые сезонные сборки с
собственного файлового сервера: набор модов под конкретную версию игры и модлоадер, подготовленный
заранее и выдаваемый целиком.

`SeasonalBuildService` — каталог этих сборок: скачивает `index.json` с файлового сервера, кэширует
в папке лаунчера и отдаёт из кэша, пока тот не устарел. Устройство повторяет
[VersionManifestService](VersionManifestService.md) — включая то, что пустой разбор считается
испорченным ответом и хороший кэш им не затирается.

## Место в проекте и зависимости

Экземпляр создаётся и принадлежит [LauncherBackend](LauncherBackend.md). Записи каталога
используются [SeasonalPackDownloader](SeasonalPackDownloader.md) — оттуда берётся адрес архива и
его контрольная сумма.

Путь к файлу кэша даёт `LauncherPaths::seasonalCatalogFile()` из
[launcherpaths.h](launcherpaths.md).

Требования сборки: `Qt6::Core` (`QDate`, `QDateTime`, `QHash`, `QUrl`) и `Qt6::Network`
(`QNetworkAccessManager`).

## Иерархия и роль

Наследует `QObject`: мета-объектная система, два сигнала и владение по родителю. Виртуальных
методов базового класса не переопределяет.

## Публичные структуры

### SeasonalBuildEntry

Одна готовая сборка с сервера сезонных сборок.

| Поле | Тип | По умолчанию | Описание |
|------|-----|--------------|----------|
| `id` | `QString` | — | Идентификатор вида `season-5`; он же ключ, по которому сборка узнаётся среди локальных записей |
| `name` | `QString` | — | Название для интерфейса, например «Сезон 5: Пустоши» |
| `revision` | `int` | `0` | Растёт при каждой публикации; сравнение с установленной ревизией даёт признак доступного обновления |
| `minecraftVersion` | `QString` | — | Версия игры, например `1.20.1` |
| `loader` | `QString` | — | Ключ модлоадера: пустая строка (чистая ваниль), `forge`, `fabric`, `neoforge` или `quilt` |
| `loaderVersion` | `QString` | — | Версия модлоадера |
| `modCount` | `int` | `0` | Число модов в сборке; показывается колонкой в таблице |
| `seasonStart` | `QDate` | — | Начало сезона |
| `seasonEnd` | `QDate` | — | Конец сезона; невалидная дата означает, что сезон ещё не закончен |
| `serverUrl` | `QString` | — | Адрес игрового сервера — не файлового, с которого качается сборка |
| `javaMajor` | `int` | `0` | Требуемая версия Java; `0` означает «определять по версии игры» |
| `description` | `QString` | — | Описание сборки; показывается в подвале окна каталога |
| `archiveUrl` | `QUrl` | — | Адрес архива сборки |
| `archiveSize` | `qint64` | `0` | Размер архива в байтах |
| `archiveSha256` | `QString` | — | Контрольная сумма архива |

Метод `isValid()` возвращает `true`, когда заполнен `id`, `revision` больше нуля и `archiveUrl`
корректен.

## Псевдонимы типов

`SeasonalBuildService::Callback` — `std::function<void(bool ok, const QString &warning)>`.
Сочетание `ok == true` с непустым `warning` означает данные из устаревшего кэша.

## Публичные методы

#### explicit SeasonalBuildService(QObject \*parent = nullptr)

Создаёт сервис и его `QNetworkAccessManager`. Конструктор помечен `explicit`.

#### QList&lt;SeasonalBuildEntry&gt; builds() const

Текущий каталог сборок. Возвращает копию.

#### bool hasData() const

Есть ли в каталоге хоть что-то — из сети или из кэша.

#### bool isRefreshing() const

Идёт ли сейчас сетевое обновление.

#### QString lastError() const

Последняя ошибка обращения к серверу; пустая строка означает, что всё в порядке.

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

#### std::optional&lt;SeasonalBuildEntry&gt; find(const QString &id) const

Запись по идентификатору сборки; `std::nullopt`, если такой нет.

#### void setBaseUrl(const QUrl &baseUrl)

Задаёт адрес сервера сборок. Адрес меняется из настроек, поэтому при смене хоста накопленные
данные и кэш сбрасываются: ссылки в них указывают на старый сервер и после смены недействительны.

#### QUrl baseUrl() const

Текущий адрес сервера сборок.

#### void ensureLoaded(Callback callback, bool forceRefresh = false)

Единственная точка входа. Свежий кэш означает отложенный вызов колбэка без сети; иначе запускается
один сетевой запрос на всех, кто успел попросить. Параметр `forceRefresh` обходит проверку
свежести — так работает кнопка обновления списка.

Окно каталога вызывает этот метод при каждом открытии без принудительного обновления: свежий кэш
отвечает без сети, поэтому вызов ничего не стоит.

## Сигналы

#### buildsChanged()

Каталог изменился — пришли новые данные или прочитан кэш. Обработчик перечитывает `builds()` и
обновляет таблицу.

#### refreshingChanged()

Изменился признак обновления. Обработчик показывает или убирает индикатор загрузки; в окне
каталога по нему же выключается кнопка обновления списка.

## Владение и время жизни

Класс наследует `QObject` и принимает `parent` — родитель его и удалит.
`QNetworkAccessManager` создаётся в конструкторе с сервисом в роли родителя.

Отложенные колбэки хранятся до завершения текущего запроса; уничтожение сервиса с незавершённым
запросом обрывает его, и колбэки не вызываются.

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

Только поток GUI. Чтение и запись кэша выполняются синхронно в вызывающем потоке.

## Взаимодействие с другими классами

`LauncherBackend` вызывает `ensureLoaded()` при открытии окна каталога и по кнопке обновления, а
сигналы переправляет в свойства для QML. Перед отдачей в интерфейс он сводит записи каталога с
локальными: строка таблицы уже содержит готовый статус и признак доступного обновления, поэтому
[SeasonalBuildsDialog](../qml/SeasonalBuildsDialog.md) ничего не считает сам.

Установка сезонной сборки начинается с `find()`: по записи бэкенд получает адрес архива и передаёт
его [SeasonalPackDownloader](SeasonalPackDownloader.md), а скачанный пак раскатывает
[BuildSwitcher](BuildSwitcher.md).

## Внешнее взаимодействие

**Сеть, исходящие запросы.** Класс скачивает `index.json` с файлового сервера сборок, адрес
которого задаётся через `setBaseUrl()`. Формат — JSON поверх HTTPS, запрос инициирует лаунчер.

Испорченный или пустой ответ не затирает хороший кэш. При недоступной сети данные отдаются из
устаревшего кэша с `ok == true` и заполненным `warning`; текст ошибки при этом попадает и в
`lastError()`.

Все сигналы и колбэки приходят в поток GUI.

## Пример использования

```cpp
auto *seasonal = new SeasonalBuildService(this);
seasonal->setBaseUrl(QUrl(settings.seasonalServer));
connect(seasonal, &SeasonalBuildService::buildsChanged, this, &Backend::rebuildSeasonalCatalog);

seasonal->ensureLoaded([this, seasonal](bool ok, const QString &warning) {
    if (!ok) {
        emit seasonalCatalogError(seasonal->lastError());
        return;
    }
    if (!warning.isEmpty())
        showStatus(warning);
});
```

---

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