36 KiB
LauncherBackend
Обзор класса
LauncherBackend — единственный класс проекта, видимый из QML, и центр всего приложения. Интерфейс
лаунчера ничего не знает ни о сети, ни о файлах, ни о процессах: он читает свойства этого класса,
вызывает его методы и слушает его сигналы.
Сам бэкенд почти ничего не делает руками. Он владеет двенадцатью сервисами — авторизация, запуск игры, каталоги версий, модлоадеров, Java и сезонных сборок, три установщика, загрузчик паков и переключатель сборок — и отвечает за то, чтобы они работали в правильном порядке. Кроме того, он хранит состояние лаунчера: профили игрока, пользовательские сборки и настройки запуска, которые читает и пишет в файлы папки лаунчера.
Ещё одна его задача — приводить данные к виду, удобному QML. Каталоги отдаются в интерфейс уже сведёнными с локальным состоянием: строка версии знает, скачана ли она, строка сезонной сборки — установлена ли и не устарела ли. Окно показывает статус, не считая ничего само.
Место в проекте и зависимости
Единственный экземпляр создаётся декларативно в Main.qml; в main.cpp он не
упоминается.
Владеет двенадцатью сервисами, каждому из которых посвящена своя страница:
| Поле | Класс | Роль |
|---|---|---|
m_auth |
AuthService | вход через Ely.by и офлайн |
m_msa |
MsaAuthService | вход через Microsoft |
m_launcher |
GameLauncher | запуск JVM с игрой |
m_manifest |
VersionManifestService | каталог версий Mojang |
m_installer |
VersionInstaller | установка версии игры |
m_loaderMeta |
ModLoaderVersionService | списки версий модлоадеров |
m_loaderInstaller |
ModLoaderInstaller | установка модлоадера |
m_switcher |
BuildSwitcher | смена активной сборки |
m_javaMeta |
JavaRuntimeService | каталог сборок Java |
m_javaInstaller |
JavaInstaller | установка Java |
m_seasonalMeta |
SeasonalBuildService | каталог сезонных сборок |
m_packDownloader |
SeasonalPackDownloader | загрузка архива сезонной сборки |
Пути ко всем файлам состояния берутся из launcherpaths.h, описания версий — из minecraftversion.h, словарь модлоадеров — из modloader.h, поиск системной Java — из javalocator.md.
Требования сборки: Qt6::Core, Qt6::Gui, Qt6::Network, Qt6::CorePrivate (опосредованно) и
Qt6::Quick — класс зарегистрирован в QML-модуле Minecraft_launcher, объявленном в
CMakeLists.txt.
Иерархия и роль
Наследует QObject: мета-объектная система, свойства, сигналы и владение по родителю. Объявлен
виртуальный деструктор. Виртуальных методов базового класса не переопределяет.
Свойства Q_PROPERTY
Профили и сборки
| Свойство | Тип | READ | WRITE | NOTIFY | Описание |
|---|---|---|---|---|---|
profileNames |
QStringList |
profileNames |
— | profilesChanged |
Имена профилей игрока в порядке добавления. Только для чтения; модель выпадающего списка профилей |
customBuildNames |
QStringList |
customBuildNames |
— | customBuildsChanged |
Имена пользовательских сборок. Только для чтения; модель списка в окне сборок |
activeBuildIndex |
int |
activeBuildIndex |
setActiveBuildIndex |
activeBuildChanged |
Сборка, которую запускает кнопка игры. Хранится по идентификатору сборки, а не по индексу: удаление соседней записи не должно переназначать активную. Запись в свойство запускает смену сборки |
activeBuildName |
QString |
activeBuildName |
— | activeBuildChanged |
Имя активной сборки для подписи на кнопке. Только для чтения |
installedVersions |
QStringList |
installedVersions |
— | installedVersionsChanged |
Версии, реально установленные в папке игры. Только для чтения |
Занятость и смена сборки
| Свойство | Тип | READ | WRITE | NOTIFY | Описание |
|---|---|---|---|---|---|
gameRunning |
bool |
gameRunning |
— | gameRunningChanged |
Игра запущена. Только для чтения; выключает кнопку запуска |
switching |
bool |
switching |
— | switchChanged |
Идёт архивация или распаковка .minecraft при смене сборки. Отдельно от busy, потому что на это время блокируется ещё и список сборок. Только для чтения |
switchProgress |
double |
switchProgress |
— | switchChanged |
Доля выполнения смены сборки от 0 до 1; -1 — итог неизвестен. Только для чтения |
switchStage |
QString |
switchStage |
— | switchChanged |
Этап смены сборки словами. Только для чтения |
switchStatus |
QString |
switchStatus |
— | switchChanged |
Строка состояния смены сборки. Только для чтения |
busy |
bool |
busy |
— | busyChanged |
Лаунчер занят: пока идёт загрузка версии, кнопка запуска гаснет. Только для чтения |
microsoftAvailable |
bool |
microsoftAvailable |
— | — | Собран ли лаунчер с Qt WebEngine. Константное свойство: без WebEngine окно входа Microsoft показать нечем, и интерфейс не должен предлагать этот путь |
Каталог версий
| Свойство | Тип | READ | WRITE | NOTIFY | Описание |
|---|---|---|---|---|---|
versionCatalog |
QVariantList |
versionCatalog |
— | versionCatalogChanged |
Объединённый список для выбора версии: установленные, разделитель, затем весь каталог Mojang. Строка содержит поля id, label, category, installed и search. Только для чтения |
catalogLoading |
bool |
catalogLoading |
— | catalogLoadingChanged |
Идёт загрузка манифеста версий. Только для чтения |
Каталог Java
| Свойство | Тип | READ | WRITE | NOTIFY | Описание |
|---|---|---|---|---|---|
javaCatalog |
QVariantList |
javaCatalog |
— | javaCatalogChanged |
Сборки Java для окна выбора: скачиваемые из сети плюс те, что уже лежат в папке лаунчера. Строка содержит поля id, label, kind, major, installed, downloadable, lts, sizeMb, detail, coverage и search. Только для чтения |
javaCatalogLoading |
bool |
javaCatalogLoading |
— | javaCatalogLoadingChanged |
Идёт загрузка каталога Java. Только для чтения |
Сезонные сборки
| Свойство | Тип | READ | WRITE | NOTIFY | Описание |
|---|---|---|---|---|---|
seasonalCatalog |
QVariantList |
seasonalCatalog |
— | seasonalCatalogChanged |
Готовые сборки с сервера. Строки уже сведены с локальными записями: окно показывает статус, не считая ничего само. Только для чтения |
seasonalCatalogLoading |
bool |
seasonalCatalogLoading |
— | seasonalCatalogLoadingChanged |
Идёт загрузка каталога сезонных сборок. Только для чтения |
seasonalCatalogError |
QString |
seasonalCatalogError |
— | seasonalCatalogChanged |
Текст ошибки обращения к серверу сборок; пусто — всё в порядке. Только для чтения |
seasonalInstalling |
bool |
seasonalInstalling |
— | seasonalInstallingChanged |
Идёт установка сезонной сборки: окно не даёт начать вторую. Только для чтения |
Загрузка
Одного сигнала на все свойства загрузки достаточно: установщик уже ограничивает частоту, а QML всё равно перечитывает их разом.
| Свойство | Тип | READ | WRITE | NOTIFY | Описание |
|---|---|---|---|---|---|
downloading |
bool |
downloading |
— | downloadChanged |
Идёт какая-либо загрузка. Только для чтения |
downloadProgress |
double |
downloadProgress |
— | downloadChanged |
Доля выполнения от 0 до 1; -1 — итог неизвестен. Только для чтения |
downloadVersion |
QString |
downloadVersion |
— | downloadChanged |
Что именно качается — версия, модлоадер, сборка Java или пак. Только для чтения |
downloadStatus |
QString |
downloadStatus |
— | downloadChanged |
Строка состояния загрузки. Только для чтения |
downloadBytesDone |
qint64 |
downloadBytesDone |
— | downloadChanged |
Принято байт. Только для чтения |
downloadBytesTotal |
qint64 |
downloadBytesTotal |
— | downloadChanged |
Ожидаемый объём в байтах; 0 — неизвестен. Только для чтения |
Панель загрузки одна на все четыре источника: свойства отдают числа того установщика или загрузчика, который работает сейчас.
Методы Q_INVOKABLE
Все перечисленные ниже методы вызываются из QML.
Профили
void addProfile(const QString &name, const QString &login, const QString &password, const QString &authType = "offline")
Добавляет профиль игрока. Параметр authType принимает значения offline, elyby и microsoft.
Для офлайн-профиля пароль не нужен, для профиля Microsoft не нужны ни логин, ни пароль. Испускает
profilesChanged.
void updateProfile(int index, const QString &name, const QString &login, const QString &password, const QString &authType = "offline")
Перезаписывает профиль по индексу теми же полями. Испускает profilesChanged.
QVariantMap profileAt(int index) const
Данные профиля для диалога редактирования: имя, логин, пароль, тип, а также признак наличия действующей сессии Microsoft и ник, полученный при официальной авторизации.
void removeProfile(int index)
Удаляет профиль. Испускает profilesChanged.
Вход через Microsoft
void startMicrosoftLogin(int profileIndex)
Начинает вход в аккаунт Microsoft: испускает microsoftLoginUrlReady с адресом страницы входа.
Значение -1 в profileIndex означает, что вход ещё не привязан к профилю: профиль создастся по
нику, который вернут Minecraft Services.
void finishMicrosoftLogin(const QString &code)
Завершает вход по коду авторизации, перехваченному окном браузера. Итог приходит сигналом
microsoftLoginSucceeded или microsoftLoginFailed.
void cancelMicrosoftLogin()
Сбрасывает начатую сессию входа. Вызывается, когда пользователь закрыл окно или адрес возврата пришёл без кода.
QVariantMap inspectMicrosoftRedirect(const QString &url) const
Разбирает адрес, на который встроенное окно возвращается после входа. Возвращает карту с полями
matched (является ли адрес адресом возврата), code и error.
Разбор живёт в C++, чтобы правила совпадения не разъезжались с теми, по которым сервис сам строит
redirect_uri.
Сборки
void addCustomBuild(const QString &name, const QString &serverUrl, const QString &minecraftVersion = QString())
Создаёт пользовательскую сборку. Испускает customBuildsChanged.
void updateCustomBuild(int index, const QVariantMap &fields)
Мержит в сборку только присланные ключи: name, serverUrl, minecraftVersion, loader,
loaderVersion, resolvedVersionId. Остальные поля остаются как были — это и позволяет карточке
сборки сохранять правки по одному полю за раз.
QVariantMap customBuildAt(int index) const
Данные сборки для карточки редактирования.
QVariantMap customBuildRemovalInfo(int index) const
Что именно потеряется при удалении сборки — для текста предупреждения. Возвращает имя сборки и три признака: есть ли у неё архив, активна ли она сейчас и последняя ли она.
void removeCustomBuild(int index)
Удаляет сборку вместе с её архивом. Испускает customBuildsChanged.
void installCustomBuild(int index)
Докачивает то, чего не хватает выбранной сборке: версию игры и, если он выбран, модлоадер. Вынесено отдельной кнопкой, потому что карточка сборки сохраняет правки по ходу редактирования и установка не должна начинаться сама при каждой правке.
QStringList checkInstallation(int buildIndex) const
Проверка комплектности без запуска — для подсказки в интерфейсе. Пустой список означает, что сборку можно запускать; иначе возвращаются описания недостающих файлов.
Запуск игры
void launchGame(int profileIndex, int buildIndex)
Главная кнопка. Проверяет выбор профиля и версии, комплектность .minecraft, при необходимости
авторизуется и стартует игру.
Авторизация асинхронна, поэтому метод возвращается сразу; дальнейший ход виден по сигналам
launchProgress, launched, launchError и twoFactorRequired.
void submitTwoFactorCode(const QString &code)
Продолжает прерванный запуск, отдавая одноразовый код двухфакторной аутентификации. Вызывается
после сигнала twoFactorRequired.
void cancelPendingLaunch()
Отменяет запуск, остановленный на ожидании кода двухфакторной аутентификации.
void stopGame()
Завершает процесс игры.
Каталог версий
void refreshVersionCatalog(bool force = false)
Обновляет каталог версий. Вызывается при открытии окна выбора версии: свежий кэш отвечает без
сети. Параметр force обходит проверку свежести.
bool isVersionInstalled(const QString &versionId) const
Установлена ли версия в папке игры.
void installVersion(const QString &versionId)
Ставит версию игры в фоне. Ход виден по свойствам загрузки.
QVariantMap versionRemovalInfo(const QString &versionId) const
Что потеряется при удалении версии — для текста предупреждения. Возвращает признак установки, занимаемый объём в мегабайтах, список зависящих профилей модлоадеров и список сборок, которые эту версию используют.
void removeVersion(const QString &versionId)
Удаляет файлы версии из versions/. Библиотеки и ресурсы остаются: они общие для всех версий.
Испускает installedVersionsChanged.
void cancelDownload()
Отменяет текущую загрузку.
Модлоадеры
QVariantList loaderVersions(const QString &loaderKey, const QString &gameVersion) const
Версии модлоадера для выбранной версии игры. Возвращает список карт с полями version, label,
recommended и stable.
Несовместимых строк в списке нет — отбор заложен в сам источник данных, поэтому проверять совместимость вызывающему коду не нужно. Пустой список означает, что лоадер эту версию игры не поддерживает.
Параметр loaderKey принимает значения forge, fabric, neoforge и quilt.
void refreshLoaderVersions(const QString &loaderKey, const QString &gameVersion, bool force = false)
Запрашивает обновление списка версий лоадера. Результат приходит сигналом loaderVersionsChanged.
bool loaderVersionsLoading(const QString &loaderKey, const QString &gameVersion) const
Идёт ли сейчас запрос по этой паре. Позволяет интерфейсу отличить «ещё грузим» от «не поддерживается».
void installLoaderForBuild(int index)
Ставит модлоадер, выбранный в сборке, и записывает получившийся профиль в resolvedVersionId.
Настройки
QVariantMap settings() const
Настройки запуска одной картой: gameDir, javaPath, javaRuntime, minMemoryMb,
maxMemoryMb, jvmArgs, windowWidth, windowHeight, fullscreen, language и вычисленный
resolvedGameDir.
void updateSettings(const QVariantMap &values)
Записывает настройки и сохраняет их на диск. Испускает settingsChanged.
Смену language после записи пробрасывает в Localization — порядок
«сохранили → переключили» гарантирует, что выбранный язык переживёт падение сразу после
переключения. Сам Localization в settings.json не пишет: файл ведёт только бэкенд.
QStringList detectedJava() const
Пути ко всем java, найденным в системе. Показывается справочной строкой в диалоге настроек.
Сборки Java
void refreshJavaCatalog(bool force = false)
Обновляет каталог сборок Java. Вызывается при открытии окна выбора: свежий кэш отвечает без сети.
void installJavaRuntime(const QString &runtimeId)
Скачивает и распаковывает сборку Java. По завершении испускается javaRuntimeInstalled.
void removeJavaRuntime(const QString &runtimeId)
Удаляет скачанную сборку Java из папки лаунчера.
QVariantMap javaRuntimeInfo(const QString &runtimeId) const
Описание установленной сборки: подпись, версия, путь к java и признак установки. Пустая карта означает, что сборки с таким идентификатором в папке лаунчера нет.
Метод не является привязкой и сам не пересчитывается, когда сборка докачается, — диалог настроек
обновляет его по сигналу javaRuntimeInstalled.
int requiredJavaMajor(int buildIndex) const
Минимальная мажорная версия Java для версии игры выбранной сборки; 0 — версия не выбрана.
Передаётся в окно выбора Java, чтобы пометить слишком старые сборки.
Сезонные сборки
void refreshSeasonalCatalog(bool force = false)
Обновляет каталог сезонных сборок.
void installSeasonalBuild(const QString &seasonalId)
Ставит или обновляет сборку целиком одной цепочкой: запись сборки, версия игры, модлоадер, Java и файлы.
Порядок шагов жёсткий: сначала сборка делается активной, затем ставится Java — она нужна
установщику Forge, — затем модлоадер, затем качается пак и только в конце его файлы раскатываются
поверх .minecraft. Раскатывать файлы имеет смысл только когда всё остальное на месте.
Строка каталога копируется на момент старта: обновление списка посреди установки не должно менять
то, что ставится. По завершении испускается seasonalInstallFinished.
void cancelSeasonalInstall()
Отменяет установку сезонной сборки.
Папки
void openMinecraftFolder()
Открывает папку модов Minecraft в файловом менеджере системы.
void openGameFolder()
Открывает корневую папку игры в файловом менеджере системы.
Сигналы
Сигналы уведомления свойств
Эти сигналы объявлены как NOTIFY соответствующих свойств; обработчик перечитывает свойство.
profilesChanged, customBuildsChanged, activeBuildChanged, switchChanged,
installedVersionsChanged, settingsChanged, gameRunningChanged, busyChanged,
versionCatalogChanged, catalogLoadingChanged, javaCatalogChanged,
javaCatalogLoadingChanged, seasonalCatalogChanged, seasonalCatalogLoadingChanged,
seasonalInstallingChanged и downloadChanged.
loaderVersionsChanged(const QString &loaderKey, const QString &gameVersion)
Список версий модлоадера изменился. Параметры сужают событие до конкретной пары, поэтому обработчик обязан сверить их со своим текущим состоянием: обновление может относиться к другой строке лоадера или к прошлой версии игры. Именно так поступает LoaderRow.
События установки
seasonalInstallFinished(const QString &seasonalId, const QString &buildName)
Сезонная сборка установлена и активна — можно запускать игру. Обработчик показывает сообщение пользователю.
javaRuntimeInstalled(const QString &runtimeId)
Сборка Java установлена. Диалог настроек по этому сигналу обновляет подпись выбранной сборки, не переоткрываясь.
Запуск игры
launchProgress(const QString &message)
Описание текущего шага запуска. Обработчик показывает сообщение без таймаута: шаг может занять заметное время, и сообщение должно держаться до следующего.
launched(const QString &profileName, const QString &buildName, const QString &serverUrl)
Игра запущена. Обработчик сообщает пользователю, какой профиль и какая сборка стартовали.
launchError(const QString &message)
Запуск не удался либо произошла ошибка, о которой нужно сказать пользователю. Через этот же сигнал сообщается о проблемах записи файлов лаунчера.
twoFactorRequired(const QString &profileName)
Ely.by отклонил пароль с пометкой two factor. Обработчик открывает диалог ввода кода и передаёт
введённое значение в submitTwoFactorCode(); отказ должен вызвать cancelPendingLaunch(), иначе
запуск останется висеть в ожидании.
gameOutput(const QString &line)
Строка вывода процесса игры. Обработчик пишет её в журнал.
gameFinished(int exitCode, bool crashed)
Игра завершилась. Обработчик сообщает итог: ненулевой код или выставленный crashed показываются
как ошибка.
Вход через Microsoft
microsoftLoginUrlReady(const QString &url)
Окну входа Microsoft: открыться на этом адресе. Обработчик создаёт окно (в сборке с Qt WebEngine) и открывает его.
microsoftLoginSucceeded(const QString &playerName)
Вход выполнен. Обработчик сообщает об этом пользователю, но не трогает выбор в списке профилей: новый профиль уже выбран тем, кто его создал, а повторный вход мог быть и не в последний профиль.
microsoftLoginFailed(const QString &message)
Вход не удался.
microsoftReloginRequired(int profileIndex)
Сессия профиля протухла настолько, что нужен повторный вход руками. Обработчик обычно сразу
вызывает startMicrosoftLogin() для этого профиля.
Владение и время жизни
Класс наследует QObject и принимает parent. Экземпляр создаётся декларативно в QML, поэтому
временем его жизни управляет движок QML: объект живёт столько же, сколько главное окно.
Все двенадцать сервисов создаются в конструкторе с бэкендом в роли родителя и уничтожаются вместе с ним. Порядок создания важен для двоих: VersionInstaller принимает в конструктор сервис манифеста, а ModLoaderInstaller — сервис версий лоадеров и установщик версий; эти указатели не переходят во владение принимающей стороны.
Кэши каталогов помечены mutable и пересобираются лениво из константных геттеров: QML читает
свойства помногу раз за кадр, пока открыт список, и пересборка по каждому чтению обошлась бы
дорого.
Потокобезопасность
Только поток GUI. Единственная работа в другом потоке — файловые операции над содержимым
.minecraft, и она полностью инкапсулирована в BuildSwitcher: сам бэкенд
общается с ним обычными сигналами и слотами.
Доступ из QML
Класс зарегистрирован макросом QML_ELEMENT в модуле Minecraft_launcher, объявленном через
qt_add_qml_module в CMakeLists.txt. Имя типа в QML совпадает с именем класса —
LauncherBackend.
Из QML доступны все 26 свойств, все 42 метода Q_INVOKABLE и все сигналы, перечисленные выше.
Синглтоном тип не объявлен: экземпляр создаётся декларативно в Main.qml и
передаётся во вложенные диалоги через их свойство backend. Все диалоги проекта объявляют его как
required property var backend.
Объект, созданный из QML, принадлежит движку QML — удалять его из C++ нельзя.
Взаимодействие с другими классами
Вниз, к сервисам. Бэкенд подписан на сигналы всех двенадцати сервисов и сводит их к своим
свойствам. Четыре разных источника загрузки — установщик версий, установщик модлоадеров,
установщик Java и загрузчик паков — отображаются в одну группу свойств download*, поэтому панель
в интерфейсе не различает, кто работает; какой из источников показывать, бэкенд решает сам.
Вверх, к QML. Интерфейс не обращается ни к одному сервису напрямую. Каталоги отдаются уже
сведёнными с локальным состоянием: строка версии знает про installed, строка сезонной сборки —
про установленную ревизию и доступное обновление.
Состояние на диске. Профили, сборки и настройки читаются при создании и пишутся при каждом
изменении. Отсутствие файла — норма (первый запуск), а повреждённое содержимое отводится в файл с
расширением .bak, чтобы рабочий файл создался заново. Проблемы хранилища, замеченные на старте,
накапливаются и показываются одним сообщением, когда интерфейс уже подключился к сигналам.
Отдельно предусмотрена миграция: файл versions.json от прежней схемы именования переносится в
customBuilds.json при первом запуске после переименования.
Восстановление после сбоя. При старте бэкенд спрашивает у переключателя сборок, не было ли прервано переключение, и предлагает доиграть его.
Внешнее взаимодействие
Собственных сетевых обращений и дочерних процессов у класса нет: всё внешнее взаимодействие
делегировано сервисам — сеть у каталогов, установщиков и служб авторизации, процессы у
GameLauncher, ModLoaderInstaller и
JavaInstaller, файловые операции над .minecraft у
BuildSwitcher.
Единственное прямое обращение к системе — открытие папки игры в файловом менеджере методами
openMinecraftFolder() и openGameFolder().
Пример использования
Класс предназначен для создания из QML, а не из C++:
import QtQuick
import Minecraft_launcher
Window {
id: window
visible: true
LauncherBackend {
id: backend
onLaunchError: (message) => console.warn(message)
onTwoFactorRequired: (profileName) => twoFactorDialog.open()
onMicrosoftLoginUrlReady: (url) => window.openMicrosoftLogin(url)
}
Button {
text: backend.activeBuildName
enabled: !backend.busy && !backend.gameRunning
onClicked: backend.launchGame(profileBox.currentIndex, backend.activeBuildIndex)
}
}
При создании этого документа использовался ИИ.