2026-09-03 09:16:56 +03:00
|
|
|
|
# LauncherBackend
|
|
|
|
|
|
|
2026-10-11 22:32:13 +03:00
|
|
|
|
`launcherbackend.h`, `launcherbackend.cpp`
|
2026-09-03 09:16:56 +03:00
|
|
|
|
|
2026-10-11 22:32:13 +03:00
|
|
|
|
Через этот класс QML работает со всем остальным кодом. Интерфейс читает его свойства, вызывает
|
|
|
|
|
|
методы и слушает сигналы, а к сервисам напрямую не обращается.
|
2026-09-03 09:16:56 +03:00
|
|
|
|
|
2026-10-11 22:32:13 +03:00
|
|
|
|
Сам бэкенд почти ничего не скачивает и не запускает. Он хранит профили, сборки и настройки,
|
|
|
|
|
|
создаёт двенадцать сервисов и следит, чтобы они не мешали друг другу: нельзя ставить Java во время
|
|
|
|
|
|
смены сборки, удалять версию, пока она качается, или запускать игру посреди установки модлоадера.
|
|
|
|
|
|
Ещё он сводит каталоги из сети с тем, что лежит на диске, чтобы окнам не пришлось этого делать.
|
2026-09-03 09:16:56 +03:00
|
|
|
|
|
2026-10-11 22:32:13 +03:00
|
|
|
|
Экземпляр один, его объявляет [Main.qml](../qml/Main.md). Во вложенные окна он передаётся через
|
|
|
|
|
|
`required property var backend`. Тип зарегистрирован через `QML_ELEMENT` в модуле `BiLauncher`;
|
|
|
|
|
|
синглтоном он не сделан.
|
2026-09-03 09:16:56 +03:00
|
|
|
|
|
2026-10-11 22:32:13 +03:00
|
|
|
|
## Сервисы
|
2026-09-03 09:16:56 +03:00
|
|
|
|
|
2026-10-11 22:32:13 +03:00
|
|
|
|
Все создаются в конструкторе с бэкендом в роли родителя.
|
2026-09-03 09:16:56 +03:00
|
|
|
|
|
2026-10-11 22:32:13 +03:00
|
|
|
|
| Поле | Класс | Зачем |
|
|
|
|
|
|
|------|-------|-------|
|
|
|
|
|
|
| `m_auth` | [AuthService](AuthService.md) | Ely.by, офлайн, загрузка authlib-injector |
|
2026-09-03 09:16:56 +03:00
|
|
|
|
| `m_msa` | [MsaAuthService](MsaAuthService.md) | вход через Microsoft |
|
2026-10-11 22:32:13 +03:00
|
|
|
|
| `m_launcher` | [GameLauncher](GameLauncher.md) | запуск JVM |
|
2026-09-03 09:16:56 +03:00
|
|
|
|
| `m_manifest` | [VersionManifestService](VersionManifestService.md) | каталог версий Mojang |
|
|
|
|
|
|
| `m_installer` | [VersionInstaller](VersionInstaller.md) | установка версии игры |
|
2026-10-11 22:32:13 +03:00
|
|
|
|
| `m_loaderMeta` | [ModLoaderVersionService](ModLoaderVersionService.md) | версии модлоадеров |
|
2026-09-03 09:16:56 +03:00
|
|
|
|
| `m_loaderInstaller` | [ModLoaderInstaller](ModLoaderInstaller.md) | установка модлоадера |
|
2026-10-11 22:32:13 +03:00
|
|
|
|
| `m_switcher` | [BuildSwitcher](BuildSwitcher.md) | смена активной сборки, раскатка пака |
|
|
|
|
|
|
| `m_javaMeta` | [JavaRuntimeService](JavaRuntimeService.md) | каталог Java |
|
2026-09-03 09:16:56 +03:00
|
|
|
|
| `m_javaInstaller` | [JavaInstaller](JavaInstaller.md) | установка Java |
|
|
|
|
|
|
| `m_seasonalMeta` | [SeasonalBuildService](SeasonalBuildService.md) | каталог сезонных сборок |
|
|
|
|
|
|
| `m_packDownloader` | [SeasonalPackDownloader](SeasonalPackDownloader.md) | загрузка архива сезонной сборки |
|
|
|
|
|
|
|
2026-10-11 22:32:13 +03:00
|
|
|
|
`VersionInstaller` получает указатель на сервис манифеста, `ModLoaderInstaller` — на сервис версий
|
|
|
|
|
|
модлоадеров и на установщик версий. Владельцем этих объектов остаётся бэкенд.
|
2026-09-03 09:16:56 +03:00
|
|
|
|
|
2026-10-11 22:32:13 +03:00
|
|
|
|
## Что происходит при старте
|
2026-09-03 09:16:56 +03:00
|
|
|
|
|
2026-10-11 22:32:13 +03:00
|
|
|
|
Конструктор делает следующее, порядок важен:
|
2026-09-03 09:16:56 +03:00
|
|
|
|
|
2026-10-11 22:32:13 +03:00
|
|
|
|
1. `LauncherPaths::ensureRootExists()` создаёт папку лаунчера и проверяет, что в неё можно писать.
|
|
|
|
|
|
2. `loadSettings()` заполняет настройки значениями по умолчанию и накладывает сверху
|
|
|
|
|
|
`settings.json`.
|
|
|
|
|
|
3. `migrateLegacyBuildsFile()` переносит старый `versions.json` в `customBuilds.json`. Это должно
|
|
|
|
|
|
случиться до следующего шага, иначе поверх старых данных появится пустой файл.
|
|
|
|
|
|
4. `loadData()` читает профили и сборки и сортирует их по `id`.
|
2026-10-23 09:55:15 +03:00
|
|
|
|
5. Если это первый запуск, `importGameDir()` заводит сборку под то, что уже лежит в папке игры
|
|
|
|
|
|
(см. ниже).
|
|
|
|
|
|
6. `ensureDataFiles()` создаёт недостающие файлы.
|
|
|
|
|
|
7. Если `BuildSwitcher` нашёл журнал оборванного переключения, бэкенд сразу продолжает его через
|
2026-10-11 22:32:13 +03:00
|
|
|
|
`resumeInterrupted()`. Пользователю ничего не предлагается. Если начать переключение заново,
|
|
|
|
|
|
оно сначала заархивирует полуразобранную `.minecraft` поверх целого архива прежней сборки.
|
2026-10-23 09:55:15 +03:00
|
|
|
|
8. Подписка на сигналы сервисов.
|
2026-09-03 09:16:56 +03:00
|
|
|
|
|
2026-10-11 22:32:13 +03:00
|
|
|
|
Все проблемы, найденные по пути (битый JSON, папка без права записи, пропущенные записи сборок),
|
|
|
|
|
|
копятся в `m_storageIssues` и уходят одним `launchError` через `QTimer::singleShot(0)`: к этому
|
|
|
|
|
|
моменту QML уже подписан на сигнал.
|
2026-09-03 09:16:56 +03:00
|
|
|
|
|
2026-10-23 09:55:15 +03:00
|
|
|
|
### Сборка из папки игры
|
|
|
|
|
|
|
|
|
|
|
|
Первым запуском считается старт, когда нет ни `settings.json`, ни `customBuilds.json`, ни
|
|
|
|
|
|
старого `versions.json`. Проверка делается до `loadData()`: битый файл она отводит в `.bak`, а
|
|
|
|
|
|
`ensureDataFiles()` потом создаёт пустой. Существующий пустой `customBuilds.json` — это решение
|
|
|
|
|
|
пользователя «сборок нет», и импорта тогда нет.
|
|
|
|
|
|
|
|
|
|
|
|
Если [GameDirImport::detect()](gamedirimport.md) что-то нашёл, запись сборки добавляется сразу,
|
|
|
|
|
|
минуя `addCustomBuild()` (тот начал бы установку), становится активной и сохраняется вместе с
|
|
|
|
|
|
`activeBuildId`. `BuildSwitcher` не вызывается: сборка описывает то, что уже разложено в папке
|
|
|
|
|
|
игры, а активная сборка без архива — штатное состояние. Сама папка игры только читается.
|
|
|
|
|
|
|
|
|
|
|
|
Имя: `<loaderTitle> <версия>` или `Minecraft <версия>`, а без версии — `build.title.fromGameDir`.
|
|
|
|
|
|
Через `QTimer::singleShot(0)` приходит `gameDirImported(buildName)`, раньше сообщения об ошибках
|
|
|
|
|
|
хранилища, чтобы оно их не перекрыло. Main.qml показывает по нему зелёную плашку.
|
|
|
|
|
|
|
2026-10-11 22:32:13 +03:00
|
|
|
|
## Файлы на диске
|
2026-09-03 09:16:56 +03:00
|
|
|
|
|
2026-10-11 22:32:13 +03:00
|
|
|
|
Все пути берутся из [launcherpaths.h](launcherpaths.md).
|
2026-09-03 09:16:56 +03:00
|
|
|
|
|
2026-10-11 22:32:13 +03:00
|
|
|
|
- `settings.json` — объект с настройками;
|
|
|
|
|
|
- `profiles.json` — массив профилей;
|
|
|
|
|
|
- `customBuilds.json` — массив сборок.
|
2026-09-03 09:16:56 +03:00
|
|
|
|
|
2026-10-11 22:32:13 +03:00
|
|
|
|
Если файла нет, это первый запуск. Если он не читается как JSON или в нём не тот тип (объект вместо
|
|
|
|
|
|
массива и наоборот), файл переименовывается в `<имя>.bak`, а на его месте создаётся пустой.
|
|
|
|
|
|
Данные из `.bak` можно потом достать руками.
|
2026-09-03 09:16:56 +03:00
|
|
|
|
|
2026-10-11 22:32:13 +03:00
|
|
|
|
Перед каждой записью снова вызывается `ensureRootExists()`: папку могли удалить, пока лаунчер
|
|
|
|
|
|
работал, и без этой проверки сохранения пропадали бы без ошибки. Пишется всё через обычный `QFile`,
|
|
|
|
|
|
кроме `builds/<id>/season.json`, для которого используется `QSaveFile`.
|
2026-09-03 09:16:56 +03:00
|
|
|
|
|
2026-10-11 22:32:13 +03:00
|
|
|
|
Пароли Ely.by и токены лежат в `profiles.json` открытым текстом.
|
2026-09-03 09:16:56 +03:00
|
|
|
|
|
|
|
|
|
|
### Настройки
|
|
|
|
|
|
|
2026-10-11 22:32:13 +03:00
|
|
|
|
| Ключ | По умолчанию | Что это |
|
|
|
|
|
|
|------|--------------|---------|
|
|
|
|
|
|
| `gameDir` | пусто | папка игры; пусто — `LauncherPaths::defaultMinecraftDir()` |
|
|
|
|
|
|
| `javaPath` | пусто | путь к java, заданный руками |
|
|
|
|
|
|
| `javaRuntime` | пусто | id сборки Java из `<root>/java`; важнее `javaPath` |
|
|
|
|
|
|
| `minMemoryMb`, `maxMemoryMb` | 512, 4096 | `-Xms`, `-Xmx` |
|
|
|
|
|
|
| `jvmArgs` | пусто | дополнительные аргументы JVM одной строкой, делятся по пробелам |
|
|
|
|
|
|
| `windowWidth`, `windowHeight` | 0, 0 | размер окна игры; 0 — не передавать |
|
|
|
|
|
|
| `fullscreen` | `false` | |
|
|
|
|
|
|
| `language` | `system` | язык интерфейса, см. [Localization](Localization.md) |
|
|
|
|
|
|
| `activeBuildId` | 0 | `id` активной сборки |
|
|
|
|
|
|
| `seasonalBaseUrl` | `https://launcher-builds.galeonworx.com` | сервер сезонных сборок |
|
|
|
|
|
|
|
|
|
|
|
|
`seasonalBaseUrl` в интерфейсе нет. Он нужен, чтобы проверять установку против локального
|
|
|
|
|
|
HTTP-сервера. Хвостовые слэши отрезаются.
|
|
|
|
|
|
|
|
|
|
|
|
Имя и версия лаунчера для `${launcher_name}` и `${launcher_version}` в настройках не хранятся: они
|
|
|
|
|
|
берутся из `QCoreApplication`, то есть из `project(... VERSION)` в CMake. Ключи `launcherName` и
|
|
|
|
|
|
`launcherVersion`, оставшиеся в старых `settings.json`, при загрузке выбрасываются и пропадают из
|
|
|
|
|
|
файла при следующем сохранении.
|
|
|
|
|
|
|
|
|
|
|
|
`settings()` возвращает всю карту и добавляет к ней вычисленный `resolvedGameDir`.
|
|
|
|
|
|
`updateSettings()` вливает присланные ключи, сохраняет файл и испускает `settingsChanged`. Если
|
|
|
|
|
|
сменился язык, после записи вызывается `Localization::setLanguage()`: так выбор сохранится, даже если
|
|
|
|
|
|
лаунчер упадёт сразу после переключения. Если сменилась папка игры, перестраивается каталог версий.
|
|
|
|
|
|
|
|
|
|
|
|
## Профили
|
|
|
|
|
|
|
|
|
|
|
|
Профиль хранит имя, логин, пароль, тип входа (`offline`, `elyby`, `microsoft`; всё прочее читается
|
|
|
|
|
|
как `offline`) и полученные при входе данные: токены, UUID, ник с сервера, `xuid`, срок жизни токена.
|
|
|
|
|
|
|
|
|
|
|
|
- `addProfile(name, login, password, authType)` — добавить.
|
|
|
|
|
|
- `updateProfile(index, ...)` — перезаписать. Если у профиля Ely.by поменялись логин или пароль,
|
|
|
|
|
|
сохранённый токен сбрасывается. Если поменялся тип входа, сбрасывается всё, что было получено
|
|
|
|
|
|
при входе. Переименование профиля Microsoft сессию не трогает.
|
|
|
|
|
|
- `profileAt(index)` — `name`, `login`, `password`, `authType`, `resolvedName` и
|
|
|
|
|
|
`hasMicrosoftSession` (есть ли refresh-токен).
|
|
|
|
|
|
- `removeProfile(index)` — удалить.
|
|
|
|
|
|
|
|
|
|
|
|
Все испускают `profilesChanged`. Свойство `profileNames` отдаёт имена в порядке `id`.
|
|
|
|
|
|
|
|
|
|
|
|
## Сборки
|
|
|
|
|
|
|
|
|
|
|
|
Сборка — это имя, адрес сервера для автоподключения, версия Minecraft и, если выбран, модлоадер с
|
|
|
|
|
|
версией. После установки модлоадера в `resolvedVersionId` записывается id профиля в `versions/`,
|
|
|
|
|
|
который на самом деле запускается. У сезонных сборок есть ещё `seasonalId` и `seasonalRevision`.
|
|
|
|
|
|
|
|
|
|
|
|
Какую папку из `versions/` запускать, решает `launchVersionIdOf()`: сначала `resolvedVersionId`,
|
|
|
|
|
|
потом `minecraftVersion`, а если и её нет, то имя сборки. Последний вариант остался от старых
|
|
|
|
|
|
записей, у которых версия была записана в имени.
|
|
|
|
|
|
|
|
|
|
|
|
- `addCustomBuild(name, serverUrl, minecraftVersion)` — создать и сразу начать ставить версию, если
|
|
|
|
|
|
она указана и её нет на диске. Если версия пустая (так создаёт сборку кнопка «+ Новая сборка»),
|
|
|
|
|
|
ничего не ставится.
|
|
|
|
|
|
- `updateCustomBuild(index, fields)` — изменить только переданные ключи. Карточка сборки сохраняет
|
|
|
|
|
|
правки по одному полю, поэтому установка здесь не запускается. Если сменилась версия игры, а
|
|
|
|
|
|
`loaderVersion` в том же вызове не прислали, `loaderVersion` и `resolvedVersionId` сбрасываются;
|
|
|
|
|
|
сам `loader` остаётся.
|
|
|
|
|
|
- `customBuildAt(index)` — все поля плюс `launchVersionId`.
|
|
|
|
|
|
- `customBuildRemovalInfo(index)` — `name`, `hasArchive`, `active`, `lastOne` для текста
|
|
|
|
|
|
предупреждения.
|
|
|
|
|
|
- `removeCustomBuild(index)` — удалить запись и архив. Пока лаунчер занят или игра запущена,
|
|
|
|
|
|
отказывает. Если удалили активную, активной становится первая из оставшихся, и запускается
|
|
|
|
|
|
переключение.
|
|
|
|
|
|
- `installCustomBuild(index)` — поставить, чего не хватает. Для ванильной сборки это версия игры,
|
|
|
|
|
|
для сборки с модлоадером — модлоадер вместе с версией. Вызывает `installLoaderForBuild()`.
|
|
|
|
|
|
- `installLoaderForBuild(index)` — то же самое; по завершении записывает `resolvedVersionId`.
|
|
|
|
|
|
- `checkInstallation(index)` — список того, чего не хватает для запуска; пустой список значит, что
|
|
|
|
|
|
всё на месте.
|
|
|
|
|
|
|
|
|
|
|
|
### Активная сборка
|
|
|
|
|
|
|
|
|
|
|
|
`activeBuildIndex` хранится в настройках как `activeBuildId`, а не как индекс. Если этот `id`
|
|
|
|
|
|
пропал, активной считается первая сборка.
|
|
|
|
|
|
|
|
|
|
|
|
Запись в свойство запускает `BuildSwitcher::switchTo()`: содержимое `.minecraft` уезжает в архив
|
|
|
|
|
|
прежней сборки, на его место распаковывается архив новой. Смена отклоняется, если уже идёт другая
|
|
|
|
|
|
смена, запущена игра или работает любой установщик. `activeBuildId` меняется только в обработчике
|
|
|
|
|
|
`BuildSwitcher::finished`, поэтому после неудачи активной остаётся прежняя сборка.
|
|
|
|
|
|
|
|
|
|
|
|
Пока идёт смена, выставлено `switching`, а ход виден в `switchProgress` (от 0 до 1, или −1, пока
|
|
|
|
|
|
объём неизвестен), `switchStage` и `switchStatus`.
|
|
|
|
|
|
|
|
|
|
|
|
## Версии игры
|
|
|
|
|
|
|
|
|
|
|
|
- `versionCatalog` — плоский список строк `{id, type, category, label, installed, search}` в порядке
|
|
|
|
|
|
манифеста, новые сверху. `category` — `release`, `snapshot`, `old_beta`, `old_alpha` или `other`.
|
|
|
|
|
|
В конец добавлены установленные версии, которых нет в манифесте (профили модлоадеров), по
|
|
|
|
|
|
алфавиту, с категорией `other`. Разделы и поиск делает
|
|
|
|
|
|
[VersionPickerDialog](../qml/VersionPickerDialog.md).
|
|
|
|
|
|
- `catalogLoading` — идёт запрос манифеста.
|
|
|
|
|
|
- `refreshVersionCatalog(force)` — сбросить кэш строк и попросить манифест. Свежий кэш манифеста
|
|
|
|
|
|
отвечает без сети.
|
|
|
|
|
|
- `installedVersions`, `isVersionInstalled(id)` — что лежит в `versions/`.
|
|
|
|
|
|
- `installVersion(id)` — поставить; если эта версия уже ставится или стоит в очереди, ничего не
|
|
|
|
|
|
делает.
|
|
|
|
|
|
- `versionRemovalInfo(id)` — `id`, `installed`, `sizeMb`, `dependents` (профили модлоадеров поверх
|
|
|
|
|
|
этой версии), `builds` (имена сборок, которые на неё ссылаются).
|
|
|
|
|
|
- `removeVersion(id)` — удалить папку версии. Библиотеки и ресурсы общие, их не трогает. Пока
|
|
|
|
|
|
лаунчер занят или запущена игра, отказывает. Сборки, ссылающиеся на версию, не меняются:
|
|
|
|
|
|
ссылка на неустановленную версию — нормальное состояние.
|
|
|
|
|
|
|
|
|
|
|
|
Список строк каталога строится лениво и кэшируется: QML читает свойство много раз за кадр, пока
|
|
|
|
|
|
открыт список. То же сделано для каталогов Java и сезонных сборок.
|
|
|
|
|
|
|
|
|
|
|
|
## Модлоадеры
|
|
|
|
|
|
|
|
|
|
|
|
- `loaderVersions(loaderKey, gameVersion)` — список `{version, label, recommended, stable}` для этой
|
|
|
|
|
|
версии игры, новые сверху. Несовместимых строк в нём не бывает. Пустой список означает, что
|
|
|
|
|
|
модлоадер эту версию не поддерживает.
|
|
|
|
|
|
- `refreshLoaderVersions(loaderKey, gameVersion, force)` — обновить список. По окончании всегда
|
|
|
|
|
|
приходит `loaderVersionsChanged(loaderKey, gameVersion)`, даже если ответил кэш. Обработчик должен
|
|
|
|
|
|
сверить пару со своей: сигнал может относиться к другому модлоадеру или к прошлой версии игры.
|
|
|
|
|
|
- `loaderVersionsLoading(loaderKey, gameVersion)` — помогает отличить «ещё грузится» от «не
|
|
|
|
|
|
поддерживается».
|
|
|
|
|
|
|
|
|
|
|
|
Ключи: `forge`, `fabric`, `neoforge`, `quilt`.
|
|
|
|
|
|
|
|
|
|
|
|
Установщику модлоадера передаётся та же Java, что пойдёт в игру: сначала скачанная лаунчером
|
|
|
|
|
|
сборка, потом `javaPath`.
|
|
|
|
|
|
|
|
|
|
|
|
## Java
|
|
|
|
|
|
|
|
|
|
|
|
- `javaCatalog` — строки `{id, kind, major, version, label, detail, coverage, sizeMb, installed, lts,
|
|
|
|
|
|
downloadable, search}`. Сначала то, что уже скачано в `<root>/java`, потом каталог Mojang, Temurin
|
|
|
|
|
|
JDK и Temurin JRE.
|
|
|
|
|
|
- `javaCatalogLoading`, `refreshJavaCatalog(force)` — как у версий.
|
|
|
|
|
|
- `installJavaRuntime(id)` — скачать. Пока работает другой установщик или идёт смена сборки,
|
|
|
|
|
|
отказывает. По завершении приходит `javaRuntimeInstalled(id)`.
|
|
|
|
|
|
- `removeJavaRuntime(id)` — удалить из папки лаунчера. Если это была выбранная в настройках сборка,
|
|
|
|
|
|
`javaRuntime` очищается.
|
|
|
|
|
|
- `javaRuntimeInfo(id)` — `id`, `kind`, `major`, `version`, `label`, `javaPath`, `installed`.
|
|
|
|
|
|
Если сборка выбрана, но не скачана, `installed` будет `false` и `javaPath` пустым. Для пустого
|
|
|
|
|
|
`id` возвращается пустая карта. Метод не пересчитывается сам, поэтому окно настроек перечитывает
|
|
|
|
|
|
его по `javaRuntimeInstalled`.
|
|
|
|
|
|
- `requiredJavaMajor(buildIndex)` — минимальная Java для версии игры сборки, 0 если версия не
|
|
|
|
|
|
выбрана.
|
|
|
|
|
|
- `detectedJava()` — найденные в системе java строками вида `Java 17 — /путь`, для справки в
|
|
|
|
|
|
настройках.
|
|
|
|
|
|
|
|
|
|
|
|
## Запуск игры
|
|
|
|
|
|
|
|
|
|
|
|
`launchGame(profileIndex, buildIndex)` сначала проверяет всё, что можно проверить синхронно:
|
|
|
|
|
|
|
|
|
|
|
|
- не работает ни один установщик, не идёт другой запуск и игра не запущена;
|
|
|
|
|
|
- профиль и сборка выбраны, папка игры существует;
|
|
|
|
|
|
- версия сборки есть в `versions/`;
|
|
|
|
|
|
- описание версии читается (`VersionLoader::load`), и все нужные файлы на месте
|
|
|
|
|
|
(`GameLauncher::missingFiles`).
|
|
|
|
|
|
|
|
|
|
|
|
После этого выставляется `busy`, а сборка запоминается по `id`, а не по индексу: пока идёт вход,
|
|
|
|
|
|
её могут удалить в другом окне.
|
|
|
|
|
|
|
|
|
|
|
|
Дальше зависит от типа профиля:
|
|
|
|
|
|
|
|
|
|
|
|
- **офлайн** — ник из логина или имени профиля, UUID считается из ника;
|
|
|
|
|
|
- **Ely.by** — сначала докачивается `authlib-injector` в `<root>/runtime`, потом вход. Если Ely.by
|
|
|
|
|
|
просит второй фактор, `busy` снимается и приходит `twoFactorRequired(profileName)`. Окно должно
|
|
|
|
|
|
вызвать `submitTwoFactorCode(code)` или `cancelPendingLaunch()`, иначе запуск так и останется
|
|
|
|
|
|
в ожидании;
|
|
|
|
|
|
- **Microsoft** — если refresh-токена нет, запуск прерывается с `microsoftReloginRequired(index)`.
|
|
|
|
|
|
Если сохранённый токен Minecraft живёт ещё дольше двух минут, сеть не трогается. Иначе сессия
|
|
|
|
|
|
продлевается по refresh-токену. Если продлить не вышло, тоже `microsoftReloginRequired`. Если у
|
|
|
|
|
|
аккаунта нет лицензии, просто ошибка.
|
|
|
|
|
|
|
|
|
|
|
|
Полученные при входе ник, UUID и токены сохраняются в профиль. Java выбирается так: скачанная
|
|
|
|
|
|
лаунчером сборка из `javaRuntime`, иначе `javaPath`, иначе её ищет `GameLauncher`. Если
|
|
|
|
|
|
`javaRuntime` задан, но такой сборки на диске нет, об этом будет сообщение в `launchProgress`.
|
|
|
|
|
|
|
|
|
|
|
|
Ход виден по сигналам `launchProgress`, `launched(profileName, buildName, serverUrl)` и
|
|
|
|
|
|
`launchError`. `stopGame()` завершает процесс. Пока игра работает, `gameRunning` равно `true`;
|
|
|
|
|
|
вывод процесса приходит построчно в `gameOutput`, конец — в `gameFinished(exitCode, crashed)`.
|
|
|
|
|
|
|
|
|
|
|
|
## Вход через Microsoft из окна профиля
|
|
|
|
|
|
|
|
|
|
|
|
1. `startMicrosoftLogin(profileIndex)` испускает `microsoftLoginUrlReady(url)`. `-1` значит, что
|
|
|
|
|
|
профиля ещё нет и его нужно создать по нику из аккаунта. Без WebEngine сразу приходит
|
|
|
|
|
|
`microsoftLoginFailed`.
|
|
|
|
|
|
2. [MicrosoftLoginDialog](../qml/MicrosoftLoginDialog.md) открывает страницу и на каждую смену
|
|
|
|
|
|
адреса вызывает `inspectMicrosoftRedirect(url)`. Ответ: `{matched, code, error}`. Разбор сделан
|
|
|
|
|
|
в C++, чтобы правила совпадали с тем, как `MsaAuthService` строит `redirect_uri`.
|
|
|
|
|
|
3. Код уходит в `finishMicrosoftLogin(code)`. Итог приходит в `microsoftLoginSucceeded(playerName)`
|
|
|
|
|
|
или `microsoftLoginFailed(message)`.
|
|
|
|
|
|
|
|
|
|
|
|
`cancelMicrosoftLogin()` забывает начатый вход. Если окно закрыли, пока шёл обмен кода, результат
|
|
|
|
|
|
будет выброшен.
|
|
|
|
|
|
|
|
|
|
|
|
`microsoftAvailable` — константа: собран ли лаунчер с Qt WebEngine.
|
|
|
|
|
|
|
|
|
|
|
|
## Сезонные сборки
|
|
|
|
|
|
|
|
|
|
|
|
- `seasonalCatalog` — строки каталога с полями сервера (`id`, `name`, `minecraftVersion`, `loader`,
|
|
|
|
|
|
`loaderTitle`, `loaderVersion`, `modCount`, `seasonStart`, `seasonEnd` в виде `дд.мм.гггг`,
|
|
|
|
|
|
`sizeBytes`, `revision`, `description`, `serverUrl`) и локальным состоянием (`installed`,
|
|
|
|
|
|
`installedRevision`, `updateAvailable`, `status`). Сборка считается установленной, если есть
|
|
|
|
|
|
запись с таким `seasonalId`. Обновление доступно, если её ревизия меньше серверной.
|
|
|
|
|
|
- `seasonalCatalogLoading`, `seasonalCatalogError`.
|
|
|
|
|
|
- `refreshSeasonalCatalog(force)` — перед запросом заново читает `seasonalBaseUrl`.
|
|
|
|
|
|
- `installSeasonalBuild(id)` — поставить или обновить.
|
|
|
|
|
|
- `cancelSeasonalInstall()` — отменить.
|
|
|
|
|
|
- `seasonalInstalling` — идёт установка.
|
|
|
|
|
|
|
|
|
|
|
|
Установка — цепочка шагов. Каждый шаг либо запускает асинхронную работу и ждёт сигнала, либо
|
|
|
|
|
|
решает, что делать нечего, и сразу передаёт ход следующему (`seasonAdvance()` со сквозными `case`).
|
|
|
|
|
|
|
|
|
|
|
|
1. **Запись.** Если сборки с таким `seasonalId` нет, она создаётся. Если есть, в неё записываются
|
|
|
|
|
|
новые версия, модлоадер и адрес сервера. Ревизию пока не трогаем: её запишем, только когда файлы
|
|
|
|
|
|
лягут на диск, иначе оборванная установка выглядела бы законченной.
|
|
|
|
|
|
2. **Switching.** Сборка становится активной: пак раскатывается поверх текущей `.minecraft`.
|
|
|
|
|
|
3. **Java.** Нужная версия берётся из `javaMajor` каталога или из таблицы требований. Если
|
|
|
|
|
|
подходящая java уже есть (своя, системная или скачанная раньше), шаг пропускается. Иначе
|
|
|
|
|
|
загружается каталог Java и ставится лучшая сборка для этой версии. Java ставится раньше
|
|
|
|
|
|
модлоадера, потому что она нужна установщику Forge.
|
|
|
|
|
|
4. **Loader.** Для ванильной сборки ставится версия игры, для остальных — модлоадер. Если всё уже
|
|
|
|
|
|
установлено и укомплектовано, шаг пропускается.
|
|
|
|
|
|
5. **Downloading.** Архив качается в `builds/<id>/.pack-r<ревизия>.zip`. Точка в начале имени нужна,
|
|
|
|
|
|
чтобы `BuildSwitcher` не принял файл за архив сборки и не стёр его.
|
|
|
|
|
|
6. **Applying.** `BuildSwitcher::applyPack()` удаляет всё, что принесла прошлая ревизия (список из
|
|
|
|
|
|
`season.json`), и распаковывает новую. Миры и скриншоты в этот список не попадают, потому что
|
|
|
|
|
|
пак их не приносил.
|
|
|
|
|
|
|
|
|
|
|
|
После раскатки `recordSeasonalPack()` пишет `builds/<id>/season.json` (`seasonalId`, `revision`,
|
|
|
|
|
|
`sha256`, `installedAt`, `entries`) и проставляет ревизию в запись сборки. Это делается и тогда,
|
|
|
|
|
|
когда раскатка была доиграна после перезапуска лаунчера. Конец цепочки — `seasonalInstallFinished
|
|
|
|
|
|
(seasonalId, buildName)`.
|
|
|
|
|
|
|
|
|
|
|
|
При ошибке на любом шаге скачанный архив удаляется, и приходит `launchError`. На шагах Switching и
|
|
|
|
|
|
Applying отменить нельзя: `.minecraft` уже разобрана, операцию нужно довести до конца.
|
|
|
|
|
|
|
|
|
|
|
|
Строка каталога копируется в `m_season.entry` при старте. Если каталог обновится во время установки,
|
|
|
|
|
|
ставиться будет всё равно то, что выбрали.
|
|
|
|
|
|
|
|
|
|
|
|
## Панель загрузки
|
|
|
|
|
|
|
|
|
|
|
|
Источников загрузки четыре: установщик версий, установщик модлоадера, установщик Java и загрузчик
|
|
|
|
|
|
пака. Наружу они видны через одну группу свойств: `downloading`, `downloadProgress`,
|
|
|
|
|
|
`downloadVersion` (что качается), `downloadStatus` (`этап — файл`), `downloadBytesDone`,
|
|
|
|
|
|
`downloadBytesTotal`. Все обновляются одним сигналом `downloadChanged`.
|
2026-09-03 09:16:56 +03:00
|
|
|
|
|
2026-10-11 22:32:13 +03:00
|
|
|
|
Чьи числа показывать, решается так. Пак качается, только когда остальные уже закончили. Java не
|
|
|
|
|
|
качается одновременно с версией или модлоадером. Во время установки модлоадера доля берётся у
|
|
|
|
|
|
`VersionInstaller`, если он сейчас качает файлы версии, потому что его оценка точнее.
|
|
|
|
|
|
|
|
|
|
|
|
`cancelDownload()` останавливает первое, что работает, в порядке: модлоадер (он сам остановит
|
|
|
|
|
|
вложенную установку версии), версия, Java, пак.
|
|
|
|
|
|
|
|
|
|
|
|
`busy` равно `true`, пока идёт вход или запуск, работает любой установщик, идёт смена сборки или
|
|
|
|
|
|
качается пак. На это время в интерфейсе гаснет кнопка запуска.
|
|
|
|
|
|
|
|
|
|
|
|
## Папки
|
|
|
|
|
|
|
|
|
|
|
|
- `openMinecraftFolder()` открывает `<gameDir>/mods` (создаёт, если её нет).
|
|
|
|
|
|
- `openGameFolder()` открывает саму папку игры.
|
|
|
|
|
|
|
|
|
|
|
|
## Потоки
|
|
|
|
|
|
|
|
|
|
|
|
Весь класс работает в GUI-потоке. В отдельном потоке выполняются только файловые операции
|
|
|
|
|
|
`BuildSwitcher`, и этот поток скрыт внутри него.
|
|
|
|
|
|
|
|
|
|
|
|
## Пример
|
2026-09-03 09:16:56 +03:00
|
|
|
|
|
|
|
|
|
|
```qml
|
|
|
|
|
|
import QtQuick
|
2026-10-11 22:32:13 +03:00
|
|
|
|
import BiLauncher
|
2026-09-03 09:16:56 +03:00
|
|
|
|
|
|
|
|
|
|
Window {
|
|
|
|
|
|
visible: true
|
|
|
|
|
|
|
|
|
|
|
|
LauncherBackend {
|
|
|
|
|
|
id: backend
|
|
|
|
|
|
onLaunchError: (message) => console.warn(message)
|
|
|
|
|
|
onTwoFactorRequired: (profileName) => twoFactorDialog.open()
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
Button {
|
|
|
|
|
|
text: backend.activeBuildName
|
|
|
|
|
|
enabled: !backend.busy && !backend.gameRunning
|
|
|
|
|
|
onClicked: backend.launchGame(profileBox.currentIndex, backend.activeBuildIndex)
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
```
|
|
|
|
|
|
|
|
|
|
|
|
---
|
|
|
|
|
|
|
|
|
|
|
|
При создании этого документа использовался ИИ.
|