# 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`. Сочетание `ok == true` с непустым `warning` означает данные из устаревшего кэша. ## Публичные методы #### explicit SeasonalBuildService(QObject \*parent = nullptr) Создаёт сервис и его `QNetworkAccessManager`. Конструктор помечен `explicit`. #### QList<SeasonalBuildEntry> builds() const Текущий каталог сборок. Возвращает копию. #### bool hasData() const Есть ли в каталоге хоть что-то — из сети или из кэша. #### bool isRefreshing() const Идёт ли сейчас сетевое обновление. #### QString lastError() const Последняя ошибка обращения к серверу; пустая строка означает, что всё в порядке. В отличие от остальных каталогов лаунчера, ошибка здесь хранится отдельным полем: пустой список и ошибка выглядят одинаково пустыми, и окно каталога показывает причину прямо на месте строк. #### std::optional<SeasonalBuildEntry> 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); }); ``` --- При создании этого документа использовался ИИ.