# ModLoaderVersionService ## Обзор класса Каждый модлоадер публикует свой список версий, и у каждого он устроен по-своему. `ModLoaderVersionService` приводит все четыре к одному виду: качает списки, кэширует в папке лаунчера и отдаёт из кэша, пока тот не устарел. Устроен так же, как [VersionManifestService](VersionManifestService.md). Главная особенность класса — в том, чего в нём нет: отдельной проверки совместимости с версией игры. Совместимость заложена в структуру данных. Fabric и Quilt отдают список сразу под нужную версию игры, а `maven-metadata` Forge и NeoForge раскладывается по версиям игры при разборе. Версии игры, под которую сборок нет, соответствует пустой список — выбрать несовместимый лоадер физически нечем. ## Место в проекте и зависимости Подключает [modloader.h](modloader.md): перечисление `ModLoader` и структура `LoaderVersionEntry` приходят оттуда. Экземпляр создаётся и принадлежит [LauncherBackend](LauncherBackend.md). Ссылку на него получает [ModLoaderInstaller](ModLoaderInstaller.md) — из записи списка он берёт адрес `installer.jar`. Путь к файлу кэша даёт `LauncherPaths::loaderCacheFile()` из [launcherpaths.h](launcherpaths.md). Требования сборки: `Qt6::Core` (`QDateTime`, `QHash`, `QSet`) и `Qt6::Network` (`QNetworkAccessManager`). ## Иерархия и роль Наследует `QObject`: мета-объектная система, два сигнала и владение по родителю. Виртуальных методов базового класса не переопределяет. ## Псевдонимы типов `ModLoaderVersionService::Callback` — `std::function`. Как и у сервиса манифеста, `ok == true` с непустым `warning` означает, что данные отдали из устаревшего кэша. ## Публичные методы #### explicit ModLoaderVersionService(QObject \*parent = nullptr) Создаёт сервис и его `QNetworkAccessManager`. Кэши читаются лениво, при первом обращении к конкретному лоадеру. Конструктор помечен `explicit`. #### void ensureLoaded(ModLoader loader, const QString &gameVersion, Callback callback, bool forceRefresh = false) Единственная точка входа. Свежий кэш означает отложенный вызов колбэка без сети; иначе запускается один сетевой запрос на всех, кто успел попросить. Свежесть считается по-разному в зависимости от лоадера: Fabric и Quilt спрашиваются по каждой версии игры отдельно, поэтому отметка времени у них своя на каждую версию; Forge и NeoForge приходят одним `maven-metadata` на все версии сразу, и отметка у них одна на весь лоадер. Параметр `forceRefresh` обходит проверку свежести. #### QList<LoaderVersionEntry> versions(ModLoader loader, const QString &gameVersion) const Список сборок лоадера под конкретную версию игры. Новые сборки идут первыми, поэтому первая строка — самая свежая; именно её интерфейс подставляет по умолчанию. Пустой список означает, что лоадер эту версию игры не поддерживает. #### bool isRefreshing(ModLoader loader, const QString &gameVersion) const Идёт ли сейчас запрос по этой паре. Интерфейс по этому признаку отличает «ещё грузим» от «не поддерживается» — оба случая выглядят пустым списком. #### std::optional<LoaderVersionEntry> find(ModLoader loader, const QString &gameVersion, const QString &loaderVersion) const Запись по версии лоадера; `std::nullopt`, если такой нет. Из неё установщик берёт ссылку на `installer.jar`. ## Сигналы #### versionsChanged(const QString &loaderKey, const QString &gameVersion) Список версий изменился. Параметры сужают событие до конкретной пары: `loaderKey` принимает значения `forge`, `fabric`, `neoforge`, `quilt`. Обработчик должен сверить оба параметра со своим текущим состоянием и перечитать `versions()`, только если они совпадают, — иначе обновление относится к другой строке лоадера. Именно так поступает [LoaderRow](../qml/LoaderRow.md). #### refreshingChanged() Изменился признак сетевого обновления у какой-либо пары. Обработчик перечитывает `isRefreshing()` для интересующей его пары. ## Владение и время жизни Класс наследует `QObject` и принимает `parent` — родитель его и удалит. `QNetworkAccessManager` создаётся в конструкторе с сервисом в роли родителя. Отложенные колбэки хранятся по ключу запроса до завершения соответствующего обращения к сети. Уничтожение сервиса с незавершённым запросом обрывает его, и накопленные колбэки не вызываются. Ссылку на сервис держит установщик модлоадеров; уничтожать сервис раньше установщика нельзя. ## Потокобезопасность Только поток GUI. Чтение и запись кэша выполняются синхронно в вызывающем потоке. ## Взаимодействие с другими классами `LauncherBackend` оборачивает сервис тремя методами, доступными из QML: получить список, запросить обновление и узнать, идёт ли загрузка. Сигнал `versionsChanged` он переправляет в QML под тем же именем, поэтому строка лоадера в карточке сборки подписывается прямо на него. [ModLoaderInstaller](ModLoaderInstaller.md) обращается к `find()` за адресом установщика перед началом установки. ## Внешнее взаимодействие **Сеть, исходящие запросы.** Класс обращается к четырём разным источникам метаданных, и форматы ответов различаются: у Fabric и Quilt это JSON, у Forge и NeoForge — XML `maven-metadata`. Разбор разделён на две функции соответственно. Испорченный ответ разбирается в пустой результат, и хороший кэш им не затирается — это сознательное решение: лучше показать вчерашний список, чем стереть его из-за сбоя на сервере. При недоступной сети данные отдаются из устаревшего кэша с `ok == true` и заполненным `warning`. Все сигналы и колбэки приходят в поток GUI. ## Пример использования ```cpp auto *loaders = new ModLoaderVersionService(this); connect(loaders, &ModLoaderVersionService::versionsChanged, this, [this](const QString &key, const QString &game) { if (key == loaderKey(ModLoader::Fabric) && game == m_gameVersion) emit fabricVersionsChanged(); }); loaders->ensureLoaded(ModLoader::Fabric, QStringLiteral("1.21.1"), [this, loaders](bool ok, const QString &warning) { if (!ok) { showStatus(warning); return; } const auto list = loaders->versions(ModLoader::Fabric, QStringLiteral("1.21.1")); if (!list.isEmpty()) selectVersion(list.first().loaderVersion); }); ``` --- При создании этого документа использовался ИИ.