1 parent
1943996608
commit
f10b654953
34 files changed
+2099
-4576
No files matched your search
+78
-159
@@ -1,175 +1,94 @@
|
||||
# SeasonalBuildService
|
||||
|
||||
## Обзор класса
|
||||
`seasonalbuildservice.h`, `seasonalbuildservice.cpp`
|
||||
|
||||
Кроме сборок, которые пользователь собирает сам, лаунчер умеет ставить готовые сезонные сборки с
|
||||
собственного файлового сервера: набор модов под конкретную версию игры и модлоадер, подготовленный
|
||||
заранее и выдаваемый целиком.
|
||||
Каталог сезонных сборок, то есть готовых паков, которые команда публикует на своём файловом
|
||||
сервере. Сервис читает `<baseUrl>/index.json`, хранит копию в
|
||||
`<root>/cache/seasonal_builds.json` и отвечает из неё 15 минут.
|
||||
|
||||
`SeasonalBuildService` — каталог этих сборок: скачивает `index.json` с файлового сервера, кэширует
|
||||
в папке лаунчера и отдаёт из кэша, пока тот не устарел. Устройство повторяет
|
||||
[VersionManifestService](VersionManifestService.md) — включая то, что пустой разбор считается
|
||||
испорченным ответом и хороший кэш им не затирается.
|
||||
Экземпляр держит [LauncherBackend](LauncherBackend.md). Он же сводит строки каталога с
|
||||
установленными сборками и ставит выбранную.
|
||||
|
||||
## Место в проекте и зависимости
|
||||
## Адрес сервера
|
||||
|
||||
Экземпляр создаётся и принадлежит [LauncherBackend](LauncherBackend.md). Записи каталога
|
||||
используются [SeasonalPackDownloader](SeasonalPackDownloader.md) — оттуда берётся адрес архива и
|
||||
его контрольная сумма.
|
||||
По умолчанию `https://launcher-builds.galeonworx.com`. Переопределяется ключом `seasonalBaseUrl` в
|
||||
`settings.json`, например чтобы проверить установку на локальном HTTP-сервере. Бэкенд передаёт
|
||||
адрес через `setBaseUrl()` при старте и перед каждым `refreshSeasonalCatalog()`.
|
||||
|
||||
Путь к файлу кэша даёт `LauncherPaths::seasonalCatalogFile()` из
|
||||
[launcherpaths.h](launcherpaths.md).
|
||||
Если адрес сменился, накопленные данные сбрасываются, а кэш с другим `baseUrl` не читается: ссылки
|
||||
в нём ведут на старый сервер. Пустой адрес или адрес без хоста — сразу ошибка.
|
||||
|
||||
Требования сборки: `Qt6::Core` (`QDate`, `QDateTime`, `QHash`, `QUrl`) и `Qt6::Network`
|
||||
(`QNetworkAccessManager`).
|
||||
## Формат index.json
|
||||
|
||||
## Иерархия и роль
|
||||
Это контракт с теми, кто публикует сборки.
|
||||
|
||||
Наследует `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<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;
|
||||
```json
|
||||
{
|
||||
"formatVersion": 1,
|
||||
"builds": [
|
||||
{
|
||||
"id": "season-5",
|
||||
"name": "Сезон 5: Пустоши",
|
||||
"revision": 3,
|
||||
"minecraftVersion": "1.20.1",
|
||||
"loader": "forge",
|
||||
"loaderVersion": "47.4.0",
|
||||
"modCount": 142,
|
||||
"seasonStart": "2026-09-01",
|
||||
"seasonEnd": "",
|
||||
"serverUrl": "play.example.com:25565",
|
||||
"javaMajor": 17,
|
||||
"description": "…",
|
||||
"archive": {
|
||||
"url": "https://launcher-builds.galeonworx.com/season-5/r3.zip",
|
||||
"size": 734003200,
|
||||
"sha256": "…64 hex…"
|
||||
}
|
||||
}
|
||||
if (!warning.isEmpty())
|
||||
showStatus(warning);
|
||||
});
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
| Поле | Обязательно | Правила |
|
||||
|------|-------------|---------|
|
||||
| `formatVersion` | да | только `1`. Другое значение — весь каталог отклоняется с понятным текстом (если версия больше, пользователю предложат обновить лаунчер) |
|
||||
| `id` | да | `[a-z0-9][a-z0-9._-]{0,63}`. По нему лаунчер узнаёт уже установленную сборку, менять его нельзя |
|
||||
| `name` | да | |
|
||||
| `revision` | да | > 0. Увеличивается при каждой публикации. Если у игрока ревизия меньше, ему покажут «доступно обновление» |
|
||||
| `minecraftVersion` | да | без `/`, `\`, `:`, не длиннее 128 символов |
|
||||
| `loader` | нет | пусто (ваниль) или `forge`, `fabric`, `neoforge`, `quilt` |
|
||||
| `loaderVersion` | если есть `loader` | |
|
||||
| `modCount` | нет | для таблицы |
|
||||
| `seasonStart`, `seasonEnd` | нет | ISO-даты. Пустой `seasonEnd` значит, что сезон идёт |
|
||||
| `serverUrl` | нет | адрес игрового сервера для автоподключения |
|
||||
| `javaMajor` | нет | 0 или нет поля — определить по версии игры |
|
||||
| `description` | нет | показывается под таблицей |
|
||||
| `archive.url` | да | `http` или `https`, **та же схема, хост и порт, что у `baseUrl`** |
|
||||
| `archive.size` | да | ≥ 0 |
|
||||
| `archive.sha256` | да | 64 hex-символа |
|
||||
|
||||
Записи, которые не проходят проверку, молча пропускаются. Если не осталось ни одной, это считается
|
||||
испорченным ответом, и хороший кэш не затирается.
|
||||
|
||||
Требование «тот же хост» не даёт подложить в каталог ссылку на чужой сервер. Пак распаковывается
|
||||
прямо в `.minecraft`, поэтому качать его откуда попало нельзя.
|
||||
|
||||
Пак — это zip, содержимое которого раскладывается в корень `.minecraft` (`mods/`, `config/` и т. п.).
|
||||
Файлы в `versions/`, `libraries/`, `assets/`, `runtime/` и `launcher_profiles.json` запрещены, пак
|
||||
с ними не установится. Подробнее — [BuildArchiveWorker](BuildArchiveWorker.md).
|
||||
|
||||
Порядок в каталоге: сначала идущие сезоны, потом по дате начала (новые сверху), потом по имени.
|
||||
|
||||
## Интерфейс
|
||||
|
||||
- `ensureLoaded(callback, forceRefresh)` — как у
|
||||
[VersionManifestService](VersionManifestService.md), только кэш живёт 15 минут.
|
||||
- `builds()`, `find(id)`, `hasData()`, `isRefreshing()`.
|
||||
- `lastError()` — текст последней ошибки сервера или пустая строка. Окно сезонных сборок
|
||||
показывает его на месте таблицы.
|
||||
- Сигналы `buildsChanged()` (в том числе после неудачного запроса, чтобы обновился `lastError`) и
|
||||
`refreshingChanged()`.
|
||||
|
||||
---
|
||||
|
||||
При создании этого документа использовался ИИ.
|
||||
Reference in new issue
Block a user