1 parent
1943996608
commit
f10b654953
34 files changed
+2099
-4576
No files matched your search
+58
-141
@@ -1,170 +1,87 @@
|
||||
# AuthService
|
||||
|
||||
## Обзор класса
|
||||
`authservice.h`, `authservice.cpp`
|
||||
|
||||
`Minecraft_launcher` поддерживает три способа входа: офлайн-профиль без пароля, учётную запись
|
||||
Ely.by и учётную запись Microsoft. `AuthService` закрывает первые два: это Yggdrasil-клиент Ely.by
|
||||
плюс офлайн-режим. За третий отвечает [MsaAuthService](MsaAuthService.md).
|
||||
Вход через Ely.by, офлайн-профиль и загрузка `authlib-injector`. Вход через Microsoft живёт
|
||||
отдельно, в [MsaAuthService](MsaAuthService.md), но отдаёт результат в той же структуре
|
||||
`AuthResult`. Поэтому запуск игры дальше идёт одинаково, как бы игрок ни вошёл.
|
||||
|
||||
Результат любого способа — структура `AuthResult`, объявленная в этом же заголовке. Она содержит
|
||||
ровно то, что подставляется в аргументы запуска вида `${auth_*}`, поэтому дальше запуск игры идёт
|
||||
по общему пути независимо от того, как пользователь вошёл.
|
||||
Экземпляр создаёт и держит [LauncherBackend](LauncherBackend.md). Сервис ничего не пишет на диск,
|
||||
кроме скачанного jar: токены между запусками сохраняет бэкенд в `profiles.json`.
|
||||
|
||||
Класс также умеет скачивать `authlib-injector` — библиотеку, которая перенаправляет обращения игры
|
||||
к серверу авторизации на Ely.by.
|
||||
## AuthResult
|
||||
|
||||
## Место в проекте и зависимости
|
||||
Каждое поле соответствует подстановке в аргументах запуска.
|
||||
|
||||
Экземпляр создаётся и принадлежит [LauncherBackend](LauncherBackend.md); других владельцев нет.
|
||||
Заголовок подключает [MsaAuthService](MsaAuthService.md) — ради общей структуры `AuthResult` — и
|
||||
[GameLauncher](GameLauncher.md) через поля `LaunchOptions`, которые заполняются из результата
|
||||
авторизации.
|
||||
| Поле | Подстановка / смысл |
|
||||
|------|---------------------|
|
||||
| `ok` | вход удался |
|
||||
| `twoFactorRequired` | Ely.by требует одноразовый код |
|
||||
| `licenseMissing` | Microsoft: вход прошёл, но игры на аккаунте нет. Отдельный флаг, потому что повторный вход тут не поможет |
|
||||
| `error` | текст ошибки, если `ok == false` |
|
||||
| `playerName` | `${auth_player_name}` |
|
||||
| `uuid` | `${auth_uuid}`, hex без дефисов |
|
||||
| `accessToken` | `${auth_access_token}` |
|
||||
| `clientToken` | `${clientid}` |
|
||||
| `userType` | `${user_type}`: `legacy` (офлайн), `ELYBY`, `msa` |
|
||||
| `refreshToken` | только Microsoft: продление сессии без пароля |
|
||||
| `xuid` | только Microsoft: `${auth_xuid}` |
|
||||
| `expiresAt` | только Microsoft: когда истекает `accessToken`, UTC |
|
||||
|
||||
Требования сборки: `Qt6::Core` (`QDateTime`, `QJsonObject`, `QString`) и `Qt6::Network`
|
||||
(`QNetworkAccessManager`).
|
||||
## Офлайн
|
||||
|
||||
## Иерархия и роль
|
||||
`static AuthResult offline(nickname)` работает без сети. UUID считается так же, как в самом
|
||||
Minecraft: MD5 от `OfflinePlayer:<ник>`, после чего выставляются биты версии 3 и варианта RFC 4122.
|
||||
Значит, у одного ника всегда один UUID, и на офлайн-сервере игрок не теряет инвентарь. В
|
||||
`accessToken` и `clientToken` записывается `"0"`.
|
||||
|
||||
Наследует `QObject`: даёт мета-объектную систему, сигнал `progress` и владение по родителю.
|
||||
Виртуальных методов базового класса не переопределяет.
|
||||
Пустой ник — ошибка.
|
||||
|
||||
## Публичные структуры
|
||||
## Ely.by
|
||||
|
||||
### AuthResult
|
||||
Сервер — `https://authserver.ely.by`, протокол Yggdrasil, JSON по HTTPS.
|
||||
|
||||
Результат авторизации — то, что подставляется в `${auth_*}` аргументы запуска.
|
||||
`loginElyBy(login, password, clientToken, accessToken, callback)` старается не спрашивать пароль:
|
||||
|
||||
| Поле | Тип | Описание |
|
||||
|------|-----|----------|
|
||||
| `ok` | `bool` | Авторизация удалась |
|
||||
| `twoFactorRequired` | `bool` | Ely.by отклонил пароль с пометкой two factor — нужен одноразовый код |
|
||||
| `licenseMissing` | `bool` | Вход в Microsoft прошёл, но копии игры на аккаунте нет. Обрабатывается отдельно от прочих ошибок, потому что чинится только покупкой |
|
||||
| `error` | `QString` | Текст ошибки, когда `ok` равен `false` |
|
||||
| `playerName` | `QString` | Подставляется в `${auth_player_name}` |
|
||||
| `uuid` | `QString` | Подставляется в `${auth_uuid}`; hex без дефисов |
|
||||
| `accessToken` | `QString` | Подставляется в `${auth_access_token}` |
|
||||
| `clientToken` | `QString` | Подставляется в `${clientid}` |
|
||||
| `userType` | `QString` | Подставляется в `${user_type}`; принимает значения `legacy` (офлайн), `msa` (Microsoft) и `ELYBY` |
|
||||
| `refreshToken` | `QString` | Только для аккаунтов Microsoft: продлевает сессию без ввода пароля |
|
||||
| `xuid` | `QString` | Только для Microsoft; подставляется в `${auth_xuid}` |
|
||||
| `expiresAt` | `QDateTime` | Только для Microsoft: UTC-время, когда протухает `accessToken` |
|
||||
1. Если сохранённого `accessToken` нет, сразу `/auth/authenticate` с паролем.
|
||||
2. Если есть, `/auth/validate`. Ответ 200 — токен рабочий, вход закончен.
|
||||
3. Иначе `/auth/refresh`. Если продлить удалось, приходят новый токен, ник и UUID.
|
||||
4. Если и это не вышло, `/auth/authenticate` с паролем.
|
||||
|
||||
## Псевдонимы типов
|
||||
Если сервер недоступен (статус 0 и сетевая ошибка), цепочка обрывается с ошибкой, без попытки войти
|
||||
по паролю.
|
||||
|
||||
`AuthService::Callback` — `std::function<void(const AuthResult &)>`. Все сетевые методы
|
||||
асинхронные: колбэк вызывается ровно один раз и всегда в потоке GUI.
|
||||
Второй фактор Ely.by не сообщает отдельным кодом. Сервис ищет в тексте ошибки подстроку
|
||||
`two factor` и выставляет `twoFactorRequired`. Тогда бэкенд спрашивает код у игрока и вызывает
|
||||
`loginElyByWithTotp(login, password, totp, clientToken, callback)`, который отправляет пароль в виде
|
||||
`пароль:код`, как требует Ely.by.
|
||||
|
||||
## Публичные методы
|
||||
`clientToken` генерируется один раз на профиль (`generateClientToken()`, случайный UUID) и хранится
|
||||
вместе с ним: Yggdrasil привязывает выданный `accessToken` к этому значению.
|
||||
|
||||
#### explicit AuthService(QObject \*parent = nullptr)
|
||||
По ходу работы испускается `progress(message)`. Бэкенд пересылает его в `launchProgress`.
|
||||
|
||||
Создаёт сервис и его `QNetworkAccessManager`. Конструктор помечен `explicit`.
|
||||
## authlib-injector
|
||||
|
||||
#### static AuthResult offline(const QString &nickname)
|
||||
Без него игра пойдёт проверять сессию на серверы Mojang, и токен Ely.by окажется недействительным.
|
||||
Jar подключается к JVM через `-javaagent` (см. [GameLauncher](GameLauncher.md)).
|
||||
|
||||
Готовит результат для офлайн-профиля без единого сетевого запроса. UUID выводится из ника так же,
|
||||
как это делает сам Minecraft в офлайне: `UUID.nameUUIDFromBytes(("OfflinePlayer:" + name)
|
||||
.getBytes(UTF_8))`. Благодаря этому один и тот же ник всегда даёт один и тот же UUID, и прогресс
|
||||
на сервере не теряется.
|
||||
`ensureAuthlibInjector(targetDir, callback)`:
|
||||
|
||||
#### static QString generateClientToken()
|
||||
- если `<targetDir>/authlib-injector.jar` уже есть, сразу отдаёт путь. Версия не проверяется, и
|
||||
обновить файл можно, только удалив его;
|
||||
- иначе читает `https://authlib-injector.yushi.moe/artifact/latest.json`, скачивает jar по
|
||||
`download_url` и, если в манифесте есть `checksums.sha256`, сверяет хеш.
|
||||
|
||||
Случайный `clientToken` лаунчера. Генерируется один раз на профиль и хранится вместе с ним:
|
||||
Yggdrasil связывает выданный `accessToken` именно с этим значением.
|
||||
Колбэк получает либо путь, либо текст ошибки. `targetDir` — это `<root>/runtime`.
|
||||
|
||||
#### void loginElyBy(const QString &login, const QString &password, const QString &clientToken, const QString &accessToken, Callback callback)
|
||||
## Как вызываются колбэки
|
||||
|
||||
Полный цикл входа в Ely.by: сначала проверка имеющегося токена, затем его продление, и только при
|
||||
неудаче — авторизация по паролю. Пароль можно оставить пустым, если уже есть рабочий
|
||||
`accessToken`, — тогда пользователю не придётся вводить его заново.
|
||||
Все сетевые методы асинхронные, колбэк вызывается один раз в GUI-потоке. Если сервис удалить во
|
||||
время запроса, менеджер сети уйдёт вместе с ним, и колбэк не вызовется вообще. Бэкенд к тому же
|
||||
проверяет в каждом колбэке, не отменили ли запуск, пока шёл запрос.
|
||||
|
||||
По ходу работы испускает `progress` с описанием текущего шага. Результат приходит в `callback`
|
||||
один раз; при ответе с пометкой двухфакторной аутентификации в нём выставлен
|
||||
`twoFactorRequired`, и вызывающий код должен спросить у пользователя код и продолжить через
|
||||
`loginElyByWithTotp()`.
|
||||
|
||||
#### void loginElyByWithTotp(const QString &login, const QString &password, const QString &totp, const QString &clientToken, Callback callback)
|
||||
|
||||
Повтор авторизации с одноразовым кодом двухфакторной аутентификации. Пароль и код объединяются в
|
||||
одно поле в формате «пароль:код», как того требует Ely.by.
|
||||
|
||||
#### void ensureAuthlibInjector(const QString &targetDir, std::function<void(const QString &path, const QString &error)> callback)
|
||||
|
||||
Скачивает `authlib-injector` в `targetDir`, если его там ещё нет. Колбэк получает либо путь к
|
||||
готовому jar, либо текст ошибки — заполнено всегда ровно одно из двух.
|
||||
|
||||
Библиотека нужна только для профилей Ely.by: она подключается к JVM аргументом `-javaagent` и
|
||||
перенаправляет обращения игры к серверу авторизации.
|
||||
|
||||
## Сигналы
|
||||
|
||||
#### progress(const QString &message)
|
||||
|
||||
Описание текущего шага авторизации. Испускается по ходу всех сетевых операций.
|
||||
|
||||
Обработчик показывает сообщение пользователю: в главном окне лаунчера оно попадает в плашку
|
||||
статуса и держится до следующего сообщения, потому что шаг может занять заметное время.
|
||||
|
||||
## Владение и время жизни
|
||||
|
||||
Класс наследует `QObject` и принимает `parent` в конструкторе — родитель его и удалит.
|
||||
`QNetworkAccessManager` создаётся в конструкторе с сервисом в роли родителя и уничтожается вместе
|
||||
с ним.
|
||||
|
||||
Колбэки захватываются по значению и живут до своего единственного вызова. Уничтожение сервиса во
|
||||
время незавершённого запроса отменяет запрос вместе с менеджером сети — колбэк в этом случае не
|
||||
вызывается, поэтому захватывать в него сырые указатели на объекты с меньшим временем жизни, чем у
|
||||
сервиса, нельзя.
|
||||
|
||||
## Потокобезопасность
|
||||
|
||||
Только поток GUI. Все сетевые методы асинхронные, и их колбэки вызываются в том же потоке, в
|
||||
котором создан сервис. Собственной синхронизации в классе нет.
|
||||
|
||||
## Взаимодействие с другими классами
|
||||
|
||||
`LauncherBackend` вызывает методы входа при запуске игры и переправляет сигнал `progress` в
|
||||
интерфейс. Полученный `AuthResult` он раскладывает по полям `LaunchOptions`, которые уходят в
|
||||
[GameLauncher](GameLauncher.md). Путь, возвращённый `ensureAuthlibInjector()`, попадает в поле
|
||||
`authlibInjectorPath` тех же параметров запуска.
|
||||
|
||||
Сохранением токенов между запусками занимается `LauncherBackend`: сам сервис ничего не пишет на
|
||||
диск, кроме скачанного jar.
|
||||
|
||||
## Внешнее взаимодействие
|
||||
|
||||
**Сеть, исходящие запросы.** Класс общается с сервером авторизации Ely.by через
|
||||
`QNetworkAccessManager`. Формат — JSON поверх HTTPS, запросы инициирует всегда лаунчер. Внутренний
|
||||
помощник `postJson()` разделяет три исхода: успешный ответ, ответ с кодом ошибки и транспортную
|
||||
ошибку — последняя отдаётся отдельным параметром, чтобы отличить недоступную сеть от отказа
|
||||
сервера.
|
||||
|
||||
Отдельным каналом идёт загрузка `authlib-injector` — обычная HTTPS-загрузка файла в
|
||||
`targetDir`. Повторных попыток при неудаче класс не делает: решение о повторе принимает вызывающий
|
||||
код.
|
||||
|
||||
Все сигналы и колбэки приходят в поток GUI.
|
||||
|
||||
## Пример использования
|
||||
|
||||
```cpp
|
||||
auto *auth = new AuthService(this);
|
||||
connect(auth, &AuthService::progress, this, &Backend::showStatus);
|
||||
|
||||
auth->loginElyBy(profile.login, profile.password,
|
||||
profile.clientToken, profile.accessToken,
|
||||
[this](const AuthResult &result) {
|
||||
if (result.twoFactorRequired) {
|
||||
emit twoFactorRequired(m_pendingProfileName);
|
||||
return;
|
||||
}
|
||||
if (!result.ok) {
|
||||
emit launchError(result.error);
|
||||
return;
|
||||
}
|
||||
continueLaunch(result);
|
||||
});
|
||||
```
|
||||
Повторных попыток при сетевых ошибках нет.
|
||||
|
||||
---
|
||||
|
||||
|
||||
+33
-144
@@ -1,165 +1,54 @@
|
||||
# BuildArchiveWorker
|
||||
|
||||
## Обзор класса
|
||||
`buildarchiveworker.h`, `buildarchiveworker.cpp`
|
||||
|
||||
Смена сборки в лаунчере — это перекладывание содержимого `.minecraft`: текущее упаковывается в
|
||||
архив, папка вычищается, на её место распаковывается архив другой сборки. Речь о гигабайтах модов,
|
||||
конфигов и миров, и гонять их в потоке GUI нельзя — окно замерзало бы на всё время смены.
|
||||
Файловые операции над `.minecraft`: упаковать, очистить, распаковать, раскатать пак. В
|
||||
`.minecraft` могут лежать гигабайты модов и миров, поэтому это единственная часть проекта,
|
||||
которая работает не в GUI-потоке.
|
||||
|
||||
`BuildArchiveWorker` — исполнитель этих операций в отдельном потоке. Он умеет четыре вещи:
|
||||
упаковать, вычистить, распаковать и докатить пак сезонной сборки поверх уже разложенного
|
||||
содержимого.
|
||||
Объект создаёт [BuildSwitcher](BuildSwitcher.md) без родителя, переносит в свой `QThread` и вызывает
|
||||
слоты через `QMetaObject::invokeMethod(..., Qt::QueuedConnection)`. Работник не знает ни про
|
||||
сборки, ни про журнал. Он получает пути и отвечает сигналами.
|
||||
|
||||
Класс намеренно ничего не знает ни о сборках, ни о путях лаунчера: он принимает готовые пути и
|
||||
списки. Порядок шагов и восстановление после сбоя — дело [BuildSwitcher](BuildSwitcher.md).
|
||||
ZIP читается и пишется через `QZipReader` и `QZipWriter` из `Qt6::CorePrivate`.
|
||||
|
||||
## Место в проекте и зависимости
|
||||
## Слоты
|
||||
|
||||
Создаётся и целиком управляется [BuildSwitcher](BuildSwitcher.md), который же и переносит его в
|
||||
собственный `QThread`. Больше к классу никто не обращается.
|
||||
Каждый слот заканчивается одним `finished(ok, error)`. По ходу работы испускается
|
||||
`progress(done, total, currentPath)` на каждый файл.
|
||||
|
||||
Требования сборки: `Qt6::Core` и `Qt6::CorePrivate` — последний нужен ради `QZipReader` и
|
||||
`QZipWriter`, которыми читаются и пишутся архивы сборок.
|
||||
### archive(gameDir, tempZipPath, excludeTop)
|
||||
|
||||
## Иерархия и роль
|
||||
Упаковывает всё содержимое `gameDir`, кроме папок и файлов верхнего уровня из `excludeTop`.
|
||||
Символьные ссылки пропускаются. Сжатие `AutoCompress`: Qt сам не сжимает то, что уже сжато.
|
||||
При ошибке или отмене недописанный архив удаляется.
|
||||
|
||||
Наследует `QObject`: мета-объектная система, слоты, сигналы и возможность жить в отдельном потоке
|
||||
через `moveToThread()`. Виртуальных методов базового класса не переопределяет.
|
||||
### clear(gameDir, keepTop)
|
||||
|
||||
Объект живёт в своём `QThread` и вызывается только через очередь сигналов — прямых вызовов его
|
||||
слотов из потока GUI быть не должно.
|
||||
Удаляет всё на верхнем уровне `gameDir`, кроме `keepTop`. Символьная ссылка удаляется как файл,
|
||||
то, на что она указывает, не трогается. Если `gameDir` нет, это успех. Отменить нельзя.
|
||||
|
||||
## Публичные методы
|
||||
### restore(zipPath, gameDir)
|
||||
|
||||
#### explicit BuildArchiveWorker(QObject \*parent = nullptr)
|
||||
Распаковывает архив в `gameDir`. Пути с `..`, абсолютные, с `\` или `:` останавливают распаковку.
|
||||
|
||||
Создаёт исполнителя. Конструктор помечен `explicit`.
|
||||
### applyPack(zipPath, gameDir, removeRelative, forbiddenTop)
|
||||
|
||||
#### void requestCancel()
|
||||
1. Сначала проверяет весь архив. Небезопасный путь или файл внутри общих папок (`forbiddenTop`:
|
||||
`versions`, `libraries` и т. д.) — отказ до любых изменений. Пак не может подменить jar игры
|
||||
или библиотеки.
|
||||
2. Удаляет файлы из `removeRelative`. Опасные пути и пути в общих папках молча пропускаются. После
|
||||
этого удаляются папки, которые стали пустыми.
|
||||
3. Распаковывает пак, перезаписывая существующие файлы.
|
||||
4. Испускает `packEntries(список файлов пака)`, потом `finished(true)`.
|
||||
|
||||
Просит прервать текущую операцию. Вызывается из потока GUI прямо во время работы — это
|
||||
единственный метод класса, предназначенный для вызова снаружи рабочего потока.
|
||||
## Отмена
|
||||
|
||||
Отмена не прерывает операцию мгновенно: признак проверяется между файлами. Хранится он в
|
||||
`QAtomicInt`, поэтому запись из одного потока и чтение из другого безопасны без блокировок.
|
||||
`requestCancel()` и `clearCancel()` выставляют атомарный флаг, его можно трогать из любого потока.
|
||||
Флаг проверяется перед каждым файлом в `archive`, `restore` и `applyPack`.
|
||||
|
||||
#### void clearCancel()
|
||||
|
||||
Сбрасывает признак отмены перед началом новой операции.
|
||||
|
||||
## Публичные слоты
|
||||
|
||||
Все четыре слота вызываются через очередь сигналов и выполняются в рабочем потоке. Каждый
|
||||
завершается сигналом `finished`.
|
||||
|
||||
#### void archive(const QString &gameDir, const QString &tempZipPath, const QStringList &excludeTop)
|
||||
|
||||
Упаковывает всё содержимое `gameDir` во временный файл `tempZipPath`. Элементы верхнего уровня,
|
||||
перечисленные в `excludeTop`, в архив не попадают — так из архива сборки исключаются общие
|
||||
каталоги лаунчера, которые не принадлежат ни одной сборке.
|
||||
|
||||
Запись идёт во временный файл, чтобы прерванная упаковка не оставила повреждённый архив на месте
|
||||
настоящего.
|
||||
|
||||
#### void clear(const QString &gameDir, const QStringList &keepTop)
|
||||
|
||||
Удаляет из `gameDir` всё, кроме элементов верхнего уровня, перечисленных в `keepTop`. Выполняется
|
||||
после упаковки, перед распаковкой другой сборки.
|
||||
|
||||
#### void restore(const QString &zipPath, const QString &gameDir)
|
||||
|
||||
Распаковывает архив сборки в `gameDir`.
|
||||
|
||||
#### void applyPack(const QString &zipPath, const QString &gameDir, const QStringList &removeRelative, const QStringList &forbiddenTop)
|
||||
|
||||
Докатывает пак сезонной сборки поверх уже разложенного содержимого. Сначала удаляет файлы из
|
||||
`removeRelative` — те, что ушли из сборки в новой ревизии, — затем распаковывает архив с
|
||||
перезаписью.
|
||||
|
||||
Того, чего нет ни в списке, ни в архиве, операция не касается: миры и скриншоты игрока остаются на
|
||||
месте. Именно это отличает обновление сезонной сборки от её переустановки.
|
||||
|
||||
Параметр `forbiddenTop` — элементы верхнего уровня, которые паку трогать нельзя: общие каталоги
|
||||
лаунчера. Проверяется здесь, а не только у издателя пака, потому что архив приезжает из сети.
|
||||
|
||||
## Сигналы
|
||||
|
||||
#### progress(int done, int total, const QString ¤tPath)
|
||||
|
||||
Ход операции: сколько файлов обработано из скольких и какой обрабатывается сейчас. Испускается по
|
||||
ходу всех четырёх операций.
|
||||
|
||||
Обработчик — `BuildSwitcher` — пересчитывает долю выполнения и обновляет панель прогресса. Сигнал
|
||||
приходит в поток GUI через очередь, поэтому прямого доступа к состоянию рабочего потока у
|
||||
обработчика нет.
|
||||
|
||||
#### finished(bool ok, const QString &error)
|
||||
|
||||
Операция завершена. При `ok == false` во втором параметре — текст ошибки.
|
||||
|
||||
Обработчик решает, что делать дальше: перейти к следующему шагу смены сборки или откатить уже
|
||||
сделанное.
|
||||
|
||||
#### packEntries(const QStringList &entries)
|
||||
|
||||
Что именно принёс пак — относительными путями. Испускается только из `applyPack()`.
|
||||
|
||||
Список сохраняется в описании сезонной сборки: следующему обновлению он нужен, чтобы вычислить,
|
||||
какие файлы из сборки ушли, и передать их в `removeRelative`.
|
||||
|
||||
## Владение и время жизни
|
||||
|
||||
Класс наследует `QObject` и принимает `parent`, но на практике родителя не имеет: объект,
|
||||
перенесённый в другой поток через `moveToThread()`, не может иметь родителя в потоке GUI.
|
||||
Ответственность за его удаление лежит на `BuildSwitcher`, который создаёт и поток, и исполнителя.
|
||||
|
||||
Удалять объект следует безопасным для потоков способом — не напрямую из потока GUI во время
|
||||
работы. Уничтожение потока раньше исполнителя приведёт к обрыву незавершённой операции.
|
||||
|
||||
## Потокобезопасность
|
||||
|
||||
Класс рассчитан на жизнь в отдельном потоке. Все четыре слота выполняются в рабочем потоке;
|
||||
вызывать их напрямую нельзя — только через очередь сигналов.
|
||||
|
||||
Единственная точка межпотокового взаимодействия — признак отмены в `QAtomicInt`:
|
||||
`requestCancel()` и `clearCancel()` пишут его из потока GUI, а рабочий поток читает между файлами.
|
||||
Другого разделяемого состояния у класса нет, поэтому иных блокировок не требуется.
|
||||
|
||||
## Взаимодействие с другими классами
|
||||
|
||||
Единственный собеседник — [BuildSwitcher](BuildSwitcher.md). Он создаёт поток и исполнителя,
|
||||
переносит второй в первый, вызывает слоты через очередь сигналов и принимает `progress`,
|
||||
`finished` и `packEntries`.
|
||||
|
||||
Пути и списки исключений `BuildSwitcher` берёт из [launcherpaths.h](launcherpaths.md) и из
|
||||
описаний сборок; сам исполнитель к ним не обращается.
|
||||
|
||||
## Внешнее взаимодействие
|
||||
|
||||
**Файловая система.** Класс читает, пишет и удаляет файлы в папке игры и в каталоге архивов
|
||||
сборок. Сетевых обращений и дочерних процессов у него нет: архив сезонной сборки к моменту вызова
|
||||
`applyPack()` уже скачан [SeasonalPackDownloader](SeasonalPackDownloader.md).
|
||||
|
||||
Все операции выполняются в рабочем потоке, все сигналы приходят в поток GUI через очередь.
|
||||
|
||||
## Пример использования
|
||||
|
||||
```cpp
|
||||
auto *thread = new QThread(this);
|
||||
auto *worker = new BuildArchiveWorker; // без родителя: уедет в другой поток
|
||||
worker->moveToThread(thread);
|
||||
connect(thread, &QThread::finished, worker, &QObject::deleteLater);
|
||||
thread->start();
|
||||
|
||||
connect(worker, &BuildArchiveWorker::progress, this, &Switcher::onProgress);
|
||||
connect(worker, &BuildArchiveWorker::finished, this, &Switcher::onStepFinished);
|
||||
|
||||
worker->clearCancel();
|
||||
QMetaObject::invokeMethod(worker, "archive", Qt::QueuedConnection,
|
||||
Q_ARG(QString, gameDir),
|
||||
Q_ARG(QString, tempZipPath),
|
||||
Q_ARG(QStringList, excludeTop));
|
||||
```
|
||||
Файлы распаковываются через `QFile`, а не `QSaveFile`. Если операцию прервать, последний файл
|
||||
может остаться недописанным. Поэтому `BuildSwitcher` и не даёт отменить шаги после архивации.
|
||||
|
||||
---
|
||||
|
||||
|
||||
+72
-177
@@ -1,210 +1,105 @@
|
||||
# BuildSwitcher
|
||||
|
||||
## Обзор класса
|
||||
`buildswitcher.h`, `buildswitcher.cpp`
|
||||
|
||||
`BuildSwitcher` меняет активную сборку: содержимое `.minecraft` уезжает в архив своей сборки,
|
||||
папка чистится, на её место разворачивается архив выбранной. Он же докатывает пак сезонной
|
||||
сборки поверх уже разложенного содержимого.
|
||||
Смена активной сборки и раскатка пака сезонной сборки. Файлы двигает
|
||||
[BuildArchiveWorker](BuildArchiveWorker.md) в отдельном потоке, а этот класс решает, в каком порядке
|
||||
это делать, и ведёт журнал, по которому можно продолжить после падения.
|
||||
|
||||
Порядок шагов подчинён одному правилу: пока новый архив не записан целиком и не переименован на
|
||||
место, из `.minecraft` не удаляется ничего. Отметка о начатом переключении пишется в
|
||||
`builds/index.json` до первого разрушающего действия, поэтому обрыв питания или принудительное
|
||||
завершение процесса всегда обнаружим на следующем запуске.
|
||||
Экземпляр держит [LauncherBackend](LauncherBackend.md).
|
||||
|
||||
Сами файловые операции выполняет [BuildArchiveWorker](BuildArchiveWorker.md) в отдельном потоке;
|
||||
`BuildSwitcher` — это порядок шагов, учёт состояния и восстановление после сбоя.
|
||||
## Как устроены сборки на диске
|
||||
|
||||
## Место в проекте и зависимости
|
||||
В `.minecraft` в каждый момент разложена одна сборка, активная. Содержимое остальных лежит в
|
||||
архивах `<root>/builds/<id>/<дата>_<имя>.zip`, по одному на сборку.
|
||||
|
||||
Экземпляр создаётся и принадлежит [LauncherBackend](LauncherBackend.md). Класс создаёт и целиком
|
||||
владеет своим `QThread` и объектом [BuildArchiveWorker](BuildArchiveWorker.md).
|
||||
Общие для всех сборок папки и файлы никогда не архивируются и не удаляются (`sharedTopLevel()`):
|
||||
|
||||
Пути к папкам сборок и файлу состояния даёт [launcherpaths.h](launcherpaths.md)
|
||||
(`buildStorageDir()`, `buildDir()`).
|
||||
- `assets`
|
||||
- `libraries`
|
||||
- `versions`
|
||||
- `launcher_profiles.json`
|
||||
- `runtime`
|
||||
|
||||
Требования сборки: `Qt6::Core` (`QThread`, `QJsonObject`) и `Qt6::CorePrivate` — опосредованно,
|
||||
через исполнителя.
|
||||
Всё остальное (моды, конфиги, миры, скриншоты, `options.txt`) принадлежит сборке.
|
||||
|
||||
## Иерархия и роль
|
||||
В папке сборки могут лежать и другие zip, но файлы с точкой в начале имени не считаются архивом
|
||||
сборки. Это временные архивы (`.tmp-*.zip`) и скачанные паки сезонных сборок (`.pack-r*.zip`).
|
||||
|
||||
Наследует `QObject`: мета-объектная система, три сигнала и владение по родителю. Объявлен
|
||||
виртуальный деструктор — класс обязан корректно остановить свой поток.
|
||||
## switchTo(fromId, fromName, toId, toName, gameDir)
|
||||
|
||||
## Публичные методы
|
||||
1. **Архивация.** Если прежней сборки нет (`fromId < 0`) или папки игры нет, шаг пропускается.
|
||||
Иначе сначала проверяется свободное место: размер всего, что уйдёт в архив, плюс 256 МБ. Потом
|
||||
архив пишется во временный `builds/<from>/.tmp-<uuid>.zip`. Если на этом шаге что-то пошло не
|
||||
так, `.minecraft` не тронута.
|
||||
2. **Фиксация архива.** Временный файл переименовывается в `<гггг-мм-дд>_<имя>.zip`, остальные
|
||||
архивы этой сборки удаляются. В `index.json` записывается `state: "switching"` с `from` и `to`.
|
||||
3. **Очистка.** Из `.minecraft` удаляется всё, кроме общего.
|
||||
4. **Распаковка.** Если у новой сборки есть архив, он распаковывается. Если нет (новая сборка),
|
||||
`.minecraft` остаётся пустой, только с общими папками.
|
||||
5. В `index.json` записываются `state: "idle"` и `active`, испускается `finished(toId)`.
|
||||
|
||||
#### explicit BuildSwitcher(QObject \*parent = nullptr)
|
||||
При ошибке приходит `failed(toId, message, gameDirIntact)`. `gameDirIntact` равен `true`, если
|
||||
ошибка случилась до очистки. Бэкенд по нему выбирает текст: «ничего не изменилось» или «папка
|
||||
игры разобрана».
|
||||
|
||||
Создаёт переключатель, рабочий поток и исполнителя, переносит второй в первый и запускает поток.
|
||||
Конструктор помечен `explicit`.
|
||||
`cancel()` работает только на шаге архивации. После него отступать уже некуда.
|
||||
|
||||
#### bool isRunning() const
|
||||
## applyPack(buildId, buildName, packZip, removeRelative, note, gameDir)
|
||||
|
||||
Идёт ли сейчас переключение или раскатка пака.
|
||||
Раскатка пака поверх активной сборки. Сначала в `index.json` пишется `state: "applying"` со всем,
|
||||
что нужно для повтора: имя файла пака, имя сборки, список на удаление и `note`. Потом работник
|
||||
удаляет файлы прошлой ревизии и распаковывает пак.
|
||||
|
||||
#### QString stage() const
|
||||
После успеха пак удаляется, `index.json` возвращается в `idle`, а `lastPackEntries()` и
|
||||
`lastPackNote()` содержат список разложенных файлов и `note`. Бэкенд забирает их в обработчике
|
||||
`finished` и пишет `season.json`.
|
||||
|
||||
Текущий этап словами — заголовок панели прогресса.
|
||||
`gameDirIntact` при ошибке раскатки равен `true`, только если работник не успел обработать ни одного
|
||||
файла.
|
||||
|
||||
#### QString status() const
|
||||
## После падения
|
||||
|
||||
Строка состояния: что обрабатывается сейчас.
|
||||
При старте бэкенд вызывает `interruptedSwitchWarning()`. Если в `index.json` осталось
|
||||
`switching` или `applying`, метод возвращает текст для пользователя, а `resumeInterrupted(gameDir)`
|
||||
продолжает операцию:
|
||||
|
||||
#### double fraction() const
|
||||
- `switching` — архив прежней сборки уже записан целиком, поэтому начинаем с очистки и
|
||||
распаковываем сборку `to`. Начинать с архивации нельзя: заархивировалась бы полуразобранная
|
||||
`.minecraft`;
|
||||
- `applying` — раскатка повторяется с тем же паком и тем же списком на удаление. Если файла пака
|
||||
уже нет, журнал просто сбрасывается в `idle`.
|
||||
|
||||
Доля выполнения от `0` до `1` либо `-1`, пока итог неизвестен.
|
||||
Если программа упала во время самой архивации, в журнале ещё ничего нет, и продолжать нечего:
|
||||
`.minecraft` цела, а временный архив удалится при следующем старте (`sweepTempArchives()` в
|
||||
конструкторе).
|
||||
|
||||
#### static QStringList sharedTopLevel()
|
||||
## Прочее
|
||||
|
||||
Общие для всех сборок каталоги верхнего уровня. Они не архивируются и не чистятся: принадлежат
|
||||
лаунчеру, а не какой-либо сборке. Список передаётся исполнителю как `excludeTop` при упаковке и
|
||||
как `keepTop` при очистке.
|
||||
- `forgetBuild(id)` — удаляет запись о сборке из `index.json` и папку `builds/<id>` целиком.
|
||||
- `hasArchive(id)` — есть ли у сборки архив.
|
||||
- `isRunning()`, `stage()`, `status()` (текущий файл), `fraction()` — для панели прогресса.
|
||||
Сигнал `progressChanged()` не ограничен по частоте и приходит на каждый обработанный файл.
|
||||
|
||||
#### void switchTo(int fromBuildId, const QString &fromName, int toBuildId, const QString &toName, const QString &gameDir)
|
||||
### index.json
|
||||
|
||||
Переключает активную сборку. Проходит четыре этапа: упаковка текущего содержимого во временный
|
||||
файл, фиксация архива переименованием на место, очистка папки игры и распаковка архива целевой
|
||||
сборки.
|
||||
`<root>/builds/index.json`:
|
||||
|
||||
Имена сборок нужны для текста в панели прогресса, идентификаторы — для путей к архивам.
|
||||
|
||||
#### void applyPack(int buildId, const QString &buildName, const QString &packZipPath, const QStringList &removeRelative, const QJsonObject ¬e, const QString &gameDir)
|
||||
|
||||
Докатывает пак сезонной сборки поверх содержимого `.minecraft`. Вызывается только для уже активной
|
||||
сборки: пак ложится на то, что сейчас разложено, а не внутрь чужого архива.
|
||||
|
||||
Параметр `removeRelative` — файлы, ушедшие из сборки в новой ревизии; они удаляются перед
|
||||
распаковкой. Всего остального операция не касается, поэтому миры и скриншоты игрока переживают
|
||||
обновление.
|
||||
|
||||
Параметр `note` — непрозрачные данные вызывающей стороны. Они переживают перезапуск вместе с
|
||||
отметкой о незавершённой операции и возвращаются через `lastPackNote()`, когда раскатка доиграна.
|
||||
Сам переключатель в них не заглядывает.
|
||||
|
||||
#### QStringList lastPackEntries() const
|
||||
|
||||
Что принёс последний успешно раскатанный пак — относительными путями. Список нужен следующему
|
||||
обновлению, чтобы вычислить, какие файлы из сборки ушли.
|
||||
|
||||
#### QJsonObject lastPackNote() const
|
||||
|
||||
Данные, с которыми пришёл последний успешно раскатанный пак, — те самые, что передавались в
|
||||
`applyPack()`.
|
||||
|
||||
#### void cancel()
|
||||
|
||||
Отменяет текущую операцию. Отмена доходит до исполнителя через признак, проверяемый между
|
||||
файлами.
|
||||
|
||||
Отменять имеет смысл только на этапе упаковки: после очистки `.minecraft` отступать некуда, и
|
||||
операцию нужно довести до конца. Именно поэтому панель прогресса смены сборки объявлена
|
||||
неотменяемой.
|
||||
|
||||
#### bool forgetBuild(int buildId)
|
||||
|
||||
Сборку удалили — убирает её архив и запись о нём. Возвращает `false`, если папку архива не удалось
|
||||
удалить целиком.
|
||||
|
||||
#### bool hasArchive(int buildId) const
|
||||
|
||||
Есть ли у сборки сохранённый архив. Используется в тексте предупреждения об удалении: вместе со
|
||||
сборкой пропадут её моды, конфиги и миры.
|
||||
|
||||
#### QString interruptedSwitchWarning() const
|
||||
|
||||
Незавершённое переключение с прошлого запуска. Возвращает пустую строку, если всё в порядке, иначе
|
||||
— готовый текст для пользователя.
|
||||
|
||||
Проверяется при старте лаунчера: отметка в `builds/index.json` пишется до первого разрушающего
|
||||
действия, поэтому прерванная операция обнаруживается всегда.
|
||||
|
||||
#### void resumeInterrupted(const QString &gameDir)
|
||||
|
||||
Доигрывает прерванное переключение: очистку и распаковку целевой сборки. Архив исходной сборки к
|
||||
этому моменту уже записан — правило порядка шагов это гарантирует.
|
||||
|
||||
## Сигналы
|
||||
|
||||
#### progressChanged()
|
||||
|
||||
Изменились числа прогресса. Обработчик перечитывает `stage()`, `status()` и `fraction()`.
|
||||
|
||||
#### finished(int toBuildId)
|
||||
|
||||
Переключение или раскатка завершены успешно; в параметре — идентификатор сборки, ставшей активной.
|
||||
|
||||
Обработчик снимает признак переключения, обновляет активную сборку и, после раскатки пака,
|
||||
забирает `lastPackEntries()` и `lastPackNote()`, чтобы записать их в описание сезонной сборки.
|
||||
|
||||
#### failed(int toBuildId, const QString &message, bool gameDirIntact)
|
||||
|
||||
Операция не удалась. Третий параметр — ключевой: он говорит, цела ли папка игры. Неудача на этапе
|
||||
упаковки оставляет `.minecraft` нетронутой, неудача после очистки — нет, и сообщение пользователю
|
||||
должно различать эти случаи.
|
||||
|
||||
## Владение и время жизни
|
||||
|
||||
Класс наследует `QObject` и принимает `parent` — родитель его и удалит.
|
||||
|
||||
Рабочий поток и исполнитель создаются в конструкторе и принадлежат переключателю. Исполнитель
|
||||
родителя не имеет — объект, живущий в другом потоке, не может принадлежать объекту из потока GUI;
|
||||
его удаление привязано к завершению потока. Деструктор обязан остановить поток и дождаться его
|
||||
завершения, иначе рабочая операция переживёт своего владельца.
|
||||
|
||||
Временный архив на диске переживает аварийное завершение процесса; накопившиеся временные файлы
|
||||
подчищаются при следующем запуске.
|
||||
|
||||
## Потокобезопасность
|
||||
|
||||
Сам переключатель живёт в потоке GUI: все его публичные методы вызываются оттуда. Тяжёлая работа
|
||||
вынесена в отдельный поток к [BuildArchiveWorker](BuildArchiveWorker.md), общение с которым идёт
|
||||
исключительно через очередь сигналов — прямых вызовов слотов исполнителя нет.
|
||||
|
||||
Единственный межпотоковый примитив — атомарный признак отмены внутри исполнителя.
|
||||
|
||||
## Взаимодействие с другими классами
|
||||
|
||||
`LauncherBackend` вызывает `switchTo()` при смене активной сборки, `applyPack()` — при установке
|
||||
или обновлении сезонной сборки, `forgetBuild()` — при удалении сборки. Сигналы переключателя он
|
||||
переправляет в свойства, которые читает QML: панель прогресса главного окна показывает `stage()`,
|
||||
`status()` и `fraction()`, а признак переключения блокирует список сборок и кнопки в
|
||||
[BuildsDialog](../qml/BuildsDialog.md).
|
||||
|
||||
При старте бэкенд спрашивает `interruptedSwitchWarning()` и, если операция была прервана,
|
||||
предлагает доиграть её через `resumeInterrupted()`.
|
||||
|
||||
Архив сезонной сборки к моменту вызова `applyPack()` уже скачан
|
||||
[SeasonalPackDownloader](SeasonalPackDownloader.md).
|
||||
|
||||
## Внешнее взаимодействие
|
||||
|
||||
**Файловая система.** Класс читает и пишет `builds/index.json` — отметку о состоянии переключения,
|
||||
— управляет папками архивов сборок и подчищает временные файлы. Все операции с содержимым самой
|
||||
папки игры делегированы исполнителю в рабочем потоке.
|
||||
|
||||
Сети и дочерних процессов у класса нет.
|
||||
|
||||
## Пример использования
|
||||
|
||||
```cpp
|
||||
auto *switcher = new BuildSwitcher(this);
|
||||
|
||||
connect(switcher, &BuildSwitcher::progressChanged, this, &Backend::switchChanged);
|
||||
connect(switcher, &BuildSwitcher::finished, this, &Backend::onSwitchFinished);
|
||||
connect(switcher, &BuildSwitcher::failed, this,
|
||||
[this](int, const QString &message, bool gameDirIntact) {
|
||||
emit launchError(gameDirIntact
|
||||
? message
|
||||
: tr("%1. Папка игры осталась незавершённой.").arg(message));
|
||||
});
|
||||
|
||||
// при старте лаунчера
|
||||
const QString warning = switcher->interruptedSwitchWarning();
|
||||
if (!warning.isEmpty()) {
|
||||
showStatus(warning);
|
||||
switcher->resumeInterrupted(gameDir);
|
||||
```json
|
||||
{
|
||||
"state": "idle",
|
||||
"active": 3,
|
||||
"builds": {
|
||||
"2": { "archive": "2026-10-09_Vanilla.zip", "name": "Vanilla", "savedAt": "..." }
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
Во время работы к нему добавляются `from`, `to`, а для раскатки ещё `pack`, `packName`,
|
||||
`packRemove`, `packNote`. Пишется через `QSaveFile`.
|
||||
|
||||
Деструктор просит работника остановиться и ждёт поток до 5 секунд.
|
||||
|
||||
---
|
||||
|
||||
При создании этого документа использовался ИИ.
|
||||
+77
-170
@@ -1,199 +1,106 @@
|
||||
# GameLauncher
|
||||
|
||||
## Обзор класса
|
||||
`gamelauncher.h`, `gamelauncher.cpp`
|
||||
|
||||
`GameLauncher` — то, ради чего существует весь остальной лаунчер: он готовит и запускает JVM с
|
||||
Minecraft. К моменту его вызова уже известно всё — версия разобрана, файлы скачаны, пользователь
|
||||
авторизован, — и класс превращает это в командную строку и дочерний процесс.
|
||||
Собирает командную строку, распаковывает нативные библиотеки и запускает JVM с игрой. К этому
|
||||
моменту версия уже разобрана ([minecraftversion.h](minecraftversion.md)), файлы скачаны, а игрок
|
||||
вошёл в аккаунт. Всё остальное приходит в структуре `LaunchOptions`.
|
||||
|
||||
Кроме самого запуска класс умеет проверять комплектность `.minecraft` и распаковывать нативные
|
||||
библиотеки, без которых игра не стартует.
|
||||
Экземпляр один, его держит [LauncherBackend](LauncherBackend.md). Одновременно может работать
|
||||
только одна игра.
|
||||
|
||||
Один экземпляр — одна игра одновременно.
|
||||
## LaunchOptions
|
||||
|
||||
## Место в проекте и зависимости
|
||||
| Поле | По умолчанию | Что это |
|
||||
|------|--------------|---------|
|
||||
| `gameDir` | | папка игры, она же рабочий каталог процесса |
|
||||
| `versionId` | | папка в `versions/` |
|
||||
| `playerName`, `uuid`, `accessToken`, `userType`, `clientToken`, `xuid` | | из [AuthResult](AuthService.md) |
|
||||
| `javaPath` | пусто | пусто — ищет [JavaLocator](javalocator.md) |
|
||||
| `minMemoryMb`, `maxMemoryMb` | 512, 4096 | `-Xms`, `-Xmx`; 0 — не передавать |
|
||||
| `extraJvmArgs` | | аргументы JVM из настроек |
|
||||
| `windowWidth`, `windowHeight` | 0, 0 | 0 — не передавать размер |
|
||||
| `fullscreen` | `false` | |
|
||||
| `serverAddress` | пусто | `host[:port]`, сервер для автоподключения |
|
||||
| `authlibInjectorPath` | пусто | путь к `authlib-injector.jar`; пусто — без него |
|
||||
| `authlibInjectorApi` | `ely.by` | сервер авторизации для инжектора |
|
||||
| `launcherName`, `launcherVersion` | `BiLauncher`, `LAUNCHER_VERSION` | бэкенд передаёт имя и версию из `QCoreApplication` |
|
||||
|
||||
Подключает [minecraftversion.h](minecraftversion.md): разобранная версия — половина входных данных
|
||||
запуска, вторая половина приходит структурой `LaunchOptions` из этого же заголовка.
|
||||
## launch(options, version, &error)
|
||||
|
||||
Экземпляр создаётся и принадлежит [LauncherBackend](LauncherBackend.md), который заполняет
|
||||
`LaunchOptions` из настроек, выбранной сборки и результата авторизации.
|
||||
1. Если игра уже идёт, отказ.
|
||||
2. `missingFiles()`: если чего-то не хватает, отказ со списком.
|
||||
3. Распаковка нативных библиотек в `<gameDir>/versions/<id>/natives`.
|
||||
4. `JavaLocator::select()` с `version.javaMajor` и `options.javaPath`. На Windows вместо
|
||||
`java.exe` запускается `javaw.exe`, чтобы не было консольного окна.
|
||||
5. `QProcess` с рабочим каталогом `gameDir` и объединёнными stdout и stderr.
|
||||
6. Ожидание старта до 15 секунд. Если процесс не стартовал, отказ.
|
||||
|
||||
Требования сборки: `Qt6::Core` (`QProcess`, `QStringList`) и `Qt6::CorePrivate` — последний нужен
|
||||
ради `QZipReader`, которым распаковываются нативные библиотеки LWJGL. Зависимость от приватного
|
||||
модуля привязывает проект к конкретной версии Qt; в `CMakeLists.txt` это осознанный выбор, и
|
||||
предупреждение о нём отключено.
|
||||
Если всё прошло, метод возвращает `true` и испускает `gameStarted(commandLine)`. В этой строке
|
||||
значение после `--accessToken` заменено на `<скрыт>`, чтобы токен не попал в журнал. Других
|
||||
мест, где мог бы оказаться токен (например, в `${auth_session}`), замена не касается.
|
||||
|
||||
## Иерархия и роль
|
||||
Перед шагами 2–4 испускается `progress(message)`.
|
||||
|
||||
Наследует `QObject`: мета-объектная система, четыре сигнала и владение по родителю. Виртуальных
|
||||
методов базового класса не переопределяет.
|
||||
Вывод игры читается кусками и испускается построчно в `output(line)`. Когда процесс завершится,
|
||||
придёт `gameFinished(exitCode, crashed)`, где `crashed` значит `QProcess::CrashExit`.
|
||||
|
||||
## Публичные структуры
|
||||
`terminate()` вызывает `kill()`, а не `terminate()`. Игра не успеет ничего сохранить, это то же
|
||||
самое, что снять процесс в диспетчере задач.
|
||||
|
||||
### LaunchOptions
|
||||
## missingFiles(options, version, limit = 12)
|
||||
|
||||
Всё, что лаунчер знает к моменту нажатия кнопки запуска.
|
||||
Проверяет, что на месте папка игры, клиентский jar, все библиотеки, индекс ресурсов и папка
|
||||
`assets/objects`. Возвращает до `limit` путей. Если папки игры нет, в списке будет только она.
|
||||
|
||||
| Поле | Тип | По умолчанию | Описание |
|
||||
|------|-----|--------------|----------|
|
||||
| `gameDir` | `QString` | — | Папка `.minecraft` |
|
||||
| `versionId` | `QString` | — | Папка в `versions`, которую запускаем |
|
||||
| `playerName` | `QString` | — | Ник игрока из результата авторизации |
|
||||
| `uuid` | `QString` | — | UUID игрока |
|
||||
| `accessToken` | `QString` | — | Токен доступа |
|
||||
| `userType` | `QString` | — | Тип учётной записи: `legacy`, `msa` или `ELYBY` |
|
||||
| `clientToken` | `QString` | — | Токен клиента |
|
||||
| `xuid` | `QString` | — | Идентификатор Xbox; пустое значение заменяется на `0`, как в офлайне |
|
||||
| `javaPath` | `QString` | — | Путь к java; пустое значение означает «искать самим» |
|
||||
| `minMemoryMb` | `int` | `512` | Значение `-Xms` |
|
||||
| `maxMemoryMb` | `int` | `4096` | Значение `-Xmx` |
|
||||
| `extraJvmArgs` | `QStringList` | — | Дополнительные аргументы JVM из настроек |
|
||||
| `windowWidth` | `int` | `0` | Ширина окна игры; `0` — не передавать `--width` и `--height` |
|
||||
| `windowHeight` | `int` | `0` | Высота окна игры |
|
||||
| `fullscreen` | `bool` | `false` | Запускать в полноэкранном режиме |
|
||||
| `serverAddress` | `QString` | — | `host[:port]` для автоматического захода на сервер |
|
||||
| `authlibInjectorPath` | `QString` | — | Путь к `authlib-injector`; пустое значение — не подключать |
|
||||
| `authlibInjectorApi` | `QString` | `ely.by` | Сервер авторизации, на который перенаправляется игра |
|
||||
| `launcherName` | `QString` | `BiLauncher` | Имя лаунчера, которое видит игра |
|
||||
| `launcherVersion` | `QString` | `1.0` | Версия лаунчера |
|
||||
Размеры и хеши не проверяются, только наличие. Функция статическая, бэкенд вызывает её и для
|
||||
подсказки в интерфейсе, и перед запуском.
|
||||
|
||||
## Публичные методы
|
||||
## extractNatives(options, version, nativesDir, &error)
|
||||
|
||||
#### explicit GameLauncher(QObject \*parent = nullptr)
|
||||
Для каждой библиотеки с `native == true` открывает jar через `QZipReader` (отсюда зависимость от
|
||||
`Qt6::CorePrivate`) и вынимает файлы `.dll`, `.so`, `.so.N`, `.dylib`, `.jnilib`. Пропускаются
|
||||
`META-INF/` и пути из `extract.exclude` описания версии. Все файлы кладутся в одну плоскую папку.
|
||||
Если файл такого же размера уже лежит, его не переписывают.
|
||||
|
||||
Создаёт объект. Процесс игры при этом не запускается. Конструктор помечен `explicit`.
|
||||
## buildArguments(options, version, nativesDir)
|
||||
|
||||
#### bool isRunning() const
|
||||
Порядок аргументов:
|
||||
|
||||
Идёт ли сейчас игра. От этого зависит доступность кнопки запуска в интерфейсе.
|
||||
1. `-javaagent:<authlib-injector>=ely.by` и `-Dauthlibinjector.side=client`, если инжектор задан;
|
||||
2. `-Xms`, `-Xmx`, потом `extraJvmArgs`;
|
||||
3. JVM-аргументы из описания версии;
|
||||
4. аргумент логирования, если файл конфигурации логов скачан;
|
||||
5. главный класс;
|
||||
6. аргументы игры из описания версии;
|
||||
7. `--width`/`--height`, если размер задан, а описание версии само его не передаёт;
|
||||
8. `--fullscreen`;
|
||||
9. сервер: `--quickPlayMultiplayer host:port` для версий, которые это понимают, иначе
|
||||
`--server host --port port` (порт по умолчанию 25565).
|
||||
|
||||
#### static QStringList missingFiles(const LaunchOptions &options, const MinecraftVersion &version, int limit = 12)
|
||||
Подстановки: `auth_player_name`, `auth_uuid`, `auth_access_token`, `auth_session`
|
||||
(`token:<токен>:<uuid>`), `auth_xuid` (пусто → `0`), `clientid` (пусто → `0`), `user_type`,
|
||||
`user_properties` (`{}`), `version_name`, `version_type`, `game_directory`, `assets_root`,
|
||||
`assets_index_name`, `game_assets`, `natives_directory`, `library_directory`, `classpath`,
|
||||
`classpath_separator`, `primary_jar`, `launcher_name`, `launcher_version`, `resolution_width`,
|
||||
`resolution_height`.
|
||||
|
||||
Проверяет `.minecraft` на комплектность и возвращает описания недостающих файлов. Пустой список
|
||||
означает, что всё на месте и сборку можно запускать.
|
||||
Аргумент, в котором после подстановки остался `${`, выбрасывается целиком. Так отсекаются
|
||||
переменные, о которых лаунчер не знает (например, связанные с демо-режимом).
|
||||
|
||||
Параметр `limit` ограничивает длину списка: перечислять все отсутствующие файлы у неустановленной
|
||||
версии бессмысленно, важен сам факт и пара примеров.
|
||||
У версий со старыми ресурсами (`legacy`, `pre-1.6`) `game_assets` указывает на
|
||||
`assets/virtual/legacy`.
|
||||
|
||||
Метод статический, ничего не меняет и вызывается интерфейсом для строки состояния сборки.
|
||||
Classpath — это все не-нативные библиотеки и в конце клиентский jar.
|
||||
|
||||
#### static QStringList buildArguments(const LaunchOptions &options, const MinecraftVersion &version, const QString &nativesDir)
|
||||
### Forge 1.17+ и `-DignoreList`
|
||||
|
||||
Собирает аргументы ровно в том порядке, в котором их ждёт JVM: сначала аргументы JVM, затем главный
|
||||
класс, затем аргументы игры. Подстановки вида `${...}` из версии заменяются значениями из
|
||||
параметров запуска.
|
||||
|
||||
Метод статический и не имеет побочных эффектов, поэтому годится и для показа собранной командной
|
||||
строки без запуска.
|
||||
|
||||
#### static bool extractNatives(const LaunchOptions &options, const MinecraftVersion &version, const QString &nativesDir, QString \*error)
|
||||
|
||||
Распаковывает файлы `.dll`, `.so` и `.dylib` из нативных библиотек в `<версия>/natives`. Учитывает
|
||||
поле `extractExclude` каждой библиотеки — перечисленные там префиксы не распаковываются.
|
||||
|
||||
Возвращает `false` и заполняет `error` при неудаче. Без этого шага игра не стартует: LWJGL ищет
|
||||
нативные библиотеки именно в этой папке.
|
||||
|
||||
#### bool launch(const LaunchOptions &options, const MinecraftVersion &version, QString \*error)
|
||||
|
||||
Полный цикл запуска: проверка комплектности, распаковка нативных библиотек, поиск java, старт
|
||||
процесса.
|
||||
|
||||
Возвращает `false` и заполняет `error`, если что-то из перечисленного не удалось; `true` означает,
|
||||
что процесс запущен — дальнейшая судьба игры приходит сигналами.
|
||||
|
||||
Путь к java берётся из `options.javaPath`, а при пустом значении ищется через
|
||||
[JavaLocator](javalocator.md) с учётом требования версии.
|
||||
|
||||
#### void terminate()
|
||||
|
||||
Завершает процесс игры. Если игра не запущена, ничего не делает.
|
||||
|
||||
## Сигналы
|
||||
|
||||
#### progress(const QString &message)
|
||||
|
||||
Описание текущего шага подготовки: проверка файлов, распаковка нативных библиотек, поиск java.
|
||||
Обработчик показывает сообщение пользователю — подготовка занимает заметное время.
|
||||
|
||||
#### output(const QString &line)
|
||||
|
||||
Одна строка вывода процесса игры. Обработчик пишет её в журнал; в лаунчере это вывод в консоль.
|
||||
|
||||
#### gameStarted(const QString &commandLine)
|
||||
|
||||
Процесс запущен; в параметре — собранная командная строка целиком. Удобно для диагностики: по ней
|
||||
видно, с какими аргументами и какой java стартовала игра.
|
||||
|
||||
#### gameFinished(int exitCode, bool crashed)
|
||||
|
||||
Игра завершилась. `exitCode` — код выхода процесса, `crashed` отличает аварийное завершение от
|
||||
обычного.
|
||||
|
||||
Обработчик снимает признак «игра идёт», возвращает доступность кнопки запуска и сообщает
|
||||
пользователю итог: ненулевой код или выставленный `crashed` показываются как ошибка.
|
||||
|
||||
## Владение и время жизни
|
||||
|
||||
Класс наследует `QObject` и принимает `parent` — родитель его и удалит.
|
||||
|
||||
Процесс игры хранится в поле `m_process` и создаётся при запуске. Один экземпляр рассчитан ровно
|
||||
на одну игру одновременно: повторный вызов `launch()` при работающем процессе не предусмотрен, и
|
||||
вызывающий код обязан проверять `isRunning()`.
|
||||
|
||||
Дочерний процесс переживает уничтожение объекта не сам по себе — завершать игру перед выходом
|
||||
должен вызывающий код через `terminate()`.
|
||||
|
||||
## Потокобезопасность
|
||||
|
||||
Только поток GUI. `QProcess` привязан к потоку, в котором создан, и все сигналы приходят туда же.
|
||||
Статические методы (`missingFiles()`, `buildArguments()`, `extractNatives()`) состояния не имеют,
|
||||
но выполняют файловый ввод-вывод и на большой версии могут заметно задержать вызывающий поток.
|
||||
|
||||
## Взаимодействие с другими классами
|
||||
|
||||
`LauncherBackend` собирает `LaunchOptions` из трёх источников: настроек лаунчера, описания
|
||||
выбранной сборки и `AuthResult` от [AuthService](AuthService.md) или
|
||||
[MsaAuthService](MsaAuthService.md). Разобранную версию он получает через
|
||||
`VersionLoader::load()` из [minecraftversion.h](minecraftversion.md).
|
||||
|
||||
Все четыре сигнала бэкенд переправляет в QML: `progress` и `gameFinished` попадают в плашку
|
||||
сообщений главного окна, `output` — в консоль, а `gameStarted` меняет признак `gameRunning`.
|
||||
|
||||
Статический `missingFiles()` вызывается отдельно от запуска — из метода проверки комплектности
|
||||
сборки, результат которого показывает [BuildsDialog](../qml/BuildsDialog.md).
|
||||
|
||||
## Внешнее взаимодействие
|
||||
|
||||
**Дочерний процесс.** Класс запускает java через `QProcess`. Аргументы собираются
|
||||
`buildArguments()`; для профилей Ely.by в них добавляется `-javaagent` с путём к
|
||||
`authlib-injector`. Стандартный вывод процесса читается построчно и отдаётся сигналом `output`,
|
||||
завершение — сигналом `gameFinished`. Направление обмена одностороннее: лаунчер запускает процесс
|
||||
и читает его вывод, ничего не передавая обратно после старта.
|
||||
|
||||
Все сигналы процесса приходят в поток GUI.
|
||||
|
||||
## Пример использования
|
||||
|
||||
```cpp
|
||||
auto *launcher = new GameLauncher(this);
|
||||
connect(launcher, &GameLauncher::progress, this, &Backend::showStatus);
|
||||
connect(launcher, &GameLauncher::gameFinished, this, &Backend::onGameFinished);
|
||||
|
||||
LaunchOptions options;
|
||||
options.gameDir = settings.resolvedGameDir;
|
||||
options.versionId = build.resolvedVersionId;
|
||||
options.playerName = auth.playerName;
|
||||
options.uuid = auth.uuid;
|
||||
options.accessToken = auth.accessToken;
|
||||
options.userType = auth.userType;
|
||||
options.maxMemoryMb = settings.maxMemoryMb;
|
||||
|
||||
QString error;
|
||||
if (!launcher->launch(options, version, &error))
|
||||
emit launchError(error);
|
||||
```
|
||||
Forge собирает слой модулей из classpath и пропускает файлы, имя которых начинается с записи в
|
||||
`-DignoreList`. В профиле Forge там записан `${version_name}.jar`. Официальный лаунчер кладёт в
|
||||
classpath jar самого профиля, а у нас своего jar у профиля нет, и в classpath попадает
|
||||
родительский, например `1.20.1.jar`. Если его имени нет в списке, он становится вторым модулем с
|
||||
классами Minecraft, и игра падает с ошибкой вида `Modules minecraft and _1._20._1 export package
|
||||
...`. Поэтому к `-DignoreList` дописывается имя клиентского jar, если его там ещё нет.
|
||||
|
||||
---
|
||||
|
||||
|
||||
+40
-180
@@ -1,200 +1,60 @@
|
||||
# JavaInstaller
|
||||
|
||||
## Обзор класса
|
||||
`javainstaller.h`, `javainstaller.cpp`
|
||||
|
||||
`JavaInstaller` ставит сборку Java в `<root>/java/<id>`. Как и
|
||||
[ModLoaderInstaller](ModLoaderInstaller.md), он прячет за одним фасадом два разных пути.
|
||||
Ставит одну сборку Java из каталога [JavaRuntimeService](JavaRuntimeService.md) в
|
||||
`<root>/java/<id>/`. Сборки Temurin приходят архивом, сборки Mojang — деревом отдельных файлов,
|
||||
поэтому путей установки два.
|
||||
|
||||
**Temurin** отдаёт один архив: лаунчер качает его, сверяет sha256 и распаковывает. **Mojang**
|
||||
отдаёт манифест с деревом файлов: лаунчер качает файлы по отдельности, как это делает официальный
|
||||
лаунчер.
|
||||
Экземпляр держит [LauncherBackend](LauncherBackend.md). Геттеры прогресса такие же, как у других
|
||||
установщиков: `label`, `stage`, `currentFile`, `bytesDone`, `bytesTotal`, `fraction`, плюс
|
||||
`runtimeId`. Сигналы: `started(label)`, `progressChanged()` (не чаще раза в 100 мс),
|
||||
`finished(runtimeId, javaPath)`, `failed(label, message)`, `canceled(label)`.
|
||||
|
||||
Набор геттеров прогресса повторяет [VersionInstaller](VersionInstaller.md): панель загрузки в
|
||||
интерфейсе читает их одинаково, независимо от того, кто сейчас работает.
|
||||
## install(entry)
|
||||
|
||||
## Место в проекте и зависимости
|
||||
Если установка уже идёт, вызов молча игнорируется. Иначе:
|
||||
|
||||
Подключает [javaruntime.h](javaruntime.md): на вход установщик принимает запись каталога
|
||||
`JavaRuntimeEntry`, а результат записывает через `JavaRuntimeStore`.
|
||||
1. проверяется папка лаунчера;
|
||||
2. **папка `<root>/java/<id>` удаляется целиком**, если она была, и создаётся заново. Повторная
|
||||
установка — это всегда установка с нуля;
|
||||
3. дальше одна из двух веток.
|
||||
|
||||
Экземпляр создаётся и принадлежит [LauncherBackend](LauncherBackend.md); записи каталога приходят
|
||||
от [JavaRuntimeService](JavaRuntimeService.md).
|
||||
### Temurin (`zip`, `tar.gz`)
|
||||
|
||||
Требования сборки: `Qt6::Core` (`QCryptographicHash`, `QSaveFile`, `QProcess`, `QTimer`),
|
||||
`Qt6::CorePrivate` (`QZipReader` для распаковки zip) и `Qt6::Network`.
|
||||
1. Архив качается в `<root>/cache/java/<имя файла>` через `QSaveFile`, sha256 считается на ходу и
|
||||
сверяется с каталогом.
|
||||
2. `zip` распаковывается через `QZipReader` по 24 записи за такт таймера, чтобы окно не
|
||||
подвисало. Права из архива переносятся. Записи, которые ведут за пределы папки сборки,
|
||||
останавливают установку.
|
||||
3. `tar.gz` распаковывается системным `tar -xzf`. Он есть на Linux, macOS и Windows 10 и новее.
|
||||
`QZipReader` tar не умеет, а держать ради этого ещё одну библиотеку незачем.
|
||||
4. Архив удаляется.
|
||||
|
||||
## Иерархия и роль
|
||||
Прогресс при загрузке считается в байтах, при распаковке zip — в записях. Для tar прогресса нет.
|
||||
|
||||
Наследует `QObject`: мета-объектная система, пять сигналов и владение по родителю. Объявлен
|
||||
виртуальный деструктор — класс владеет незавершёнными загрузками, открытым архивом и процессом
|
||||
распаковки.
|
||||
### Mojang
|
||||
|
||||
## Публичные структуры
|
||||
1. Качается `manifest.json` компонента. Его sha1 из каталога сейчас **не проверяется**.
|
||||
2. Из `files` строится дерево: папки создаются сразу, ссылки откладываются, файлы ставятся в
|
||||
очередь.
|
||||
3. До 4 файлов качаются параллельно, sha1 каждого сверяется. При временной ошибке (таймаут,
|
||||
5xx и т. п.) и при несовпадении хеша файл уходит в конец очереди без паузы. После третьей
|
||||
неудачи установка падает. Файлам с `executable: true` ставится бит исполнения.
|
||||
4. Ссылки создаются через `QFile::link`. Если не вышло (на Windows без прав разработчика), файл
|
||||
копируется.
|
||||
|
||||
### JavaFileTask
|
||||
### Конец
|
||||
|
||||
Один файл рантайма Mojang.
|
||||
`JavaRuntimeStore::locateBinary()` находит `bin/java`. На Unix ему ставится бит исполнения.
|
||||
`runtime.json` записывается последним: пока его нет, `JavaRuntimeStore` сборку не видит. Поэтому
|
||||
недокачанная сборка в списке не появится, даже если лаунчер упал на середине.
|
||||
|
||||
| Поле | Тип | По умолчанию | Описание |
|
||||
|------|-----|--------------|----------|
|
||||
| `url` | `QUrl` | — | Откуда качать |
|
||||
| `path` | `QString` | — | Абсолютный путь назначения |
|
||||
| `sha1` | `QString` | — | Контрольная сумма файла |
|
||||
| `size` | `qint64` | `0` | Размер в байтах |
|
||||
| `executable` | `bool` | `false` | Файлу нужно выставить право на исполнение — иначе `bin/java` не запустится |
|
||||
| `attempts` | `int` | `0` | Сколько попыток уже сделано |
|
||||
При ошибке и при отмене папка сборки и скачанный архив удаляются.
|
||||
|
||||
### JavaActiveDownload
|
||||
## cancel()
|
||||
|
||||
Файл в процессе скачивания: задача, сетевой ответ, открытый `QSaveFile`, накапливаемая
|
||||
контрольная сумма и число принятых байт. Файлы пишутся потоком — рантайм весит около двухсот
|
||||
мегабайт.
|
||||
|
||||
Структура `ZipExtraction`, хранящая состояние распаковки, объявлена вперёд и спрятана в
|
||||
`.cpp`: так приватный заголовок `QZipReader` не расходится по проекту вместе с этим заголовком.
|
||||
|
||||
## Публичные методы
|
||||
|
||||
#### explicit JavaInstaller(QObject \*parent = nullptr)
|
||||
|
||||
Создаёт установщик, его `QNetworkAccessManager` и два таймера. Конструктор помечен `explicit`.
|
||||
|
||||
#### bool isRunning() const
|
||||
|
||||
Идёт ли установка прямо сейчас.
|
||||
|
||||
#### QString runtimeId() const
|
||||
|
||||
Идентификатор устанавливаемой сборки.
|
||||
|
||||
#### QString label() const
|
||||
|
||||
Подпись установки для интерфейса.
|
||||
|
||||
#### QString stage() const
|
||||
|
||||
Текущий этап словами: загрузка, проверка, распаковка.
|
||||
|
||||
#### QString currentFile() const
|
||||
|
||||
Файл, который обрабатывается сейчас.
|
||||
|
||||
#### qint64 bytesDone() const
|
||||
|
||||
Сколько байт уже получено. Байты считаются только на загрузке: на распаковке считать нечего, и
|
||||
панель по нулевому итогу сама прячет мегабайты.
|
||||
|
||||
#### qint64 bytesTotal() const
|
||||
|
||||
Ожидаемый общий объём загрузки.
|
||||
|
||||
#### double fraction() const
|
||||
|
||||
Доля выполнения от `0` до `1` либо `-1`, пока итог неизвестен. На этапе распаковки доля считается
|
||||
по числу обработанных записей архива или файлов дерева, а не по байтам.
|
||||
|
||||
#### void install(const JavaRuntimeEntry &entry)
|
||||
|
||||
Ставит сборку по записи каталога. Путь установки выбирается по полю `archive` записи: значения
|
||||
`zip` и `tar.gz` ведут по пути Temurin, значение `mojang` — по пути манифеста.
|
||||
|
||||
По завершении установщик находит исполняемый файл java в распакованном дереве через
|
||||
`JavaRuntimeStore::locateBinary()` и записывает описание сборки рядом с ней.
|
||||
|
||||
Одновременно ставится одна сборка; очереди у этого установщика нет.
|
||||
|
||||
#### void cancel()
|
||||
|
||||
Отменяет установку. Отмена проверяется между файлами и между кусками распаковки, поэтому
|
||||
срабатывает не мгновенно, но без замораживания интерфейса.
|
||||
|
||||
## Сигналы
|
||||
|
||||
#### started(const QString &label)
|
||||
|
||||
Установка началась. Обработчик показывает панель прогресса.
|
||||
|
||||
#### progressChanged()
|
||||
|
||||
Изменились числа прогресса; испускается не чаще, чем позволяет внутренний таймер. Обработчик
|
||||
перечитывает геттеры.
|
||||
|
||||
#### finished(const QString &runtimeId, const QString &javaPath)
|
||||
|
||||
Сборка установлена; во втором параметре — абсолютный путь к исполняемому файлу java.
|
||||
|
||||
Обработчик обновляет каталог: сборка становится помеченной как скачанная, а диалог настроек
|
||||
перечитывает её описание.
|
||||
|
||||
#### failed(const QString &label, const QString &message)
|
||||
|
||||
Установка не удалась; в параметре — текст ошибки для пользователя.
|
||||
|
||||
#### canceled(const QString &label)
|
||||
|
||||
Установка отменена пользователем.
|
||||
|
||||
## Владение и время жизни
|
||||
|
||||
Класс наследует `QObject` и принимает `parent` — родитель его и удалит.
|
||||
`QNetworkAccessManager` и оба таймера создаются в конструкторе с установщиком в роли родителя.
|
||||
|
||||
Владение внутренними ресурсами построено на RAII: скачиваемый архив хранится как
|
||||
`std::unique_ptr<QSaveFile>`, состояние распаковки — как `std::unique_ptr<ZipExtraction>`,
|
||||
активные загрузки — как `std::shared_ptr<JavaActiveDownload>` с собственным `QSaveFile` внутри.
|
||||
Незавершённая запись отменяется вместе с уничтожением объекта, и испорченный файл не попадает на
|
||||
место назначения.
|
||||
|
||||
Процесс `tar`, используемый для распаковки архивов `tar.gz`, создаётся по ходу работы и
|
||||
завершается в деструкторе.
|
||||
|
||||
## Потокобезопасность
|
||||
|
||||
Только поток GUI. Отдельного потока у класса нет намеренно: распаковка zip идёт по кускам по
|
||||
таймеру — держать поток GUI занятым на всю сотню мегабайт нельзя, а заводить поток ради одной
|
||||
операции незачем. Распаковка `tar.gz` отдана внешнему процессу, который работает параллельно сам.
|
||||
|
||||
## Взаимодействие с другими классами
|
||||
|
||||
`LauncherBackend` вызывает `install()` с записью, полученной от
|
||||
[JavaRuntimeService](JavaRuntimeService.md), и переправляет сигналы прогресса в те же свойства,
|
||||
что и остальные установщики. Сигнал `finished` бэкенд переправляет в QML под собственным именем —
|
||||
на него подписан диалог настроек, чтобы обновить строку выбранной сборки, когда та докачается.
|
||||
|
||||
Раскладку папки `<root>/java`, поиск исполняемого файла и запись описания обеспечивает
|
||||
`JavaRuntimeStore` из [javaruntime.h](javaruntime.md).
|
||||
|
||||
## Внешнее взаимодействие
|
||||
|
||||
**Сеть, исходящие запросы.** Загрузка по HTTPS с серверов Adoptium или Mojang. Архив Temurin
|
||||
скачивается одним запросом с проверкой sha256; дерево Mojang — множеством параллельных запросов,
|
||||
каждый с проверкой sha1. Неудачная задача повторяется, счётчик попыток хранится в самой задаче.
|
||||
|
||||
**Дочерний процесс.** Архивы `tar.gz` распаковываются системным `tar` через `QProcess` —
|
||||
собственного распаковщика для этого формата в Qt нет. Обмен односторонний: лаунчер запускает
|
||||
процесс и ждёт его завершения.
|
||||
|
||||
**Файловая система.** После распаковки дерева Mojang применяются символические ссылки из
|
||||
манифеста, а файлам с признаком `executable` выставляется право на исполнение.
|
||||
|
||||
Все сигналы приходят в поток GUI.
|
||||
|
||||
## Пример использования
|
||||
|
||||
```cpp
|
||||
auto *javaInstaller = new JavaInstaller(this);
|
||||
|
||||
connect(javaInstaller, &JavaInstaller::finished, this,
|
||||
[this](const QString &runtimeId, const QString &javaPath) {
|
||||
m_settings.javaRuntime = runtimeId;
|
||||
m_resolvedJavaPath = javaPath;
|
||||
emit javaRuntimeInstalled(runtimeId);
|
||||
});
|
||||
|
||||
const auto entry = javaCatalog->find(runtimeId);
|
||||
if (entry)
|
||||
javaInstaller->install(*entry);
|
||||
```
|
||||
Обрывает загрузки и убивает `tar`. `canceled` приходит, когда вернутся все активные запросы.
|
||||
|
||||
---
|
||||
|
||||
|
||||
+45
-133
@@ -1,155 +1,67 @@
|
||||
# JavaRuntimeService
|
||||
|
||||
## Обзор класса
|
||||
`javaruntimeservice.h`, `javaruntimeservice.cpp`
|
||||
|
||||
`JavaRuntimeService` — каталог сборок Java, которые лаунчер умеет скачать: качает, кэширует в
|
||||
папке лаунчера и отдаёт из кэша, пока тот не устарел. Устроен так же, как
|
||||
[VersionManifestService](VersionManifestService.md).
|
||||
Каталог сборок Java, которые лаунчер может скачать. Устроен как
|
||||
[VersionManifestService](VersionManifestService.md), но кэш хранится сутки и лежит в
|
||||
`<root>/cache/java_runtimes.json`.
|
||||
|
||||
Источников два, и они дополняют друг друга. **Mojang** (категория «Java») — ровно тот рантайм,
|
||||
которым запускает игру официальный лаунчер: версий немного, зато они заведомо совместимы.
|
||||
**Eclipse Temurin** (категории JDK и JRE) — свежие сборки всех мажорных версий, включая те, до
|
||||
которых Mojang ещё не дошёл.
|
||||
Источников два:
|
||||
|
||||
Отдаются только сборки под текущие операционную систему и архитектуру: выбрать заведомо
|
||||
неработающую нечем. Исключение — macOS на Apple Silicon, где под Java 8 и 16 сборок `aarch64` нет
|
||||
вовсе и приходится брать `x64`, который работает через Rosetta.
|
||||
- **Mojang** (в интерфейсе категория «Java») — те же рантаймы, с которыми игру запускает
|
||||
официальный лаунчер. Их немного, зато с игрой они точно совместимы.
|
||||
- **Eclipse Temurin** (JDK и JRE) — свежие сборки всех мажорных версий, в том числе тех, которых у
|
||||
Mojang ещё нет.
|
||||
|
||||
## Место в проекте и зависимости
|
||||
В каталог попадают только сборки под текущую ОС и архитектуру.
|
||||
|
||||
Подключает [javaruntime.h](javaruntime.md): перечисление `JavaRuntimeKind` и структура
|
||||
`JavaRuntimeEntry` приходят оттуда.
|
||||
Экземпляр держит [LauncherBackend](LauncherBackend.md), ставит сборки
|
||||
[JavaInstaller](JavaInstaller.md).
|
||||
|
||||
Экземпляр создаётся и принадлежит [LauncherBackend](LauncherBackend.md). Записями каталога
|
||||
пользуется [JavaInstaller](JavaInstaller.md).
|
||||
## Как обновляется каталог
|
||||
|
||||
Путь к файлу кэша даёт `LauncherPaths::javaCatalogFile()` из [launcherpaths.h](launcherpaths.md).
|
||||
Запросы идут параллельно. Каталог заменяется целиком, когда ответят все.
|
||||
|
||||
Требования сборки: `Qt6::Core` (`QDateTime`, `QHash`, `QSet`) и `Qt6::Network`
|
||||
(`QNetworkAccessManager`).
|
||||
1. `api.adoptium.net/v3/info/available_releases` — какие мажорные версии есть и какие из них LTS.
|
||||
2. Для каждой мажорной версии `assets/feature_releases/<major>/ga` с фильтрами: ОС, архитектура,
|
||||
HotSpot, обычная куча, vendor `eclipse`, три последних релиза. Из каждого берутся бинарники с
|
||||
`image_type` = `jdk` или `jre` в виде `.zip` или `.tar.gz` по `https`.
|
||||
3. `piston-meta.mojang.com/.../java-runtime/.../all.json` — раздел под текущую платформу
|
||||
(`windows-x64`, `mac-os-arm64`, `linux` и т. п.), по одному варианту на компонент. Компонент
|
||||
`minecraft-java-exe` пропускается: это не Java.
|
||||
|
||||
## Иерархия и роль
|
||||
**Нет сборки под aarch64.** У Temurin под ARM нет Java 8 и 16. Если для aarch64 ничего не нашлось,
|
||||
запрос повторяется для `x64`. На Mac с Apple Silicon такая сборка работает через Rosetta. Сейчас
|
||||
повтор делается на любой ОС с aarch64, хотя комментарий в заголовке говорит только про macOS.
|
||||
|
||||
Наследует `QObject`: мета-объектная система, два сигнала и владение по родителю. Виртуальных
|
||||
методов базового класса не переопределяет.
|
||||
Id строк: `temurin-<jdk|jre>-<версия>-<арх>` и `mojang-<компонент>`, пропущенные через
|
||||
`JavaRuntimeStore::sanitizeId()`.
|
||||
|
||||
## Псевдонимы типов
|
||||
Версия Temurin 8 показывается как `8u<security>`, остальные — как `openjdk_version` без `-LTS`.
|
||||
Мажорная версия у Mojang берётся из имени (`8u…` → 8, иначе первое число).
|
||||
|
||||
`JavaRuntimeService::Callback` — `std::function<void(bool ok, const QString &warning)>`. Как и в
|
||||
остальных каталогах лаунчера, `ok == true` с непустым `warning` означает данные из устаревшего
|
||||
кэша.
|
||||
У сборок Mojang `size` равен 0: в каталоге указан только размер манифеста, а не рантайма.
|
||||
|
||||
## Публичные методы
|
||||
Если какие-то запросы упали, а другие ответили, каталог всё равно обновляется, а предупреждения
|
||||
склеиваются через `; `. Если не пришло ничего, но старые данные есть, колбэк получает `ok = true` и
|
||||
предупреждение.
|
||||
|
||||
#### explicit JavaRuntimeService(QObject \*parent = nullptr)
|
||||
Кэш помечается платформой (`mojangPlatform()`). Кэш с другой платформы не читается. Это возможно,
|
||||
например, если папку лаунчера перенесли с другой машины.
|
||||
|
||||
Создаёт сервис и его `QNetworkAccessManager`. Кэш читается лениво. Конструктор помечен `explicit`.
|
||||
## Методы
|
||||
|
||||
#### void ensureLoaded(Callback callback, bool forceRefresh = false)
|
||||
- `ensureLoaded(callback, forceRefresh)` — как у манифеста версий.
|
||||
- `entries(kind)` — строки одного вида, отсортированы по мажорной версии, потом по дате выхода,
|
||||
новые сверху.
|
||||
- `find(id)`.
|
||||
- `bestFor(major)` — что скачать, если игре нужна Java `major`, а подходящей нет. Выбирается
|
||||
наименьшая мажорная версия не ниже `major`. При равенстве порядок такой: Temurin JDK, потом
|
||||
Mojang, потом Temurin JRE. JDK идёт первым, потому что установщикам Forge и NeoForge иногда нужны
|
||||
инструменты из полного комплекта. Если и здесь равенство, берётся более свежая.
|
||||
- `isRefreshing()`, `hasData()`.
|
||||
|
||||
Единственная точка входа. Свежий кэш означает отложенный вызов колбэка без сети; иначе запускается
|
||||
обновление. Параметр `forceRefresh` обходит проверку свежести — так работает кнопка обновления
|
||||
каталога.
|
||||
|
||||
Одно обновление складывается из нескольких запросов сразу к обоим источникам. Ответы приходят
|
||||
вразнобой, поэтому они накапливаются, и каталог подменяется целиком только когда пришли все.
|
||||
|
||||
#### QList<JavaRuntimeEntry> entries(JavaRuntimeKind kind) const
|
||||
|
||||
Сборки одной категории: `Mojang`, `Jdk` или `Jre`. Новые версии идут первыми.
|
||||
|
||||
#### std::optional<JavaRuntimeEntry> find(const QString &id) const
|
||||
|
||||
Запись по идентификатору сборки; `std::nullopt`, если такой нет. Поиск идёт по внутреннему
|
||||
указателю.
|
||||
|
||||
#### std::optional<JavaRuntimeEntry> bestFor(int major) const
|
||||
|
||||
Что скачать, если для версии игры нужна Java указанной мажорной версии, а подходящей в системе
|
||||
нет.
|
||||
|
||||
Правила выбора: точное совпадение мажорной версии предпочтительнее более новой, а JDK
|
||||
предпочтительнее JRE — установщики Forge и NeoForge иногда требуют инструментов из полного
|
||||
комплекта. `std::nullopt` означает, что предложить нечего.
|
||||
|
||||
#### bool isRefreshing() const
|
||||
|
||||
Идут ли сейчас запросы. Признак остаётся истинным, пока не завершится последний из них.
|
||||
|
||||
#### bool hasData() const
|
||||
|
||||
Есть ли в каталоге хоть что-то — из сети или из кэша.
|
||||
|
||||
## Сигналы
|
||||
|
||||
#### catalogChanged()
|
||||
|
||||
Каталог заменён новыми данными. Испускается один раз за обновление, когда пришли ответы от всех
|
||||
источников, а не по каждому из них.
|
||||
|
||||
Обработчик перечитывает `entries()` для нужных категорий и обновляет интерфейс.
|
||||
|
||||
#### refreshingChanged()
|
||||
|
||||
Изменился признак обновления. Обработчик показывает или убирает индикатор загрузки.
|
||||
|
||||
## Владение и время жизни
|
||||
|
||||
Класс наследует `QObject` и принимает `parent` — родитель его и удалит.
|
||||
`QNetworkAccessManager` создаётся в конструкторе с сервисом в роли родителя.
|
||||
|
||||
Отложенные колбэки хранятся до завершения текущего обновления. Уничтожение сервиса с
|
||||
незавершёнными запросами обрывает их вместе с менеджером сети, и колбэки не вызываются.
|
||||
|
||||
## Потокобезопасность
|
||||
|
||||
Только поток GUI. Чтение и запись кэша выполняются синхронно в вызывающем потоке.
|
||||
|
||||
## Взаимодействие с другими классами
|
||||
|
||||
`LauncherBackend` вызывает `ensureLoaded()` при открытии окна выбора Java и по кнопке обновления,
|
||||
а сигналы переправляет в свойства, которые читает QML. Перед отдачей в интерфейс он сводит записи
|
||||
каталога с уже установленными сборками из `JavaRuntimeStore` — окно выбора показывает статус, не
|
||||
считая ничего само.
|
||||
|
||||
[JavaInstaller](JavaInstaller.md) получает запись каталога и по ней скачивает и распаковывает
|
||||
сборку. `bestFor()` используется, когда для запуска не хватает Java и лаунчер должен сам
|
||||
предложить, что поставить.
|
||||
|
||||
Требования версий игры к Java берутся из пространства имён `JavaRequirement` в
|
||||
[javaruntime.h](javaruntime.md).
|
||||
|
||||
## Внешнее взаимодействие
|
||||
|
||||
**Сеть, исходящие запросы.** Класс обращается к двум внешним каталогам: API Adoptium (сборки
|
||||
Temurin) и списку рантаймов Mojang. Формат обоих — JSON поверх HTTPS, разбор разделён на две
|
||||
функции.
|
||||
|
||||
Запросов на одно обновление несколько: список сборок Temurin запрашивается по мажорным версиям,
|
||||
и на macOS с Apple Silicon неудачный запрос сборки `aarch64` может быть переспрошен для `x64` —
|
||||
это и есть тот самый случай Java 8 и 16.
|
||||
|
||||
Общий счётчик незавершённых запросов сводит их воедино: пока он не обнулился, каталог не
|
||||
подменяется, а предупреждения от отдельных источников накапливаются.
|
||||
|
||||
Все сигналы и колбэки приходят в поток GUI.
|
||||
|
||||
## Пример использования
|
||||
|
||||
```cpp
|
||||
auto *javaCatalog = new JavaRuntimeService(this);
|
||||
connect(javaCatalog, &JavaRuntimeService::catalogChanged, this, &Backend::rebuildJavaCatalog);
|
||||
|
||||
javaCatalog->ensureLoaded([this, javaCatalog](bool ok, const QString &warning) {
|
||||
if (!ok) {
|
||||
emit javaCatalogError(warning);
|
||||
return;
|
||||
}
|
||||
const auto best = javaCatalog->bestFor(version.javaMajor);
|
||||
if (best)
|
||||
emit suggestJavaInstall(best->id, best->version);
|
||||
});
|
||||
```
|
||||
Сигналы: `catalogChanged()`, `refreshingChanged()`.
|
||||
|
||||
---
|
||||
|
||||
|
||||
+310
-488
@@ -1,531 +1,353 @@
|
||||
# LauncherBackend
|
||||
|
||||
## Обзор класса
|
||||
`launcherbackend.h`, `launcherbackend.cpp`
|
||||
|
||||
`LauncherBackend` — единственный класс проекта, видимый из QML, и центр всего приложения. Интерфейс
|
||||
лаунчера ничего не знает ни о сети, ни о файлах, ни о процессах: он читает свойства этого класса,
|
||||
вызывает его методы и слушает его сигналы.
|
||||
Через этот класс QML работает со всем остальным кодом. Интерфейс читает его свойства, вызывает
|
||||
методы и слушает сигналы, а к сервисам напрямую не обращается.
|
||||
|
||||
Сам бэкенд почти ничего не делает руками. Он владеет двенадцатью сервисами — авторизация, запуск
|
||||
игры, каталоги версий, модлоадеров, Java и сезонных сборок, три установщика, загрузчик паков и
|
||||
переключатель сборок — и отвечает за то, чтобы они работали в правильном порядке. Кроме того, он
|
||||
хранит состояние лаунчера: профили игрока, пользовательские сборки и настройки запуска, которые
|
||||
читает и пишет в файлы папки лаунчера.
|
||||
Сам бэкенд почти ничего не скачивает и не запускает. Он хранит профили, сборки и настройки,
|
||||
создаёт двенадцать сервисов и следит, чтобы они не мешали друг другу: нельзя ставить Java во время
|
||||
смены сборки, удалять версию, пока она качается, или запускать игру посреди установки модлоадера.
|
||||
Ещё он сводит каталоги из сети с тем, что лежит на диске, чтобы окнам не пришлось этого делать.
|
||||
|
||||
Ещё одна его задача — приводить данные к виду, удобному QML. Каталоги отдаются в интерфейс уже
|
||||
сведёнными с локальным состоянием: строка версии знает, скачана ли она, строка сезонной сборки —
|
||||
установлена ли и не устарела ли. Окно показывает статус, не считая ничего само.
|
||||
Экземпляр один, его объявляет [Main.qml](../qml/Main.md). Во вложенные окна он передаётся через
|
||||
`required property var backend`. Тип зарегистрирован через `QML_ELEMENT` в модуле `BiLauncher`;
|
||||
синглтоном он не сделан.
|
||||
|
||||
## Место в проекте и зависимости
|
||||
## Сервисы
|
||||
|
||||
Единственный экземпляр создаётся декларативно в [Main.qml](../qml/Main.md); в `main.cpp` он не
|
||||
упоминается.
|
||||
Все создаются в конструкторе с бэкендом в роли родителя.
|
||||
|
||||
Владеет двенадцатью сервисами, каждому из которых посвящена своя страница:
|
||||
|
||||
| Поле | Класс | Роль |
|
||||
|------|-------|------|
|
||||
| `m_auth` | [AuthService](AuthService.md) | вход через Ely.by и офлайн |
|
||||
| Поле | Класс | Зачем |
|
||||
|------|-------|-------|
|
||||
| `m_auth` | [AuthService](AuthService.md) | Ely.by, офлайн, загрузка authlib-injector |
|
||||
| `m_msa` | [MsaAuthService](MsaAuthService.md) | вход через Microsoft |
|
||||
| `m_launcher` | [GameLauncher](GameLauncher.md) | запуск JVM с игрой |
|
||||
| `m_launcher` | [GameLauncher](GameLauncher.md) | запуск JVM |
|
||||
| `m_manifest` | [VersionManifestService](VersionManifestService.md) | каталог версий Mojang |
|
||||
| `m_installer` | [VersionInstaller](VersionInstaller.md) | установка версии игры |
|
||||
| `m_loaderMeta` | [ModLoaderVersionService](ModLoaderVersionService.md) | списки версий модлоадеров |
|
||||
| `m_loaderMeta` | [ModLoaderVersionService](ModLoaderVersionService.md) | версии модлоадеров |
|
||||
| `m_loaderInstaller` | [ModLoaderInstaller](ModLoaderInstaller.md) | установка модлоадера |
|
||||
| `m_switcher` | [BuildSwitcher](BuildSwitcher.md) | смена активной сборки |
|
||||
| `m_javaMeta` | [JavaRuntimeService](JavaRuntimeService.md) | каталог сборок Java |
|
||||
| `m_switcher` | [BuildSwitcher](BuildSwitcher.md) | смена активной сборки, раскатка пака |
|
||||
| `m_javaMeta` | [JavaRuntimeService](JavaRuntimeService.md) | каталог Java |
|
||||
| `m_javaInstaller` | [JavaInstaller](JavaInstaller.md) | установка Java |
|
||||
| `m_seasonalMeta` | [SeasonalBuildService](SeasonalBuildService.md) | каталог сезонных сборок |
|
||||
| `m_packDownloader` | [SeasonalPackDownloader](SeasonalPackDownloader.md) | загрузка архива сезонной сборки |
|
||||
|
||||
Пути ко всем файлам состояния берутся из [launcherpaths.h](launcherpaths.md), описания версий — из
|
||||
[minecraftversion.h](minecraftversion.md), словарь модлоадеров — из [modloader.h](modloader.md),
|
||||
поиск системной Java — из [javalocator.md](javalocator.md).
|
||||
`VersionInstaller` получает указатель на сервис манифеста, `ModLoaderInstaller` — на сервис версий
|
||||
модлоадеров и на установщик версий. Владельцем этих объектов остаётся бэкенд.
|
||||
|
||||
Требования сборки: `Qt6::Core`, `Qt6::Gui`, `Qt6::Network`, `Qt6::CorePrivate` (опосредованно) и
|
||||
`Qt6::Quick` — класс зарегистрирован в QML-модуле `Minecraft_launcher`, объявленном в
|
||||
`CMakeLists.txt`.
|
||||
## Что происходит при старте
|
||||
|
||||
## Иерархия и роль
|
||||
Конструктор делает следующее, порядок важен:
|
||||
|
||||
Наследует `QObject`: мета-объектная система, свойства, сигналы и владение по родителю. Объявлен
|
||||
виртуальный деструктор. Виртуальных методов базового класса не переопределяет.
|
||||
1. `LauncherPaths::ensureRootExists()` создаёт папку лаунчера и проверяет, что в неё можно писать.
|
||||
2. `loadSettings()` заполняет настройки значениями по умолчанию и накладывает сверху
|
||||
`settings.json`.
|
||||
3. `migrateLegacyBuildsFile()` переносит старый `versions.json` в `customBuilds.json`. Это должно
|
||||
случиться до следующего шага, иначе поверх старых данных появится пустой файл.
|
||||
4. `loadData()` читает профили и сборки и сортирует их по `id`.
|
||||
5. `ensureDataFiles()` создаёт недостающие файлы.
|
||||
6. Если `BuildSwitcher` нашёл журнал оборванного переключения, бэкенд сразу продолжает его через
|
||||
`resumeInterrupted()`. Пользователю ничего не предлагается. Если начать переключение заново,
|
||||
оно сначала заархивирует полуразобранную `.minecraft` поверх целого архива прежней сборки.
|
||||
7. Подписка на сигналы сервисов.
|
||||
|
||||
## Свойства Q_PROPERTY
|
||||
Все проблемы, найденные по пути (битый JSON, папка без права записи, пропущенные записи сборок),
|
||||
копятся в `m_storageIssues` и уходят одним `launchError` через `QTimer::singleShot(0)`: к этому
|
||||
моменту QML уже подписан на сигнал.
|
||||
|
||||
### Профили и сборки
|
||||
## Файлы на диске
|
||||
|
||||
| Свойство | Тип | READ | WRITE | NOTIFY | Описание |
|
||||
|----------|-----|------|-------|--------|----------|
|
||||
| `profileNames` | `QStringList` | `profileNames` | — | `profilesChanged` | Имена профилей игрока в порядке добавления. Только для чтения; модель выпадающего списка профилей |
|
||||
| `customBuildNames` | `QStringList` | `customBuildNames` | — | `customBuildsChanged` | Имена пользовательских сборок. Только для чтения; модель списка в окне сборок |
|
||||
| `activeBuildIndex` | `int` | `activeBuildIndex` | `setActiveBuildIndex` | `activeBuildChanged` | Сборка, которую запускает кнопка игры. Хранится по идентификатору сборки, а не по индексу: удаление соседней записи не должно переназначать активную. Запись в свойство запускает смену сборки |
|
||||
| `activeBuildName` | `QString` | `activeBuildName` | — | `activeBuildChanged` | Имя активной сборки для подписи на кнопке. Только для чтения |
|
||||
| `installedVersions` | `QStringList` | `installedVersions` | — | `installedVersionsChanged` | Версии, реально установленные в папке игры. Только для чтения |
|
||||
Все пути берутся из [launcherpaths.h](launcherpaths.md).
|
||||
|
||||
### Занятость и смена сборки
|
||||
- `settings.json` — объект с настройками;
|
||||
- `profiles.json` — массив профилей;
|
||||
- `customBuilds.json` — массив сборок.
|
||||
|
||||
| Свойство | Тип | 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 показать нечем, и интерфейс не должен предлагать этот путь |
|
||||
Если файла нет, это первый запуск. Если он не читается как JSON или в нём не тот тип (объект вместо
|
||||
массива и наоборот), файл переименовывается в `<имя>.bak`, а на его месте создаётся пустой.
|
||||
Данные из `.bak` можно потом достать руками.
|
||||
|
||||
### Каталог версий
|
||||
Перед каждой записью снова вызывается `ensureRootExists()`: папку могли удалить, пока лаунчер
|
||||
работал, и без этой проверки сохранения пропадали бы без ошибки. Пишется всё через обычный `QFile`,
|
||||
кроме `builds/<id>/season.json`, для которого используется `QSaveFile`.
|
||||
|
||||
| Свойство | Тип | 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`.
|
||||
Пароли Ely.by и токены лежат в `profiles.json` открытым текстом.
|
||||
|
||||
### Настройки
|
||||
|
||||
#### QVariantMap settings() const
|
||||
|
||||
Настройки запуска одной картой: `gameDir`, `javaPath`, `javaRuntime`, `minMemoryMb`,
|
||||
`maxMemoryMb`, `jvmArgs`, `windowWidth`, `windowHeight`, `fullscreen`, `language` и вычисленный
|
||||
`resolvedGameDir`.
|
||||
|
||||
#### void updateSettings(const QVariantMap &values)
|
||||
|
||||
Записывает настройки и сохраняет их на диск. Испускает `settingsChanged`.
|
||||
|
||||
Смену `language` после записи пробрасывает в [Localization](Localization.md) — порядок
|
||||
«сохранили → переключили» гарантирует, что выбранный язык переживёт падение сразу после
|
||||
переключения. Сам `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](../qml/LoaderRow.md).
|
||||
|
||||
### События установки
|
||||
|
||||
#### 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](VersionInstaller.md) принимает в
|
||||
конструктор сервис манифеста, а [ModLoaderInstaller](ModLoaderInstaller.md) — сервис версий
|
||||
лоадеров и установщик версий; эти указатели не переходят во владение принимающей стороны.
|
||||
|
||||
Кэши каталогов помечены `mutable` и пересобираются лениво из константных геттеров: QML читает
|
||||
свойства помногу раз за кадр, пока открыт список, и пересборка по каждому чтению обошлась бы
|
||||
дорого.
|
||||
|
||||
## Потокобезопасность
|
||||
|
||||
Только поток GUI. Единственная работа в другом потоке — файловые операции над содержимым
|
||||
`.minecraft`, и она полностью инкапсулирована в [BuildSwitcher](BuildSwitcher.md): сам бэкенд
|
||||
общается с ним обычными сигналами и слотами.
|
||||
|
||||
## Доступ из QML
|
||||
|
||||
Класс зарегистрирован макросом `QML_ELEMENT` в модуле `Minecraft_launcher`, объявленном через
|
||||
`qt_add_qml_module` в `CMakeLists.txt`. Имя типа в QML совпадает с именем класса —
|
||||
`LauncherBackend`.
|
||||
|
||||
Из QML доступны все 26 свойств, все 42 метода `Q_INVOKABLE` и все сигналы, перечисленные выше.
|
||||
Синглтоном тип не объявлен: экземпляр создаётся декларативно в [Main.qml](../qml/Main.md) и
|
||||
передаётся во вложенные диалоги через их свойство `backend`. Все диалоги проекта объявляют его как
|
||||
`required property var backend`.
|
||||
|
||||
Объект, созданный из QML, принадлежит движку QML — удалять его из C++ нельзя.
|
||||
|
||||
## Взаимодействие с другими классами
|
||||
|
||||
**Вниз, к сервисам.** Бэкенд подписан на сигналы всех двенадцати сервисов и сводит их к своим
|
||||
свойствам. Четыре разных источника загрузки — установщик версий, установщик модлоадеров,
|
||||
установщик Java и загрузчик паков — отображаются в одну группу свойств `download*`, поэтому панель
|
||||
в интерфейсе не различает, кто работает; какой из источников показывать, бэкенд решает сам.
|
||||
|
||||
**Вверх, к QML.** Интерфейс не обращается ни к одному сервису напрямую. Каталоги отдаются уже
|
||||
сведёнными с локальным состоянием: строка версии знает про `installed`, строка сезонной сборки —
|
||||
про установленную ревизию и доступное обновление.
|
||||
|
||||
**Состояние на диске.** Профили, сборки и настройки читаются при создании и пишутся при каждом
|
||||
изменении. Отсутствие файла — норма (первый запуск), а повреждённое содержимое отводится в файл с
|
||||
расширением `.bak`, чтобы рабочий файл создался заново. Проблемы хранилища, замеченные на старте,
|
||||
накапливаются и показываются одним сообщением, когда интерфейс уже подключился к сигналам.
|
||||
|
||||
Отдельно предусмотрена миграция: файл `versions.json` от прежней схемы именования переносится в
|
||||
`customBuilds.json` при первом запуске после переименования.
|
||||
|
||||
**Восстановление после сбоя.** При старте бэкенд спрашивает у переключателя сборок, не было ли
|
||||
прервано переключение, и предлагает доиграть его.
|
||||
|
||||
## Внешнее взаимодействие
|
||||
|
||||
Собственных сетевых обращений и дочерних процессов у класса нет: всё внешнее взаимодействие
|
||||
делегировано сервисам — сеть у каталогов, установщиков и служб авторизации, процессы у
|
||||
[GameLauncher](GameLauncher.md), [ModLoaderInstaller](ModLoaderInstaller.md) и
|
||||
[JavaInstaller](JavaInstaller.md), файловые операции над `.minecraft` у
|
||||
[BuildSwitcher](BuildSwitcher.md).
|
||||
|
||||
Единственное прямое обращение к системе — открытие папки игры в файловом менеджере методами
|
||||
`openMinecraftFolder()` и `openGameFolder()`.
|
||||
|
||||
## Пример использования
|
||||
|
||||
Класс предназначен для создания из QML, а не из C++:
|
||||
| Ключ | По умолчанию | Что это |
|
||||
|------|--------------|---------|
|
||||
| `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`.
|
||||
|
||||
Чьи числа показывать, решается так. Пак качается, только когда остальные уже закончили. Java не
|
||||
качается одновременно с версией или модлоадером. Во время установки модлоадера доля берётся у
|
||||
`VersionInstaller`, если он сейчас качает файлы версии, потому что его оценка точнее.
|
||||
|
||||
`cancelDownload()` останавливает первое, что работает, в порядке: модлоадер (он сам остановит
|
||||
вложенную установку версии), версия, Java, пак.
|
||||
|
||||
`busy` равно `true`, пока идёт вход или запуск, работает любой установщик, идёт смена сборки или
|
||||
качается пак. На это время в интерфейсе гаснет кнопка запуска.
|
||||
|
||||
## Папки
|
||||
|
||||
- `openMinecraftFolder()` открывает `<gameDir>/mods` (создаёт, если её нет).
|
||||
- `openGameFolder()` открывает саму папку игры.
|
||||
|
||||
## Потоки
|
||||
|
||||
Весь класс работает в GUI-потоке. В отдельном потоке выполняются только файловые операции
|
||||
`BuildSwitcher`, и этот поток скрыт внутри него.
|
||||
|
||||
## Пример
|
||||
|
||||
```qml
|
||||
import QtQuick
|
||||
import Minecraft_launcher
|
||||
import BiLauncher
|
||||
|
||||
Window {
|
||||
id: window
|
||||
visible: true
|
||||
|
||||
LauncherBackend {
|
||||
id: backend
|
||||
|
||||
onLaunchError: (message) => console.warn(message)
|
||||
onTwoFactorRequired: (profileName) => twoFactorDialog.open()
|
||||
onMicrosoftLoginUrlReady: (url) => window.openMicrosoftLogin(url)
|
||||
}
|
||||
|
||||
Button {
|
||||
|
||||
+103
-94
@@ -1,17 +1,15 @@
|
||||
# Localization
|
||||
|
||||
`localization.h` / `localization.cpp`
|
||||
`localization.h`, `localization.cpp`
|
||||
|
||||
Единственный источник всех текстов интерфейса. Каталог — [`i18n/translations.json`](#каталог),
|
||||
он лежит в ресурсах и правится руками. Штатных `.ts`/`.qm` в проекте нет намеренно: `lupdate`,
|
||||
`lrelease` и Linguist не нужны, а все переводы видны в одном файле.
|
||||
Все тексты интерфейса на всех языках лежат в одном файле, `i18n/translations.json`, и читаются
|
||||
через этот класс. Стандартные `.ts`/`.qm`, `lupdate`, `lrelease` и Linguist в проекте не
|
||||
используются. Язык переключается без перезапуска.
|
||||
|
||||
Язык переключается на лету: при смене все привязки QML перевычисляются, перезапуск не требуется.
|
||||
## Из QML
|
||||
|
||||
## Доступ из QML
|
||||
|
||||
Синглтон зарегистрирован декларативно (`QML_NAMED_ELEMENT(Loc)` + `QML_SINGLETON`) и доступен
|
||||
в любом файле модуля без импорта.
|
||||
Синглтон `Loc` (`QML_NAMED_ELEMENT(Loc)` + `QML_SINGLETON`) доступен в любом файле модуля без
|
||||
импорта.
|
||||
|
||||
```qml
|
||||
Text { text: Loc.t.settings.title }
|
||||
@@ -19,31 +17,22 @@ Text { text: Loc.t.java.progress.downloading.arg(label) }
|
||||
DarkCombo { model: Loc.t.profile.authTypes }
|
||||
```
|
||||
|
||||
Точечный путь `Loc.t.a.b.c` — это обращение к вложенным объектам дерева, которое строится из
|
||||
плоских ключей каталога разбиением по точке.
|
||||
`Loc.t` — дерево JS-объектов, собранное из плоских ключей каталога разбиением по точке.
|
||||
|
||||
**Почему свойство, а не метод.** Вызов `Q_INVOKABLE` не регистрирует зависимость привязки, и
|
||||
`text: Loc.t("ключ")` никогда бы не обновился при смене языка. Чтение `Q_PROPERTY` с сигналом
|
||||
`NOTIFY` зависимость регистрирует: `languageChanged` перевычисляет все привязки, которые читали
|
||||
`Loc.t`. Сегменты после `t` — обычные обращения к членам JS-объекта, отслеживать их не нужно,
|
||||
потому что при смене языка дерево заменяется целиком.
|
||||
Почему `t` сделано свойством, а не методом `Loc.t("ключ")`. Вызов `Q_INVOKABLE` не регистрирует
|
||||
зависимость привязки, и текст не обновился бы при смене языка. Чтение `Q_PROPERTY` с `NOTIFY`
|
||||
регистрирует, поэтому `languageChanged` перевычисляет все привязки, где читали `Loc.t`. Дальнейшие
|
||||
`.settings.title` — обычные обращения к полям JS-объекта. Следить за ними не нужно, потому что при
|
||||
смене языка дерево заменяется целиком.
|
||||
|
||||
Тип свойства — `QJSValue`, а не `QVariantMap`: `QVariantMap` пересобирался бы в новый JS-объект
|
||||
при каждом чтении, а привязок в проекте полторы сотни. `QJSValue` строится один раз на смену
|
||||
языка.
|
||||
Тип свойства — `QJSValue`. С `QVariantMap` движок собирал бы новый JS-объект при каждом чтении, а
|
||||
привязок в проекте около полутора сотен. `QJSValue` строится один раз на каждую смену языка.
|
||||
|
||||
Из тела JS-функции `Loc.t` читается так же — это просто чтение свойства:
|
||||
Кроме `t` есть `language` (что выбрано: `system`, `ru`, `en`), `resolvedLanguage` (во что это
|
||||
превратилось, всегда `ru` или `en`) и `availableLanguages()`. Последний сейчас не используется:
|
||||
список языков в настройках [Main.qml](../qml/Main.md) задан руками (`stLanguage`).
|
||||
|
||||
```qml
|
||||
onLaunched: (profileName, buildName) =>
|
||||
window.showToast(Loc.t.launch.status.started.arg(profileName).arg(buildName), "#4b7a1f")
|
||||
```
|
||||
|
||||
## Доступ из C++
|
||||
|
||||
Свободные функции, а не методы: их вызывают и из namespace-обёрток
|
||||
([LauncherPaths](launcherpaths.md), [JavaLocator](javalocator.md),
|
||||
[ZlibReference](zlibreference.md)), где никакого `QObject` нет.
|
||||
## Из C++
|
||||
|
||||
```cpp
|
||||
#include "localization.h"
|
||||
@@ -53,29 +42,34 @@ emit launchProgress(Loc::text("java.progress.downloading").arg(label));
|
||||
const QStringList kinds = Loc::list("profile.authTypes");
|
||||
```
|
||||
|
||||
Ключа нет — возвращается сам ключ, а в отладочной сборке ещё и `qWarning`: строка вида
|
||||
`launch.error.noProfile` в интерфейсе сразу бросается в глаза.
|
||||
Это свободные функции, потому что их вызывают и из пространств имён без `QObject`
|
||||
([launcherpaths.h](launcherpaths.md), [javalocator.h](javalocator.md),
|
||||
[zlibreference.h](zlibreference.md)).
|
||||
|
||||
Если ключа нет, возвращается сам ключ, а в отладочной сборке ещё пишется `qWarning`. В интерфейсе
|
||||
такую строку легко заметить.
|
||||
|
||||
### Потоки
|
||||
|
||||
`Loc::text()` и `Loc::list()` можно звать из любого потока. Каталог заполняется ровно один раз
|
||||
в `load()`, который отрабатывает в `main()` до того, как [BuildSwitcher](BuildSwitcher.md)
|
||||
создаст свой поток; дальше он только читается, а копирование `QString` из хэша безопасно само по
|
||||
себе. Единственное, что меняется на ходу, — индекс текущего языка, и он `QAtomicInt`. В худшем
|
||||
случае сообщение, которое собиралось в момент переключения, уедет на прежнем языке.
|
||||
`Loc::text()` и `Loc::list()` можно вызывать из любого потока. Каталог заполняется один раз в
|
||||
`load()`, до того как [BuildSwitcher](BuildSwitcher.md) запустит свой поток, и дальше только
|
||||
читается. На ходу меняется лишь индекс текущего языка, а он `QAtomicInt`. В худшем случае строка,
|
||||
собранная в момент переключения, окажется на прежнем языке.
|
||||
|
||||
`setLanguage()` и рассылка `languageChanged` — только поток GUI; это проверяется `Q_ASSERT`.
|
||||
`setLanguage()` вызывается только из GUI-потока, это проверяет `Q_ASSERT`.
|
||||
|
||||
## Каталог
|
||||
|
||||
`i18n/translations.json` попадает в ресурсы через список `RESOURCES` в `qt_add_qml_module`,
|
||||
поэтому читается по пути `:/qt/qml/Minecraft_launcher/i18n/translations.json`.
|
||||
Файл попадает в ресурсы через `RESOURCES` в `qt_add_qml_module` и читается из
|
||||
`:/qt/qml/BiLauncher/i18n/translations.json`.
|
||||
|
||||
```json
|
||||
{
|
||||
"_meta": {
|
||||
"languages": ["ru", "en"],
|
||||
"displayNames": { "ru": "Русский", "en": "English" }
|
||||
"displayNames": { "ru": "Русский", "en": "English" },
|
||||
"_readme": ["..."],
|
||||
"_glossary": { "сборка": "build", "...": "..." }
|
||||
},
|
||||
"strings": {
|
||||
"settings.title": { "ru": "Настройки запуска", "en": "Launch settings" },
|
||||
@@ -88,77 +82,92 @@ const QStringList kinds = Loc::list("profile.authTypes");
|
||||
}
|
||||
```
|
||||
|
||||
Ключи плоские, языки рядом: забытый перевод виден на соседней строке, правка пары — один участок
|
||||
файла, а третий язык добавляется колонкой без правок загрузчика.
|
||||
Ключи плоские, языки записаны рядом. Так сразу видно, если перевод забыли, а новый язык
|
||||
добавляется ещё одним полем без правок загрузчика. `_readme` и `_glossary` — подсказки для тех, кто
|
||||
правит файл. Загрузчик их не читает.
|
||||
|
||||
**Имя ключа — `<домен>.<вид>.<имя>`**, сегменты в lowerCamelCase.
|
||||
Значение — либо строка, либо список строк (для моделей выпадающих списков). Тип определяется по
|
||||
первому языку.
|
||||
|
||||
- **Домен** — область, а не имя файла: `app`, `common`, `settings`, `profile`, `build`,
|
||||
`seasonal`, `version`, `java`, `loader`, `auth` (с `auth.ely.*`, `auth.msa.*`), `launch`,
|
||||
`game`, `switch`, `storage`, `zlib`.
|
||||
- **Вид** — `title`, `label`, `button`, `placeholder`, `hint`, `header`; для сообщений `error`,
|
||||
`progress`, `status`, `warning`.
|
||||
- **Имя** описывает условие, а не формулировку, чтобы перевод не переименовывал ключ:
|
||||
`gameRunning`, `downloadFailed`, `checksumMismatch`.
|
||||
### Имена ключей
|
||||
|
||||
Литерал, который нужен в двух и более файлах, живёт в `common.*`.
|
||||
`<домен>.<вид>.<имя>`, сегменты в lowerCamelCase.
|
||||
|
||||
Подстановки `%1`/`%2` сохраняются дословно: их одинаково понимают `QString::arg()` и
|
||||
QML-овский `String.arg()`. **Набор `%N` в `ru` и `en` обязан совпадать** — порядок слов может
|
||||
отличаться, состав нет. Множественного числа формат не поддерживает; в коде сейчас нет ни одной
|
||||
строки, которой оно требуется.
|
||||
- **Домен** — область интерфейса, а не файл: `app`, `common`, `settings`, `profile`, `build`,
|
||||
`seasonal`, `version`, `java`, `loader`, `auth` (`auth.ely.*`, `auth.msa.*`), `launch`, `game`,
|
||||
`switch`, `storage`, `zlib`.
|
||||
- **Вид**: `title`, `label`, `button`, `placeholder`, `hint`, `header`, `tooltip`; для сообщений
|
||||
`error`, `progress`, `status`, `warning`.
|
||||
- **Имя** описывает ситуацию, а не формулировку: `gameRunning`, `downloadFailed`,
|
||||
`checksumMismatch`. Тогда при правке перевода ключ не придётся переименовывать.
|
||||
|
||||
## Как добавить строку
|
||||
Строка, которая нужна в двух и более местах, кладётся в `common.*`.
|
||||
|
||||
1. Добавить запись в `strings` файла `i18n/translations.json` — сразу с `ru` и `en`.
|
||||
2. Сослаться на неё: `Loc.t.домен.вид.имя` в QML или `Loc::text("домен.вид.имя")` в C++.
|
||||
3. Прогнать `python3 tools/check_translations.py`.
|
||||
`%1`, `%2` пишутся как есть: их одинаково понимают `QString::arg()` и `String.arg()` в QML. Набор
|
||||
`%N` в `ru` и `en` должен совпадать, порядок может быть разным. Множественного числа формат не
|
||||
поддерживает, и пока ни одна строка его не требует.
|
||||
|
||||
## Как добавить язык
|
||||
## Добавить строку
|
||||
|
||||
1. Дописать код в `_meta.languages` и название в `_meta.displayNames`.
|
||||
2. Добавить колонку с этим кодом в каждую запись `strings`.
|
||||
3. Добавить код в `stLanguage.codes` и пункт в модель комбобокса в [Main.qml](../qml/Main.md),
|
||||
а также ключ `settings.language.<код>` с эндонимом (название языка не переводится — оно
|
||||
одинаково во всех колонках).
|
||||
4. При необходимости поправить `systemLanguage()` в `localization.cpp`: сейчас он выбирает
|
||||
русский для русской системной локали и английский во всех остальных случаях.
|
||||
1. Запись в `strings` сразу с `ru` и `en`.
|
||||
2. `Loc.t.домен.вид.имя` в QML или `Loc::text("домен.вид.имя")` в C++.
|
||||
3. `python3 tools/check_translations.py`.
|
||||
|
||||
## Выбор языка и его хранение
|
||||
## Добавить язык
|
||||
|
||||
Ключ настройки — `language`, значения `"system"`, `"ru"`, `"en"`, по умолчанию `"system"`.
|
||||
Хранится в `settings.json` рядом с остальными настройками; в интерфейсе — первым пунктом
|
||||
диалога «Настройки запуска», применяется по кнопке «Сохранить».
|
||||
1. Код в `_meta.languages`, название в `_meta.displayNames`.
|
||||
2. Значение для этого кода в каждой записи `strings`.
|
||||
3. Ключ `settings.language.<код>` с названием языка на нём самом, одинаковым во всех колонках.
|
||||
4. Пункт в модели `stLanguage` и код в `stLanguage.codes` в [Main.qml](../qml/Main.md).
|
||||
5. Если нужно, поправить `systemLanguage()`: сейчас при русской системной локали выбирается
|
||||
русский, при любой другой — английский.
|
||||
|
||||
Круг замкнут в одну сторону, циклической зависимости нет:
|
||||
## Где хранится выбранный язык
|
||||
|
||||
Ключ `language` в `settings.json`, по умолчанию `system`. Меняется в окне настроек и применяется
|
||||
по кнопке «Сохранить».
|
||||
|
||||
```
|
||||
main.cpp ──► Localization::load() ──► LauncherPaths::settingsFile() (только чтение, один раз)
|
||||
LauncherBackend::updateSettings() ──► Localization::setLanguage() (в одну сторону)
|
||||
main.cpp ──► Localization::load() ──► читает settings.json (один раз)
|
||||
LauncherBackend::updateSettings() ──► Localization::setLanguage()
|
||||
```
|
||||
|
||||
`Localization` ничего не знает про [LauncherBackend](LauncherBackend.md) — писать `settings.json`
|
||||
по-прежнему может только он. Читать настройки самому приходится потому, что язык нужен раньше,
|
||||
чем QML вычислит первую привязку, а бэкенд появляется только вместе с движком.
|
||||
`settings.json` записывает только [LauncherBackend](LauncherBackend.md). Localization его только
|
||||
читает, и только при старте: язык нужен раньше первой привязки QML, а бэкенд создаётся вместе с
|
||||
движком. Обратной зависимости от бэкенда у Localization нет.
|
||||
|
||||
Неизвестное значение (файл правили руками) откатывается на `"system"` с предупреждением.
|
||||
Отсутствующий или испорченный каталог — ошибка на старте: `main()` пишет причину и возвращает
|
||||
`-1`. Файл вкомпилирован в бинарник, так что это может быть только ошибка сборки, а лаунчер,
|
||||
у которого все подписи выглядят как точечные ключи, хуже, чем лаунчер, который сказал, почему
|
||||
не запустился.
|
||||
Неизвестный язык в файле (если его правили руками) заменяется на `system` с предупреждением.
|
||||
|
||||
**Известное ограничение.** Уже сложенные в поля C++ строки не перепереводятся:
|
||||
Если каталог не найден или не разбирается, это ошибка сборки, потому что файл вкомпилирован в
|
||||
бинарник. `main()` пишет причину в `qCritical` и возвращает `-1`.
|
||||
|
||||
### Что не переводится при смене языка
|
||||
|
||||
Строки, которые уже собраны и лежат в полях C++, остаются на старом языке:
|
||||
`LauncherBackend::m_storageIssues` собирается при старте, а `stage()`/`status()` у
|
||||
[BuildSwitcher](BuildSwitcher.md) заменяются на следующем тике прогресса. Все они
|
||||
диагностические и короткоживущие.
|
||||
[BuildSwitcher](BuildSwitcher.md) обновятся только на следующем шаге. Всё это короткоживущие
|
||||
диагностические сообщения.
|
||||
|
||||
## Проверка
|
||||
Отдельно: `GameLauncher` прячет токен в строке запуска за `<скрыт>`, и эта строка не берётся из
|
||||
каталога.
|
||||
|
||||
`tools/check_translations.py` — только чтение, ненулевой код возврата при любой ошибке:
|
||||
## Проверка каталога
|
||||
|
||||
- каждый `Loc::text("…")` и `Loc::list("…")` из C++ есть в каталоге и совпадает по типу значения;
|
||||
- каждый путь `Loc.t.a.b.c` из QML разворачивается в существующий ключ;
|
||||
- наборы ключей у всех языков совпадают, пустых значений нет, типы одинаковы;
|
||||
- наборы `%N` совпадают по языкам, длины списков равны;
|
||||
- не осталось ни одного `tr(`, `qsTr(` или `QCoreApplication::translate`;
|
||||
- ключи, на которые никто не ссылается, — предупреждением.
|
||||
`tools/check_translations.py` ничего не меняет и возвращает ненулевой код при любой ошибке.
|
||||
Проверяет:
|
||||
|
||||
- у всех языков одинаковый набор ключей и типы, пустых значений нет, длины списков равны;
|
||||
- набор `%N` совпадает по языкам;
|
||||
- каждый `Loc::text("…")` и `Loc::list("…")` в C++ есть в каталоге с нужным типом;
|
||||
- каждый `Loc.t.a.b.c` в QML ведёт к существующему ключу (последний сегмент может быть методом
|
||||
JS, например `.arg`);
|
||||
- нигде не осталось `tr(`, `qsTr(`, `QCoreApplication::translate`.
|
||||
|
||||
Ключи, на которые никто не ссылается, выводятся предупреждением.
|
||||
|
||||
Скрипт смотрит только файлы в корне репозитория. Если исходники разложат по подпапкам, его нужно
|
||||
будет поправить.
|
||||
|
||||
---
|
||||
|
||||
При создании этого документа использовался ИИ.
|
||||
+54
-167
@@ -1,190 +1,77 @@
|
||||
# ModLoaderInstaller
|
||||
|
||||
## Обзор класса
|
||||
`modloaderinstaller.h`, `modloaderinstaller.cpp`
|
||||
|
||||
`ModLoaderInstaller` ставит модлоадер в `.minecraft`. Под одним фасадом он прячет два совершенно
|
||||
разных пути установки.
|
||||
Ставит модлоадер в `.minecraft`. Fabric и Quilt ставятся иначе, чем Forge и NeoForge, но снаружи у
|
||||
них одинаковый интерфейс.
|
||||
|
||||
**Fabric и Quilt** отдают готовое описание версии: лаунчер кладёт его в
|
||||
`versions/<id>/<id>.json` и передаёт дальше [VersionInstaller](VersionInstaller.md), который по
|
||||
полю `inheritsFrom` сам поставит ванильную версию и библиотеки лоадера.
|
||||
- **Fabric и Quilt** отдают готовое описание версии. Его достаточно положить в
|
||||
`versions/<id>/<id>.json`, а остальное докачает [VersionInstaller](VersionInstaller.md).
|
||||
- **Forge и NeoForge** при установке патчат клиентский jar, поэтому запускается их официальный
|
||||
`installer.jar` без окна. После него проверяется, какой профиль появился в `versions/`.
|
||||
|
||||
**Forge и NeoForge** так не умеют: их установка — это патч клиентского jar. Поэтому лаунчер
|
||||
запускает официальный `installer.jar` найденной Java в headless-режиме и смотрит, какой профиль
|
||||
появился в `versions`. Процессу установщика при этом подсовывается эталонный zlib — см.
|
||||
[ZlibReference](zlibreference.md).
|
||||
Экземпляр держит [LauncherBackend](LauncherBackend.md). Геттеры прогресса устроены так же, как у
|
||||
`VersionInstaller` (`label`, `stage`, `currentFile`, `bytesDone`, `bytesTotal`, `fraction`), и
|
||||
панель загрузки читает их одинаково.
|
||||
|
||||
Оба пути начинаются одинаково: пока ванильная версия не скачана целиком, ставить лоадер некуда.
|
||||
## install(gameDir, loader, gameVersion, loaderVersion, javaPreference)
|
||||
|
||||
Набор геттеров прогресса намеренно повторяет `VersionInstaller`: панель загрузки в интерфейсе
|
||||
читает их одинаково, независимо от того, кто сейчас работает.
|
||||
Если установка уже идёт или не указана версия, сразу приходит `failed`. Иначе:
|
||||
|
||||
## Место в проекте и зависимости
|
||||
1. Список версий модлоадера через [ModLoaderVersionService](ModLoaderVersionService.md).
|
||||
2. **Базовая версия игры.** Если её нет или не хватает файлов, она ставится через
|
||||
`VersionInstaller`, и ход идёт дальше только после её `finished`. Без целой ванильной версии
|
||||
модлоадер ставить некуда.
|
||||
3. Дальше один из двух путей.
|
||||
|
||||
Подключает [modloader.h](modloader.md). В конструктор принимает
|
||||
[ModLoaderVersionService](ModLoaderVersionService.md) — за адресом установщика — и
|
||||
[VersionInstaller](VersionInstaller.md) — за базовой версией и за докачиванием того, что
|
||||
`installer.jar` не положил.
|
||||
### Fabric и Quilt
|
||||
|
||||
Экземпляр создаётся и принадлежит [LauncherBackend](LauncherBackend.md).
|
||||
Описание скачивается из `meta.fabricmc.net/v2/versions/loader/<игра>/<лоадер>/profile/json`
|
||||
(у Quilt `meta.quiltmc.org/v3/...`). Проверяется, что в нём есть безопасный `id` и что
|
||||
`inheritsFrom` совпадает с выбранной версией игры. Файл сохраняется, и `VersionInstaller` получает
|
||||
этот `id`.
|
||||
|
||||
Требования сборки: `Qt6::Core` (`QProcess`, `QFile`) и `Qt6::Network` (`QNetworkAccessManager`,
|
||||
`QNetworkReply`).
|
||||
### Forge и NeoForge
|
||||
|
||||
## Иерархия и роль
|
||||
1. Ссылка на установщик берётся из записи `ModLoaderVersionService::find()`. Если записи нет,
|
||||
значит, эта пара версий несовместима. Установщик качается в
|
||||
`<root>/cache/loaders/<ключ>-<версия>-installer.jar`.
|
||||
2. Если в `.minecraft` нет `launcher_profiles.json`, создаётся заглушка. Без неё установщик
|
||||
отказывается работать: он считает, что официальный лаунчер не установлен.
|
||||
3. Java берётся через `JavaLocator::select()` по `javaMajor` базовой версии. `javaPreference` — это
|
||||
та же Java, что выбрана для игры.
|
||||
4. Запоминается список профилей в `versions/`.
|
||||
5. Запуск: `java -jar installer.jar --installClient <gameDir>`. Рабочий каталог — папка загрузок
|
||||
модлоадеров. На Linux в окружение процесса подставляется эталонный zlib (см.
|
||||
[zlibreference.h](zlibreference.md)).
|
||||
6. Вывод установщика построчно уходит в `log(line)`, бэкенд показывает его в журнале. Полностью он
|
||||
пишется в `<root>/cache/loaders/<ключ>-<версия>-install.log`, а последние 12 строк хранятся в
|
||||
памяти для текста ошибки.
|
||||
7. Через 15 минут процесс убивается.
|
||||
|
||||
Наследует `QObject`: мета-объектная система, шесть сигналов и владение по родителю. Объявлен
|
||||
виртуальный деструктор — класс владеет процессом установщика и открытым файлом журнала.
|
||||
После успешного завершения новый профиль ищется как разница списков до и после. Угадывать имя по
|
||||
строке нельзя: у Forge в разные годы оно составлялось по-разному. Если новых папок нет (установщик
|
||||
переписал существующую), ищется профиль, который наследует базовую версию и содержит в имени версию
|
||||
модлоадера. Если новых папок несколько, берётся та, где есть версия модлоадера.
|
||||
|
||||
## Публичные методы
|
||||
Найденный профиль передаётся `VersionInstaller`, который докачивает библиотеки, не положенные
|
||||
установщиком. Когда он отчитается, приходит `finished(loaderKey, gameVersion, loaderVersion,
|
||||
producedVersionId)`. Бэкенд записывает `producedVersionId` в `resolvedVersionId` сборки.
|
||||
|
||||
#### ModLoaderInstaller(ModLoaderVersionService \*meta, VersionInstaller \*versionInstaller, QObject \*parent = nullptr)
|
||||
### Если установщик упал
|
||||
|
||||
Создаёт установщик поверх двух сервисов. Ни один из них не переходит во владение установщика —
|
||||
оба обязаны пережить его.
|
||||
Все папки, которые он успел создать в `versions/`, удаляются. В текст ошибки идут код выхода,
|
||||
хвост журнала и путь к полному журналу.
|
||||
|
||||
#### bool isRunning() const
|
||||
Если подменить zlib не удалось, а в журнале есть `Processor failed` или `invalid outputs`, к
|
||||
сообщению добавляется прямая подсказка про zlib-ng. Почти наверняка дело в нём.
|
||||
|
||||
Идёт ли установка прямо сейчас.
|
||||
## cancel()
|
||||
|
||||
#### QString label() const
|
||||
Обрывает загрузку, убивает процесс установщика и удаляет то, что он создал. Если ход уже передан
|
||||
`VersionInstaller`, отменяется и он, а `canceled` придёт после его отчёта.
|
||||
|
||||
Подпись текущей установки для интерфейса — название лоадера с версиями.
|
||||
|
||||
#### QString stage() const
|
||||
|
||||
Текущий этап словами.
|
||||
|
||||
#### QString currentFile() const
|
||||
|
||||
Файл, который обрабатывается сейчас.
|
||||
|
||||
#### qint64 bytesDone() const
|
||||
|
||||
Сколько байт уже получено.
|
||||
|
||||
#### qint64 bytesTotal() const
|
||||
|
||||
Ожидаемый общий объём; `0` — неизвестен.
|
||||
|
||||
#### double fraction() const
|
||||
|
||||
Доля выполнения от `0` до `1` либо `-1`, пока итог неизвестен. У пути с `installer.jar` доля
|
||||
почти всё время равна `-1`: сколько работы осталось внутри чужого процесса, лаунчер не знает.
|
||||
|
||||
#### void install(const QString &gameDir, ModLoader loader, const QString &gameVersion, const QString &loaderVersion, const QString &javaPreference)
|
||||
|
||||
Ставит модлоадер. Параметр `javaPreference` — путь к java, указанный пользователем в настройках;
|
||||
он проверяется первым, а при пустом или неподходящем значении java ищется сама. Java нужна даже на
|
||||
пути Fabric и Quilt, потому что перед установкой лоадера скачивается базовая версия игры.
|
||||
|
||||
Перед запуском `installer.jar` установщик записывает заглушку `launcher_profiles.json`: без этого
|
||||
файла официальные установщики Forge и NeoForge отказываются работать.
|
||||
|
||||
#### void cancel()
|
||||
|
||||
Отменяет установку. Если `installer.jar` уже успел отработать, за появившиеся в `versions` папки
|
||||
отвечает лаунчер, и при отмене они убираются.
|
||||
|
||||
## Сигналы
|
||||
|
||||
#### started(const QString &label)
|
||||
|
||||
Установка началась. Обработчик показывает панель прогресса.
|
||||
|
||||
#### progressChanged()
|
||||
|
||||
Изменились числа прогресса. Обработчик перечитывает геттеры — тот же набор, что у
|
||||
[VersionInstaller](VersionInstaller.md).
|
||||
|
||||
#### finished(const QString &loaderKey, const QString &gameVersion, const QString &loaderVersion, const QString &producedVersionId)
|
||||
|
||||
Модлоадер установлен. Последний параметр — идентификатор появившегося профиля `versions/<id>`.
|
||||
Он важен именно для Forge: заранее этот идентификатор неизвестен, поэтому новый профиль ищется
|
||||
разницей между списком версий до и после запуска установщика, а не угадыванием строки —
|
||||
идентификаторы Forge отличаются по эпохам.
|
||||
|
||||
Обработчик записывает `producedVersionId` в сборку: именно этот профиль будет запускаться.
|
||||
|
||||
#### failed(const QString &label, const QString &message)
|
||||
|
||||
Установка не удалась. Текст ошибки по возможности объясняет причину: отдельно распознаётся случай,
|
||||
когда в системе стоит zlib-ng, а подменить его было нечем — это почти наверняка причина
|
||||
расхождения sha1 у Forge, и о ней стоит сказать прямым текстом.
|
||||
|
||||
#### canceled(const QString &label)
|
||||
|
||||
Установка отменена пользователем.
|
||||
|
||||
#### log(const QString &line)
|
||||
|
||||
Строка вывода процесса `installer.jar`. Обработчик пишет её в журнал; вывод также сохраняется в
|
||||
файл, чтобы разбираться с неудачной установкой после закрытия лаунчера.
|
||||
|
||||
## Владение и время жизни
|
||||
|
||||
Класс наследует `QObject` и принимает `parent` — родитель его и удалит.
|
||||
|
||||
Указатели на `ModLoaderVersionService` и `VersionInstaller`, переданные в конструктор, **не
|
||||
принадлежат** установщику: оба сервиса создаются раньше и живут дольше.
|
||||
|
||||
`QNetworkAccessManager` создаётся в конструкторе с установщиком в роли родителя; сетевой ответ,
|
||||
процесс установщика и файл журнала создаются по ходу работы и закрываются в деструкторе.
|
||||
|
||||
Установка асинхронна и состоит из вложенных продолжений: `ensureBaseVersion()` принимает функцию,
|
||||
которая будет вызвана после появления базовой версии. Уничтожение установщика посреди этой цепочки
|
||||
обрывает её.
|
||||
|
||||
## Потокобезопасность
|
||||
|
||||
Только поток GUI. Дочерний процесс `installer.jar` работает параллельно, но общение с ним идёт
|
||||
через сигналы `QProcess`, которые приходят в поток GUI.
|
||||
|
||||
## Взаимодействие с другими классами
|
||||
|
||||
`LauncherBackend` вызывает `install()` при установке сборки с модлоадером и переправляет сигналы
|
||||
прогресса в те же свойства, что и у `VersionInstaller`, — панель загрузки не различает, кто
|
||||
работает. По сигналу `finished` бэкенд записывает `producedVersionId` в поле сборки
|
||||
`resolvedVersionId`, которое до установки пусто и означает запуск на чистой ванили.
|
||||
|
||||
[ModLoaderVersionService](ModLoaderVersionService.md) даёт адрес `installer.jar`,
|
||||
[VersionInstaller](VersionInstaller.md) ставит базовую версию до начала работы и докачивает
|
||||
недостающие библиотеки после неё, [ZlibReference](zlibreference.md) готовит окружение процесса.
|
||||
|
||||
## Внешнее взаимодействие
|
||||
|
||||
**Сеть, исходящие запросы.** Загрузка `installer.jar` по HTTPS для Forge и NeoForge. Для Fabric и
|
||||
Quilt сеть используется только через сервис версий и `VersionInstaller`.
|
||||
|
||||
**Дочерний процесс.** `installer.jar` запускается java в headless-режиме через `QProcess`.
|
||||
Окружение процесса готовится `ZlibReference::applyTo()`: на Linux с zlib-ng туда дописывается
|
||||
`LD_PRELOAD` с эталонной библиотекой, иначе установщик Forge падает с сообщением «Processor
|
||||
failed, invalid outputs» — он сверяет sha1 собранных им же jar-файлов с эталоном, посчитанным на
|
||||
обычном zlib.
|
||||
|
||||
Вывод процесса читается построчно, отдаётся сигналом `log` и параллельно пишется в файл. Результат
|
||||
установки определяется не кодом выхода, а появлением новой папки в `versions`.
|
||||
|
||||
Все сигналы приходят в поток GUI.
|
||||
|
||||
## Пример использования
|
||||
|
||||
```cpp
|
||||
auto *loaderInstaller = new ModLoaderInstaller(loaderVersions, versionInstaller, this);
|
||||
|
||||
connect(loaderInstaller, &ModLoaderInstaller::finished, this,
|
||||
[this](const QString &key, const QString &game,
|
||||
const QString &loaderVersion, const QString &producedVersionId) {
|
||||
m_build.resolvedVersionId = producedVersionId;
|
||||
saveBuilds();
|
||||
});
|
||||
connect(loaderInstaller, &ModLoaderInstaller::failed, this,
|
||||
[this](const QString &, const QString &message) { emit launchError(message); });
|
||||
|
||||
loaderInstaller->install(gameDir, ModLoader::Forge,
|
||||
QStringLiteral("1.20.1"), QStringLiteral("47.4.0"),
|
||||
settings.javaPath);
|
||||
```
|
||||
Деструктор убивает процесс установщика, если тот ещё работает.
|
||||
|
||||
---
|
||||
|
||||
|
||||
@@ -1,152 +1,71 @@
|
||||
# ModLoaderVersionService
|
||||
|
||||
## Обзор класса
|
||||
`modloaderversionservice.h`, `modloaderversionservice.cpp`
|
||||
|
||||
Каждый модлоадер публикует свой список версий, и у каждого он устроен по-своему. `ModLoaderVersionService`
|
||||
приводит все четыре к одному виду: качает списки, кэширует в папке лаунчера и отдаёт из кэша, пока
|
||||
тот не устарел. Устроен так же, как [VersionManifestService](VersionManifestService.md).
|
||||
Списки версий Forge, Fabric, NeoForge и Quilt. Устроен так же, как
|
||||
[VersionManifestService](VersionManifestService.md): качает, хранит копию в
|
||||
`<root>/cache/loader_<ключ>.json` и отвечает из неё, пока она моложе 6 часов.
|
||||
|
||||
Главная особенность класса — в том, чего в нём нет: отдельной проверки совместимости с версией
|
||||
игры. Совместимость заложена в структуру данных. Fabric и Quilt отдают список сразу под нужную
|
||||
версию игры, а `maven-metadata` Forge и NeoForge раскладывается по версиям игры при разборе.
|
||||
Версии игры, под которую сборок нет, соответствует пустой список — выбрать несовместимый лоадер
|
||||
физически нечем.
|
||||
Совместимость с версией игры отдельно не проверяется: данные с самого начала разложены по версиям
|
||||
игры. Под версию, для которой сборок нет, лежит пустой список, и выбрать несовместимый модлоадер
|
||||
нечем.
|
||||
|
||||
## Место в проекте и зависимости
|
||||
Экземпляр держит [LauncherBackend](LauncherBackend.md). [ModLoaderInstaller](ModLoaderInstaller.md)
|
||||
берёт отсюда ссылку на `installer.jar` через `find()`.
|
||||
|
||||
Подключает [modloader.h](modloader.md): перечисление `ModLoader` и структура `LoaderVersionEntry`
|
||||
приходят оттуда.
|
||||
## Откуда берутся данные
|
||||
|
||||
Экземпляр создаётся и принадлежит [LauncherBackend](LauncherBackend.md). Ссылку на него получает
|
||||
[ModLoaderInstaller](ModLoaderInstaller.md) — из записи списка он берёт адрес `installer.jar`.
|
||||
**Fabric и Quilt** спрашиваются отдельно под каждую версию игры:
|
||||
`meta.fabricmc.net/v2/versions/loader/<игра>` и `meta.quiltmc.org/v3/versions/loader/<игра>`.
|
||||
Признак стабильности берётся из ответа, а если его нет, угадывается по имени.
|
||||
|
||||
Путь к файлу кэша даёт `LauncherPaths::loaderCacheFile()` из [launcherpaths.h](launcherpaths.md).
|
||||
**Forge** — один `maven-metadata.xml` на все версии игры. Строка `1.20.1-47.4.0` делится по первому
|
||||
дефису. Параллельно запрашивается `promotions_slim.json`, оттуда берутся отметки recommended. Если
|
||||
он не ответил, список всё равно сохраняется, только без отметок.
|
||||
|
||||
Требования сборки: `Qt6::Core` (`QDateTime`, `QHash`, `QSet`) и `Qt6::Network`
|
||||
(`QNetworkAccessManager`).
|
||||
**NeoForge** тоже из `maven-metadata.xml`, но версия игры в номере не записана. Её приходится
|
||||
выводить: `21.1.248` → `1.21.1`, `20.4.80` → `1.20.4`, `21.0.x` → `1.21`. Для четырёхчастных
|
||||
номеров вроде `25.1.0.1` правило другое: `25.1.0.1` → `25.1`. Вторым запросом читается старый
|
||||
репозиторий `net/neoforged/forge` с первыми сборками NeoForge под 1.20.1, которые выходили под
|
||||
именем артефакта `forge` и номерами в стиле Forge.
|
||||
|
||||
## Иерархия и роль
|
||||
Ссылка на установщик собирается по схеме maven:
|
||||
`<репозиторий>/<версия>/<артефакт>-<версия>-installer.jar`.
|
||||
|
||||
Наследует `QObject`: мета-объектная система, два сигнала и владение по родителю. Виртуальных
|
||||
методов базового класса не переопределяет.
|
||||
Версии внутри каждой версии игры отсортированы от новых к старым. Числа сравниваются как числа,
|
||||
поэтому `47.10` новее `47.9`.
|
||||
|
||||
## Псевдонимы типов
|
||||
## ensureLoaded(loader, gameVersion, callback, forceRefresh)
|
||||
|
||||
`ModLoaderVersionService::Callback` — `std::function<void(bool ok, const QString &warning)>`.
|
||||
Как и у сервиса манифеста, `ok == true` с непустым `warning` означает, что данные отдали из
|
||||
устаревшего кэша.
|
||||
Логика та же, что у манифеста версий. Отличие в том, что свежесть у Fabric и Quilt считается для
|
||||
каждой версии игры отдельно, а у Forge и NeoForge для модлоадера целиком. По этому же ключу
|
||||
объединяются одновременные запросы.
|
||||
|
||||
## Публичные методы
|
||||
- Пустая `gameVersion` — сразу `ok` и пустой список.
|
||||
- При первом обращении к модлоадеру читается кэш, и если он есть, испускается
|
||||
`versionsChanged(key, gameVersion)`.
|
||||
- Если сеть не ответила, но какие-то данные по модлоадеру есть, колбэк получит `ok = true` и
|
||||
`warning`.
|
||||
- Если ответ не разобрался, хороший кэш не затирается.
|
||||
|
||||
#### explicit ModLoaderVersionService(QObject \*parent = nullptr)
|
||||
`versionsChanged(loaderKey, gameVersion)` испускается после каждого успешного обновления.
|
||||
`refreshingChanged()` — в начале и в конце запроса. `isRefreshing(loader, gameVersion)` отвечает,
|
||||
идёт ли запрос по этому ключу.
|
||||
|
||||
Создаёт сервис и его `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);
|
||||
});
|
||||
```json
|
||||
{
|
||||
"fetchedAt": "...",
|
||||
"games": {
|
||||
"1.20.1": { "fetchedAt": "...", "entries": [ { "loaderVersion": "47.4.0", ... } ] }
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
Верхний `fetchedAt` используется для Forge и NeoForge, `fetchedAt` внутри версии игры — для Fabric и
|
||||
Quilt.
|
||||
|
||||
---
|
||||
|
||||
При создании этого документа использовался ИИ.
|
||||
+57
-137
@@ -1,160 +1,80 @@
|
||||
# MsaAuthService
|
||||
|
||||
## Обзор класса
|
||||
`msaauthservice.h`, `msaauthservice.cpp`
|
||||
|
||||
`MsaAuthService` — авторизация через учётную запись Microsoft, то есть вход с лицензионной копией
|
||||
игры. Это та же цепочка, что и в официальном лаунчере: OAuth2 → Xbox Live → XSTS → Minecraft
|
||||
Services → проверка лицензии.
|
||||
Вход через аккаунт Microsoft, то есть с лицензионной копией игры. Цепочка такая же, как у
|
||||
официального лаунчера: OAuth2 → Xbox Live → XSTS → Minecraft Services → проверка лицензии →
|
||||
профиль. Результат приходит в `AuthResult` из [AuthService](AuthService.md).
|
||||
|
||||
Результат отдаётся тем же `AuthResult`, что и [AuthService](AuthService.md) для Ely.by и офлайна,
|
||||
поэтому запуск игры дальше идёт по общему пути и ничего не знает о способе входа.
|
||||
Окно входа сервис не показывает. Страницу Microsoft открывает
|
||||
[MicrosoftLoginDialog](../qml/MicrosoftLoginDialog.md), а сервис даёт адрес этой страницы и умеет
|
||||
узнать адрес возврата. Класс компилируется всегда, но без Qt WebEngine показать страницу нечем, и
|
||||
[LauncherBackend](LauncherBackend.md) сразу отвечает ошибкой.
|
||||
|
||||
Класс не показывает окно входа сам: страницу Microsoft открывает
|
||||
[MicrosoftLoginDialog](../qml/MicrosoftLoginDialog.md) на стороне QML, а сервис даёт ему адрес
|
||||
страницы и разбирает адрес возврата.
|
||||
## Чьё это приложение
|
||||
|
||||
## Место в проекте и зависимости
|
||||
Сейчас используется `client_id` официального лаунчера Mojang (`00000000402b5328`). Это старое
|
||||
приложение Live Connect, поэтому и адреса от `login.live.com`:
|
||||
|
||||
Подключает `authservice.h` — ради общей структуры `AuthResult`. Экземпляр создаётся и принадлежит
|
||||
[LauncherBackend](LauncherBackend.md).
|
||||
| Константа | Значение |
|
||||
|-----------|----------|
|
||||
| `kAuthorizeUrl` | `https://login.live.com/oauth20_authorize.srf` |
|
||||
| `kTokenUrl` | `https://login.live.com/oauth20_token.srf` |
|
||||
| `kRedirectUri` | `https://login.live.com/oauth20_desktop.srf` |
|
||||
| `kScope` | `service::user.auth.xboxlive.com::MBI_SSL` |
|
||||
| `kRpsTicketPrefix` | пусто |
|
||||
|
||||
Требования сборки: `Qt6::Core` (`QJsonObject`, `QString`, `QUrl`) и `Qt6::Network`
|
||||
(`QNetworkAccessManager`).
|
||||
Это временное решение. Чтобы перейти на своё приложение в Azure (с включённым «Allow public client
|
||||
flows»), меняются эти шесть констант. Что именно подставить, написано в комментарии в начале
|
||||
`msaauthservice.cpp`. Главное, что легко пропустить: у нового приложения в `RpsTicket` нужна
|
||||
приставка `d=`.
|
||||
|
||||
Сам класс собирается всегда и не зависит от Qt WebEngine — от наличия WebEngine зависит только
|
||||
окно, в котором показывается страница входа. Поэтому в сборке без WebEngine сервис существует, но
|
||||
воспользоваться им нельзя: показать страницу нечем.
|
||||
`clientId()` отдаёт этот id наружу. Бэкенд подставляет его в `${clientid}`, потому что официальный
|
||||
лаунчер передаёт туда id приложения, а не случайный токен.
|
||||
|
||||
## Иерархия и роль
|
||||
## Окно входа
|
||||
|
||||
Наследует `QObject`: мета-объектная система, сигнал `progress`, владение по родителю. Виртуальных
|
||||
методов базового класса не переопределяет.
|
||||
`authorizationUrl()` — адрес страницы входа с `client_id`, `response_type=code`, `redirect_uri`,
|
||||
`scope` и `prompt=select_account`. Последний параметр нужен, чтобы при повторном входе можно было
|
||||
выбрать другой аккаунт.
|
||||
|
||||
## Псевдонимы типов
|
||||
`matchRedirect(url, &code, &error)` возвращает `true`, только если хост и путь совпадают с
|
||||
`kRedirectUri`. Код или ошибка ищутся сначала в query, потом во фрагменте. В `error` попадает
|
||||
`error_description`, а если его нет, то `error`. Эта проверка сделана в C++, а не в QML, чтобы она
|
||||
всегда совпадала с тем адресом, который сервис сам передаёт в `redirect_uri`.
|
||||
|
||||
`MsaAuthService::Callback` — `std::function<void(const AuthResult &)>`. Колбэк вызывается ровно
|
||||
один раз и в потоке GUI.
|
||||
## Цепочка
|
||||
|
||||
## Публичные методы
|
||||
`loginWithCode(code, callback)` меняет код на токен (`grant_type=authorization_code`).
|
||||
`loginWithRefreshToken(refreshToken, callback)` делает то же по refresh-токену. Дальше путь общий:
|
||||
|
||||
#### explicit MsaAuthService(QObject \*parent = nullptr)
|
||||
1. **Токен Microsoft.** Форма на `kTokenUrl`. Из ответа берутся `access_token` и `refresh_token`.
|
||||
2. **Xbox Live.** `user.auth.xboxlive.com/user/authenticate`, `RpsTicket` = приставка + токен
|
||||
Microsoft.
|
||||
3. **XSTS.** `xsts.auth.xboxlive.com/xsts/authorize` для `rp://api.minecraftservices.com/`. При 401
|
||||
код `XErr` переводится в понятный текст: нет профиля Xbox, страна не поддерживается, нужна
|
||||
проверка возраста, детский аккаунт без разрешения взрослого. Отсюда же берутся `uhs` и `xid`
|
||||
(он станет `xuid`).
|
||||
4. **Minecraft Services.** `login_with_xbox` с `XBL3.0 x=<uhs>;<xsts>`. В ответе токен Minecraft и
|
||||
`expires_in`, обычно сутки; от него считается `expiresAt`.
|
||||
5. **Лицензия.** `entitlements/mcstore`. Результат только запоминается и сам вход не останавливает.
|
||||
6. **Профиль.** `minecraft/profile`. Ответы 404 и 401 означают, что профиля нет: выставляется
|
||||
`licenseMissing`. Текст ошибки зависит от шага 5. Если игра куплена, значит, игрок ещё не выбрал
|
||||
ник. Если не куплена, значит, лицензии нет.
|
||||
|
||||
Создаёт сервис и его `QNetworkAccessManager`. Конструктор помечен `explicit`.
|
||||
`userType` = `msa`, `clientToken` = id приложения.
|
||||
|
||||
#### static QString clientId()
|
||||
Каждый шаг испускает `progress(message)` до отправки запроса. Таймаут на каждый запрос 20 секунд,
|
||||
повторных попыток нет. Сетевая ошибка и отказ сервера дают разные тексты.
|
||||
|
||||
Идентификатор приложения лаунчера в Microsoft. Игра ждёт его в `${clientid}`: официальный лаунчер
|
||||
подставляет туда именно идентификатор приложения, а не случайный токен сессии.
|
||||
Refresh-токен Microsoft живёт намного дольше суточного токена Minecraft, поэтому при обычном
|
||||
запуске окно входа не нужно: бэкенд вызывает `loginWithRefreshToken()`. Если Microsoft не вернул
|
||||
новый refresh-токен, бэкенд оставляет старый.
|
||||
|
||||
#### static QUrl authorizationUrl()
|
||||
## Колбэки
|
||||
|
||||
Адрес страницы входа для встроенного окна браузера. В адресе уже собраны идентификатор клиента,
|
||||
`redirect_uri` и параметр выбора аккаунта — вызывающему коду достаточно открыть эту ссылку.
|
||||
|
||||
#### static bool matchRedirect(const QUrl &url, QString \*code, QString \*error)
|
||||
|
||||
Отличает адрес, на который Microsoft возвращает управление после входа, от остальной навигации
|
||||
внутри окна. Возвращает `true` только для адреса возврата.
|
||||
|
||||
При совпадении заполняется ровно одно из двух: `code` — код авторизации при успешном входе, либо
|
||||
`error` — текст отказа. Разбор адреса живёт здесь, а не в QML, потому что правила совпадения
|
||||
обязаны совпадать с теми, по которым сервис сам строит `redirect_uri`.
|
||||
|
||||
#### void loginWithCode(const QString &code, Callback callback)
|
||||
|
||||
Полный вход по коду, полученному из окна браузера. Проходит всю цепочку: обмен кода на токен
|
||||
Microsoft, аутентификация в Xbox Live, авторизация XSTS, вход в Minecraft Services, проверка
|
||||
лицензии и получение профиля.
|
||||
|
||||
По ходу испускает `progress` с описанием текущего шага — цепочка длинная, и без обратной связи
|
||||
вход выглядел бы зависанием. Результат приходит в `callback` один раз.
|
||||
|
||||
Если вход прошёл, но копии игры на аккаунте нет, в результате выставлен `licenseMissing`: этот
|
||||
случай чинится только покупкой, поэтому обрабатывается отдельно от прочих ошибок.
|
||||
|
||||
#### void loginWithRefreshToken(const QString &refreshToken, Callback callback)
|
||||
|
||||
Продление сессии без участия пользователя. Refresh-токен Microsoft живёт куда дольше суточного
|
||||
токена Minecraft, так что при повторном запуске лаунчера обычно хватает его, и окно входа
|
||||
показывать не приходится.
|
||||
|
||||
Проходит ту же цепочку, начиная с обмена refresh-токена. Неудача означает, что токен окончательно
|
||||
протух и нужен полноценный вход через окно.
|
||||
|
||||
## Сигналы
|
||||
|
||||
#### progress(const QString &message)
|
||||
|
||||
Описание текущего шага цепочки авторизации.
|
||||
|
||||
Обработчик показывает сообщение пользователю. Сигнал особенно важен для этого класса: шагов пять,
|
||||
каждый — отдельный сетевой запрос, и между ними проходит заметное время.
|
||||
|
||||
## Владение и время жизни
|
||||
|
||||
Класс наследует `QObject` и принимает `parent` — родитель его и удалит. `QNetworkAccessManager`
|
||||
создаётся в конструкторе с сервисом в роли родителя.
|
||||
|
||||
Каждый шаг цепочки вызывается из колбэка предыдущего, поэтому незавершённый вход держит цепочку
|
||||
захваченных колбэков до своего конца. Уничтожение сервиса посреди цепочки обрывает её вместе с
|
||||
менеджером сети, и колбэк не вызывается.
|
||||
|
||||
## Потокобезопасность
|
||||
|
||||
Только поток GUI. Все методы асинхронные, колбэки и сигналы приходят в поток, где создан сервис.
|
||||
|
||||
## Взаимодействие с другими классами
|
||||
|
||||
`LauncherBackend` создаёт сервис, отдаёт в QML адрес страницы входа, принимает от окна код
|
||||
авторизации и вызывает `loginWithCode()`. Полученные `refreshToken` и `expiresAt` он сохраняет в
|
||||
профиле, чтобы при следующем запуске обойтись `loginWithRefreshToken()`.
|
||||
|
||||
Заполненный `AuthResult` дальше раскладывается по полям `LaunchOptions` для
|
||||
[GameLauncher](GameLauncher.md) — ровно так же, как результат от [AuthService](AuthService.md).
|
||||
|
||||
Со стороны QML вход выглядит так: `LauncherBackend` испускает сигнал с адресом страницы, главное
|
||||
окно открывает [MicrosoftLoginDialog](../qml/MicrosoftLoginDialog.md), тот следит за навигацией и
|
||||
возвращает код обратно в бэкенд.
|
||||
|
||||
## Внешнее взаимодействие
|
||||
|
||||
**Сеть, исходящие запросы.** Класс последовательно обращается к пяти внешним службам: конечной
|
||||
точке OAuth2 Microsoft, Xbox Live, XSTS, Minecraft Services и профильной конечной точке Minecraft.
|
||||
Формат — JSON поверх HTTPS, кроме первого шага, где тело запроса отправляется как форма
|
||||
(`postForm()`); дальше используются `postJson()` и `getJson()` с токеном в заголовке
|
||||
авторизации.
|
||||
|
||||
Все запросы инициирует лаунчер. Ответ каждого шага разбирается тремя исходами: успех, ошибка с
|
||||
кодом состояния и транспортная ошибка — последняя приходит отдельным параметром, чтобы отличить
|
||||
недоступную сеть от отказа службы.
|
||||
|
||||
Повторных попыток класс не делает. Все сигналы и колбэки приходят в поток GUI.
|
||||
|
||||
## Пример использования
|
||||
|
||||
```cpp
|
||||
auto *msa = new MsaAuthService(this);
|
||||
connect(msa, &MsaAuthService::progress, this, &Backend::showStatus);
|
||||
|
||||
// 1. открыть окно браузера на этом адресе
|
||||
emit microsoftLoginUrlReady(MsaAuthService::authorizationUrl());
|
||||
|
||||
// 2. когда окно поймало адрес возврата
|
||||
QString code, error;
|
||||
if (MsaAuthService::matchRedirect(url, &code, &error) && !code.isEmpty()) {
|
||||
msa->loginWithCode(code, [this](const AuthResult &result) {
|
||||
if (result.licenseMissing) {
|
||||
emit loginFailed(tr("На аккаунте нет копии Minecraft"));
|
||||
return;
|
||||
}
|
||||
if (!result.ok) {
|
||||
emit loginFailed(result.error);
|
||||
return;
|
||||
}
|
||||
storeSession(result);
|
||||
});
|
||||
}
|
||||
```
|
||||
Колбэк вызывается один раз в GUI-потоке. Шаги вызываются из колбэков предыдущих. Если удалить
|
||||
сервис посреди цепочки, она оборвётся вместе с менеджером сети, и колбэк не вызовется.
|
||||
|
||||
---
|
||||
|
||||
|
||||
+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()`.
|
||||
|
||||
---
|
||||
|
||||
При создании этого документа использовался ИИ.
|
||||
@@ -1,161 +1,31 @@
|
||||
# SeasonalPackDownloader
|
||||
|
||||
## Обзор класса
|
||||
`seasonalpackdownloader.h`, `seasonalpackdownloader.cpp`
|
||||
|
||||
`SeasonalPackDownloader` скачивает один архив сезонной сборки в файл. Задача узкая и отдельная по
|
||||
двум причинам: пак — это сотни мегабайт, поэтому он пишется потоком, а не держится в памяти; и его
|
||||
sha256 обязательно сверяется, потому что распаковывать битую загрузку поверх рабочей `.minecraft`
|
||||
нельзя.
|
||||
Качает один архив сезонной сборки в файл и проверяет sha256. Пак весит сотни мегабайт, поэтому
|
||||
пишется потоком через `QSaveFile`, а не собирается в памяти. Битый архив распаковывать поверх
|
||||
рабочей `.minecraft` нельзя, поэтому файл появляется на диске только после совпадения хеша.
|
||||
|
||||
Распаковкой класс не занимается — это делает [BuildSwitcher](BuildSwitcher.md) в отдельном потоке
|
||||
вместе с остальными операциями над содержимым папки игры.
|
||||
Распаковывает пак [BuildSwitcher](BuildSwitcher.md). Экземпляр держит
|
||||
[LauncherBackend](LauncherBackend.md), который и вызывает загрузку на шаге Downloading установки
|
||||
сезонной сборки.
|
||||
|
||||
Набор геттеров прогресса повторяет [VersionInstaller](VersionInstaller.md) и
|
||||
[JavaInstaller](JavaInstaller.md): панель загрузки в интерфейсе читает их одинаково, независимо от
|
||||
того, кто сейчас работает.
|
||||
## download(url, targetPath, sha256, expectedSize, label)
|
||||
|
||||
## Место в проекте и зависимости
|
||||
- `targetPath` — `<root>/builds/<id>/.pack-r<ревизия>.zip`. Если файл уже есть, он будет
|
||||
перезаписан: недокачанный пак с прошлой попытки не должен дожить до новой.
|
||||
- `expectedSize` из каталога используется для прогресса, пока сервер не сообщил настоящий размер.
|
||||
- Таймаут — 60 секунд без данных. Повторных попыток нет: если загрузка оборвалась, установку
|
||||
нужно начать заново.
|
||||
|
||||
Экземпляр создаётся и принадлежит [LauncherBackend](LauncherBackend.md). Адрес архива, его размер
|
||||
и контрольную сумму даёт запись каталога от [SeasonalBuildService](SeasonalBuildService.md).
|
||||
Если загрузка уже идёт, сразу приходит `failed`.
|
||||
|
||||
Требования сборки: `Qt6::Core` (`QCryptographicHash`, `QSaveFile`, `QTimer`) и `Qt6::Network`
|
||||
(`QNetworkAccessManager`, `QNetworkReply`).
|
||||
Сигналы: `started(label)`, `progressChanged()` (не чаще раза в 100 мс), `finished(path)`,
|
||||
`failed(label, message)`, `canceled(label)`. Геттеры прогресса такие же, как у других
|
||||
установщиков.
|
||||
|
||||
## Иерархия и роль
|
||||
|
||||
Наследует `QObject`: мета-объектная система, пять сигналов и владение по родителю. Объявлен
|
||||
виртуальный деструктор — класс владеет незавершённой загрузкой и открытым файлом.
|
||||
|
||||
## Публичные методы
|
||||
|
||||
#### explicit SeasonalPackDownloader(QObject \*parent = nullptr)
|
||||
|
||||
Создаёт загрузчик, его `QNetworkAccessManager` и таймер сглаживания прогресса. Конструктор помечен
|
||||
`explicit`.
|
||||
|
||||
#### bool isRunning() const
|
||||
|
||||
Идёт ли загрузка прямо сейчас.
|
||||
|
||||
#### QString label() const
|
||||
|
||||
Подпись загрузки для интерфейса — как правило, название сезонной сборки.
|
||||
|
||||
#### QString stage() const
|
||||
|
||||
Текущий этап словами.
|
||||
|
||||
#### QString currentFile() const
|
||||
|
||||
Файл, который качается сейчас.
|
||||
|
||||
#### qint64 bytesDone() const
|
||||
|
||||
Сколько байт уже получено.
|
||||
|
||||
#### qint64 bytesTotal() const
|
||||
|
||||
Ожидаемый размер архива.
|
||||
|
||||
#### double fraction() const
|
||||
|
||||
Доля выполнения от `0` до `1` либо `-1`, пока итог неизвестен — например, когда сервер не сообщил
|
||||
размер, а в записи каталога он не был указан.
|
||||
|
||||
#### void download(const QUrl &url, const QString &targetPath, const QString &sha256, qint64 expectedSize, const QString &label)
|
||||
|
||||
Скачивает архив по адресу `url` в `targetPath`, сверяя sha256 с переданным значением.
|
||||
|
||||
Файл `targetPath` перезаписывается: недокачанный пак с прошлой попытки не должен пережить новую.
|
||||
Запись идёт через `QSaveFile`, поэтому на месте назначения файл появляется только целиком и только
|
||||
после успешной проверки контрольной суммы.
|
||||
|
||||
Параметр `expectedSize` берётся из записи каталога и используется для расчёта доли выполнения,
|
||||
пока сервер не сообщил размер сам.
|
||||
|
||||
#### void cancel()
|
||||
|
||||
Отменяет загрузку. Недокачанный файл на месте назначения не остаётся.
|
||||
|
||||
## Сигналы
|
||||
|
||||
#### started(const QString &label)
|
||||
|
||||
Загрузка началась. Обработчик показывает панель прогресса.
|
||||
|
||||
#### progressChanged()
|
||||
|
||||
Изменились числа прогресса; испускается не чаще, чем позволяет внутренний таймер, — иначе сигнал
|
||||
на каждый принятый блок обошёлся бы дороже самой загрузки. Обработчик перечитывает геттеры.
|
||||
|
||||
#### finished(const QString &path)
|
||||
|
||||
Архив скачан и проверен; в параметре — путь к готовому файлу.
|
||||
|
||||
Обработчик передаёт этот путь [BuildSwitcher](BuildSwitcher.md) для раскатки поверх содержимого
|
||||
`.minecraft`.
|
||||
|
||||
#### failed(const QString &label, const QString &message)
|
||||
|
||||
Загрузка не удалась: сеть недоступна, сервер ответил ошибкой или не сошлась контрольная сумма.
|
||||
Последний случай особенно важен — он означает, что архив повреждён и распаковывать его нельзя.
|
||||
|
||||
#### canceled(const QString &label)
|
||||
|
||||
Загрузка отменена пользователем.
|
||||
|
||||
## Владение и время жизни
|
||||
|
||||
Класс наследует `QObject` и принимает `parent` — родитель его и удалит.
|
||||
`QNetworkAccessManager` и таймер создаются в конструкторе с загрузчиком в роли родителя.
|
||||
|
||||
Скачиваемый файл хранится как `std::unique_ptr<QSaveFile>`: незавершённая запись отменяется вместе
|
||||
с уничтожением объекта, и повреждённый архив не попадает на место назначения. Сетевой ответ
|
||||
создаётся по ходу работы и закрывается в деструкторе.
|
||||
|
||||
## Потокобезопасность
|
||||
|
||||
Только поток GUI. Данные пишутся на диск блоками по мере поступления, поэтому длительных
|
||||
синхронных операций в потоке нет.
|
||||
|
||||
## Взаимодействие с другими классами
|
||||
|
||||
`LauncherBackend` вызывает `download()` при установке или обновлении сезонной сборки, передавая
|
||||
адрес и контрольную сумму из записи [SeasonalBuildService](SeasonalBuildService.md). Сигналы
|
||||
прогресса он переправляет в те же свойства, что и остальные загрузчики, поэтому панель в главном
|
||||
окне не различает, кто работает.
|
||||
|
||||
По сигналу `finished` бэкенд передаёт путь к архиву в `BuildSwitcher::applyPack()` вместе со
|
||||
списком уходящих файлов из описания предыдущей ревизии.
|
||||
|
||||
## Внешнее взаимодействие
|
||||
|
||||
**Сеть, исходящие запросы.** Одна загрузка по HTTPS с файлового сервера сборок. Направление
|
||||
одностороннее, тело ответа — двоичный архив.
|
||||
|
||||
Данные пишутся потоком через `QSaveFile` с одновременным подсчётом sha256; несовпадение суммы
|
||||
приводит к сигналу `failed`, и файл на месте назначения не появляется. Повторных попыток класс не
|
||||
делает: решение о повторе принимает пользователь.
|
||||
|
||||
Все сигналы приходят в поток GUI.
|
||||
|
||||
## Пример использования
|
||||
|
||||
```cpp
|
||||
auto *packLoader = new SeasonalPackDownloader(this);
|
||||
|
||||
connect(packLoader, &SeasonalPackDownloader::progressChanged, this, &Backend::downloadChanged);
|
||||
connect(packLoader, &SeasonalPackDownloader::finished, this, [this](const QString &path) {
|
||||
m_switcher->applyPack(m_buildId, m_buildName, path,
|
||||
m_previousEntries, m_note, m_gameDir);
|
||||
});
|
||||
connect(packLoader, &SeasonalPackDownloader::failed, this,
|
||||
[this](const QString &, const QString &message) { emit launchError(message); });
|
||||
|
||||
packLoader->download(entry.archiveUrl, targetPath,
|
||||
entry.archiveSha256, entry.archiveSize, entry.name);
|
||||
```
|
||||
При ошибке и несовпадении хеша файл удаляется. `cancel()` обрывает запрос, и `canceled` приходит
|
||||
после его завершения.
|
||||
|
||||
---
|
||||
|
||||
|
||||
+70
-205
@@ -1,210 +1,75 @@
|
||||
# VersionInstaller
|
||||
|
||||
## Обзор класса
|
||||
|
||||
Установить версию Minecraft — значит положить в `.minecraft` пять групп файлов: описание версии,
|
||||
клиентский jar, библиотеки, индекс ресурсов и сами ресурсы. Последних — десятки тысяч мелких
|
||||
файлов.
|
||||
|
||||
`VersionInstaller` делает это фоном, не блокируя интерфейс: складывает всё нужное в очередь
|
||||
загрузок, качает несколько файлов параллельно, пишет их потоком на диск и по ходу сообщает
|
||||
прогресс. Одна версия ставится за раз, остальные ждут в очереди.
|
||||
|
||||
Класс также разворачивает цепочку наследования: если у версии есть `inheritsFrom`, родительская
|
||||
версия ставится перед ней.
|
||||
|
||||
## Место в проекте и зависимости
|
||||
|
||||
Подключает [minecraftversion.h](minecraftversion.md) — по разобранной версии он и понимает, что
|
||||
качать. В конструктор принимает [VersionManifestService](VersionManifestService.md): оттуда
|
||||
берётся адрес описания версии.
|
||||
|
||||
Экземпляр создаётся и принадлежит [LauncherBackend](LauncherBackend.md). На него же опирается
|
||||
[ModLoaderInstaller](ModLoaderInstaller.md) — профиль модлоадера ставится поверх установленной
|
||||
версии игры.
|
||||
|
||||
Требования сборки: `Qt6::Core` (`QCryptographicHash`, `QSaveFile`, `QQueue`, `QTimer`) и
|
||||
`Qt6::Network` (`QNetworkAccessManager`, `QNetworkReply`).
|
||||
|
||||
## Иерархия и роль
|
||||
|
||||
Наследует `QObject`: мета-объектная система, пять сигналов и владение по родителю. Объявлен
|
||||
виртуальный деструктор — класс владеет незавершёнными загрузками и обязан их закрыть.
|
||||
|
||||
## Публичные структуры
|
||||
|
||||
### DownloadTask
|
||||
|
||||
Один файл, который нужно положить в `.minecraft`.
|
||||
|
||||
| Поле | Тип | По умолчанию | Описание |
|
||||
|------|-----|--------------|----------|
|
||||
| `kind` | `DownloadTask::Kind` | `ClientJar` | Что это за файл |
|
||||
| `url` | `QUrl` | — | Откуда качать |
|
||||
| `path` | `QString` | — | Абсолютный путь назначения |
|
||||
| `sha1` | `QString` | — | Контрольная сумма; пустая означает «не проверять» |
|
||||
| `size` | `qint64` | `0` | Ожидаемый размер; `0` — неизвестен |
|
||||
| `label` | `QString` | — | Что показать в панели прогресса |
|
||||
| `attempts` | `int` | `0` | Сколько попыток уже сделано |
|
||||
|
||||
#### Перечисление DownloadTask::Kind
|
||||
|
||||
| Значение | Описание |
|
||||
|----------|----------|
|
||||
| `ClientJar` | Клиентский jar версии |
|
||||
| `Library` | Библиотека из `libraries/` |
|
||||
| `AssetIndex` | Индекс ресурсов |
|
||||
| `AssetObject` | Один файл ресурсов |
|
||||
| `LoggingConfig` | Конфигурация журналирования log4j |
|
||||
|
||||
### ActiveDownload
|
||||
|
||||
Файл в процессе скачивания: задача, сетевой ответ, открытый `QSaveFile`, накапливаемая
|
||||
контрольная сумма и число принятых байт. Файлы пишутся потоком — держать десятки мегабайт в
|
||||
памяти незачем, а нескольких параллельных загрузок хватило бы на сотни.
|
||||
|
||||
## Публичные методы
|
||||
|
||||
#### explicit VersionInstaller(VersionManifestService \*manifest, QObject \*parent = nullptr)
|
||||
|
||||
Создаёт установщик поверх сервиса манифеста. Сервис не переходит во владение установщика и обязан
|
||||
пережить его. Конструктор помечен `explicit`.
|
||||
|
||||
#### bool isRunning() const
|
||||
|
||||
Идёт ли установка прямо сейчас.
|
||||
|
||||
#### QString versionId() const
|
||||
|
||||
Идентификатор версии, которая ставится в данный момент.
|
||||
|
||||
#### QString stage() const
|
||||
|
||||
Текущий этап установки словами — это же значение показывается в заголовке панели прогресса.
|
||||
|
||||
#### QString currentFile() const
|
||||
|
||||
Подпись файла, который качается сейчас.
|
||||
|
||||
#### qint64 bytesDone() const
|
||||
|
||||
Сколько байт уже получено, с учётом идущих загрузок.
|
||||
|
||||
#### qint64 bytesTotal() const
|
||||
|
||||
Ожидаемый общий объём. Растёт по ходу установки: полный размер ресурсов становится известен
|
||||
только после разбора их индекса.
|
||||
|
||||
#### double fraction() const
|
||||
|
||||
Доля выполнения от `0` до `1` либо `-1`, пока итоговый объём неизвестен. Значение `-1` панель
|
||||
прогресса показывает многоточием вместо процентов.
|
||||
|
||||
#### void install(const QString &gameDir, const QString &versionId)
|
||||
|
||||
Ставит версию в указанную папку игры. Если установка уже идёт, версия становится в очередь.
|
||||
|
||||
Порядок работы: разрешение записи манифеста, загрузка описания версии, подготовка списка задач,
|
||||
скачивание, разворачивание индекса ресурсов и — для версий до 1.6 — раскладка ресурсов в плоскую
|
||||
папку, которую те версии умеют читать.
|
||||
|
||||
#### bool isQueued(const QString &versionId) const
|
||||
|
||||
Стоит ли версия в очереди на установку. Позволяет не ставить одну и ту же версию дважды.
|
||||
|
||||
#### void cancel()
|
||||
|
||||
Отменяет текущую установку и очищает очередь. Незавершённые файлы не остаются на диске: они
|
||||
пишутся через `QSaveFile` и фиксируются только целиком.
|
||||
|
||||
## Сигналы
|
||||
|
||||
#### started(const QString &versionId)
|
||||
|
||||
Установка версии началась. Обработчик показывает панель прогресса и выставляет признак занятости.
|
||||
|
||||
#### progressChanged()
|
||||
|
||||
Изменились числа прогресса. Испускается не чаще десяти раз в секунду: при тысячах мелких файлов
|
||||
сигнал на каждый принятый блок обошёлся бы дороже самой загрузки.
|
||||
|
||||
Обработчик перечитывает `stage()`, `currentFile()`, `bytesDone()`, `bytesTotal()` и `fraction()`.
|
||||
|
||||
#### finished(const QString &versionId)
|
||||
|
||||
Версия установлена успешно. Обработчик убирает панель прогресса, обновляет список установленных
|
||||
версий и пересчитывает комплектность сборок.
|
||||
|
||||
#### failed(const QString &versionId, const QString &message)
|
||||
|
||||
Установка не удалась; в параметре — текст ошибки для пользователя. Часть файлов при этом может
|
||||
остаться на диске: повторная установка докачает недостающее.
|
||||
|
||||
#### canceled(const QString &versionId)
|
||||
|
||||
Установка отменена пользователем. В отличие от `failed`, ошибку показывать не нужно.
|
||||
|
||||
## Владение и время жизни
|
||||
|
||||
Класс наследует `QObject` и принимает `parent` — родитель его и удалит.
|
||||
|
||||
Указатель на `VersionManifestService`, переданный в конструктор, **не принадлежит** установщику:
|
||||
сервис создаётся раньше и живёт дольше. `QNetworkAccessManager` и оба таймера создаются в
|
||||
конструкторе с установщиком в роли родителя.
|
||||
|
||||
Активные загрузки хранятся как `std::shared_ptr<ActiveDownload>`, а файл внутри каждой — как
|
||||
`std::unique_ptr<QSaveFile>`: незавершённая запись отменяется вместе с уничтожением объекта, и
|
||||
испорченный файл не попадает на место назначения.
|
||||
|
||||
## Потокобезопасность
|
||||
|
||||
Только поток GUI, как и остальной сетевой код лаунчера. Параллелизм здесь — не потоки, а
|
||||
несколько одновременных сетевых запросов в одном цикле событий. Раскладка ресурсов для старых
|
||||
версий выполняется порциями по таймеру, чтобы не занимать поток надолго.
|
||||
|
||||
## Взаимодействие с другими классами
|
||||
|
||||
`LauncherBackend` вызывает `install()` при установке сборки и переправляет все пять сигналов в
|
||||
свойства, которые читает QML: панель прогресса главного окна показывает `stage()`, `fraction()` и
|
||||
байты, а `finished` обновляет список установленных версий.
|
||||
|
||||
[ModLoaderInstaller](ModLoaderInstaller.md) держит ссылку на установщик: профиль модлоадера
|
||||
требует, чтобы базовая версия игры была уже на месте.
|
||||
|
||||
Разбор описания версии идёт через `VersionLoader::load()` из
|
||||
[minecraftversion.h](minecraftversion.md).
|
||||
|
||||
## Внешнее взаимодействие
|
||||
|
||||
**Сеть, исходящие запросы.** Класс качает файлы с серверов Mojang через `QNetworkAccessManager`.
|
||||
Направление одностороннее, протокол — HTTPS; описание версии и индекс ресурсов приходят как JSON,
|
||||
остальное — двоичными файлами.
|
||||
|
||||
Каждый файл пишется потоком через `QSaveFile` с одновременным подсчётом sha1; несовпадение
|
||||
контрольной суммы считается неудачей загрузки. Неудачная задача повторяется — счётчик попыток
|
||||
хранится в самой задаче, — и только исчерпав попытки, приводит к сигналу `failed`.
|
||||
|
||||
Существующие файлы сверяются только по размеру: перехеширование сотен мегабайт при каждом
|
||||
добавлении версии дороже, чем риск битого файла.
|
||||
|
||||
Все сигналы приходят в поток GUI.
|
||||
|
||||
## Пример использования
|
||||
|
||||
```cpp
|
||||
auto *installer = new VersionInstaller(manifestService, this);
|
||||
|
||||
connect(installer, &VersionInstaller::progressChanged, this, [this, installer] {
|
||||
emit downloadProgress(installer->fraction(), installer->currentFile());
|
||||
});
|
||||
connect(installer, &VersionInstaller::finished, this, &Backend::onVersionInstalled);
|
||||
connect(installer, &VersionInstaller::failed, this, [this](const QString &id, const QString &message) {
|
||||
emit launchError(tr("Не удалось установить %1: %2").arg(id, message));
|
||||
});
|
||||
|
||||
if (!installer->isQueued(versionId))
|
||||
installer->install(gameDir, versionId);
|
||||
```
|
||||
`versioninstaller.h`, `versioninstaller.cpp`
|
||||
|
||||
Ставит версию игры в `.minecraft`: описание версии, клиентский jar, библиотеки, индекс ресурсов,
|
||||
сами ресурсы и конфиг логирования. Одновременно ставится одна версия, остальные ждут в очереди.
|
||||
Всё работает в GUI-потоке на асинхронной сети.
|
||||
|
||||
Экземпляр держит [LauncherBackend](LauncherBackend.md). Ещё им пользуется
|
||||
[ModLoaderInstaller](ModLoaderInstaller.md): перед установкой модлоадера нужно поставить базовую
|
||||
версию, а после установки докачать библиотеки нового профиля.
|
||||
|
||||
## Интерфейс
|
||||
|
||||
- `install(gameDir, versionId)` — поставить в очередь. Если эта версия уже ставится или ждёт,
|
||||
ничего не происходит. Если id содержит слэши, `.` или `..`, сразу приходит `failed`.
|
||||
- `cancel()` — отменить текущую установку и очистить очередь.
|
||||
- `isRunning()`, `isQueued(id)`, `versionId()`.
|
||||
- Для панели прогресса: `stage()`, `currentFile()`, `bytesDone()`, `bytesTotal()`, `fraction()`
|
||||
(от 0 до 1; −1, если общий объём неизвестен).
|
||||
|
||||
Сигналы: `started(id)`, `progressChanged()` (не чаще раза в 100 мс), `finished(id)`,
|
||||
`failed(id, message)`, `canceled(id)`.
|
||||
|
||||
`started` испускается один раз на всю серию установок из очереди. `finished` испускается для
|
||||
каждой версии.
|
||||
|
||||
## Как идёт установка
|
||||
|
||||
1. **Описание версии.** У [VersionManifestService](VersionManifestService.md) берётся ссылка на
|
||||
`<id>.json`, файл скачивается, sha1 сверяется с манифестом, и файл сохраняется в
|
||||
`versions/<id>/`. Если версии в манифесте нет (профиль модлоадера) или сеть недоступна, берётся
|
||||
файл, который уже лежит на диске. Если нет и его, установка падает.
|
||||
2. **Родитель.** Если в описании есть `inheritsFrom`, а родителя на диске нет, текущая версия
|
||||
уступает место: в начало очереди встают сначала родитель, потом она сама. Глубина цепочки — до
|
||||
10 звеньев.
|
||||
3. **Список файлов.** `VersionLoader::load()` сводит цепочку, и из результата составляется список:
|
||||
клиентский jar, все библиотеки, индекс ресурсов и конфиг логов. Ссылки не по `https` и
|
||||
библиотеки без адреса пропускаются с предупреждением.
|
||||
4. **Загрузка.** До 8 файлов одновременно. Каждый пишется потоком в `QSaveFile`, sha1 считается на
|
||||
ходу. При временной ошибке (таймаут, обрыв, HTTP 429 или 5xx) и при несовпадении хеша файл
|
||||
ставится обратно в начало очереди с паузой 400 мс, потом 800 мс. После третьей неудачи
|
||||
установка падает. Другие ошибки сразу её прерывают. Таймаут на файл 30 секунд.
|
||||
5. **Ресурсы.** Как только индекс ресурсов на диске, он разворачивается в задачи на загрузку из
|
||||
`resources.download.minecraft.net/<xx>/<hash>`. До этого момента в общем объёме учитывается
|
||||
`totalSize` из описания версии, а потом он заменяется точной суммой.
|
||||
6. **Старые ресурсы.** У версий до 1.6 (`virtual`, `map_to_resources`, `assets` = `legacy` или
|
||||
`pre-1.6`) игра ищет ресурсы не в `assets/objects`, а в обычной папке с именами файлов. Они
|
||||
копируются в `assets/virtual/legacy/` или в `resources/` по 64 файла за такт таймера, чтобы
|
||||
окно не подвисало.
|
||||
7. **Проверка.** `GameLauncher::missingFiles()`. Если чего-то нет, это ошибка.
|
||||
|
||||
## Уже скачанные файлы
|
||||
|
||||
Существующий файл считается целым, если его размер совпадает с ожидаемым (или размер неизвестен).
|
||||
Хеш не пересчитывается: перехеширование сотен мегабайт ресурсов при каждой установке обходится
|
||||
дороже, чем редкий битый файл. Для будущей кнопки «Проверить файлы» есть флаг `m_verifyExisting`,
|
||||
сейчас он всегда `false`.
|
||||
|
||||
Пропуск файлов идёт пачками по 256 за такт. Иначе установка версии, у которой уже есть все
|
||||
ресурсы, повесила бы окно на перебор десятков тысяч записей.
|
||||
|
||||
## Ошибки и отмена
|
||||
|
||||
При ошибке все активные загрузки обрываются, недописанные файлы откатываются (`QSaveFile`), а
|
||||
**очередь очищается**: остальные версии из неё тоже не поставятся. К тексту ошибки добавляются до
|
||||
трёх накопленных предупреждений.
|
||||
|
||||
Отмена обрывает активные запросы и ждёт, пока все они вернутся. Только после этого приходит
|
||||
`canceled`. Пока отмена не закончилась, `install()` отвечает отказом.
|
||||
|
||||
Деструктор блокирует сигналы и отменяет установку.
|
||||
|
||||
---
|
||||
|
||||
|
||||
@@ -1,167 +1,67 @@
|
||||
# VersionManifestService
|
||||
|
||||
## Обзор класса
|
||||
`versionmanifestservice.h`, `versionmanifestservice.cpp`
|
||||
|
||||
Каталог версий Minecraft — это манифест Mojang: около тысячи записей от альф 2010 года до
|
||||
свежайших снапшотов. `VersionManifestService` отвечает за него целиком: скачивает манифест,
|
||||
кэширует в папке лаунчера и отдаёт из кэша, пока тот не устарел.
|
||||
Каталог версий Mojang. Скачивает `version_manifest_v2.json`, хранит его копию в папке лаунчера и
|
||||
отвечает из неё, пока копия свежая.
|
||||
|
||||
Класс нужен двум потребителям: окну выбора версии, которому нужен весь список, и установщику,
|
||||
которому по идентификатору версии нужна ссылка на её описание.
|
||||
Экземпляр держит [LauncherBackend](LauncherBackend.md): из каталога он строит список для окна
|
||||
выбора версии. [VersionInstaller](VersionInstaller.md) берёт из каталога ссылку на описание версии
|
||||
через `find()`.
|
||||
|
||||
## Место в проекте и зависимости
|
||||
## RemoteVersionEntry
|
||||
|
||||
Экземпляр создаётся и принадлежит [LauncherBackend](LauncherBackend.md). Ссылку на него получает
|
||||
[VersionInstaller](VersionInstaller.md) — установщик берёт из манифеста адрес описания версии.
|
||||
| Поле | Что это |
|
||||
|------|---------|
|
||||
| `id` | `1.21.8`, `25w33a`, `b1.7.3` |
|
||||
| `type` | `release`, `snapshot`, `old_beta`, `old_alpha` |
|
||||
| `url` | адрес `<id>.json` |
|
||||
| `sha1` | хеш этого json |
|
||||
| `releaseTime` | |
|
||||
|
||||
Путь к файлу кэша даёт `LauncherPaths::versionManifestFile()` из
|
||||
[launcherpaths.h](launcherpaths.md).
|
||||
Записи без `id` или с адресом не по `https` отбрасываются. Список отсортирован по `releaseTime`,
|
||||
новые сверху. При одинаковом времени сортировка идёт по `id`.
|
||||
|
||||
Требования сборки: `Qt6::Core` (`QDateTime`, `QHash`, `QList`, `QUrl`) и `Qt6::Network`
|
||||
(`QNetworkAccessManager`).
|
||||
## ensureLoaded(callback, forceRefresh = false)
|
||||
|
||||
## Иерархия и роль
|
||||
Через этот метод сервис получает данные; других способов нет.
|
||||
|
||||
Наследует `QObject`: мета-объектная система, два сигнала и владение по родителю. Виртуальных
|
||||
методов базового класса не переопределяет.
|
||||
1. При первом вызове читается кэш `<root>/cache/version_manifest.json`. Если он разобрался,
|
||||
сразу испускается `versionsChanged`, и окно показывает список ещё до ответа сети.
|
||||
2. Если данные моложе 6 часов и `forceRefresh` не задан, колбэк вызывается через
|
||||
`QTimer::singleShot(0)` с `ok = true`. Сеть не трогается.
|
||||
3. Иначе колбэк встаёт в очередь. Если запрос уже идёт, второй не отправляется.
|
||||
|
||||
## Публичные структуры
|
||||
Ответ сети разбирается, кэш перезаписывается через `QSaveFile`, испускается `versionsChanged`, и
|
||||
все колбэки из очереди получают результат.
|
||||
|
||||
### RemoteVersionEntry
|
||||
Если сеть не ответила, но старые данные есть, колбэк получит `ok = true` и непустой `warning`:
|
||||
работать можно, просто список, возможно, неполный. Если данных нет совсем, придёт `ok = false`.
|
||||
Бэкенд показывает `warning` пользователю только во втором случае.
|
||||
|
||||
Одна строка манифеста Mojang.
|
||||
Таймаут запроса 20 секунд.
|
||||
|
||||
| Поле | Тип | Описание |
|
||||
|------|-----|----------|
|
||||
| `id` | `QString` | Идентификатор версии: `1.21.8`, `25w33a`, `b1.7.3` |
|
||||
| `type` | `QString` | Категория: `release`, `snapshot`, `old_beta` или `old_alpha` |
|
||||
| `url` | `QUrl` | Адрес `<id>.json` с описанием версии |
|
||||
| `sha1` | `QString` | Контрольная сумма самого описания |
|
||||
| `releaseTime` | `QDateTime` | Дата выпуска; по ней список сортируется новыми вперёд |
|
||||
## Остальное
|
||||
|
||||
Категории `type` — те же ключи, по которым окно выбора версии делит каталог на вкладки; версии, не
|
||||
попавшие ни в одну из четырёх, интерфейс относит к категории «прочие».
|
||||
- `versions()` — весь список.
|
||||
- `find(id)` — запись по id (через хеш-таблицу, а не перебором).
|
||||
- `hasData()`, `isRefreshing()`, `fetchedAt()`.
|
||||
- `refreshingChanged` испускается в начале и в конце запроса. Бэкенд превращает его в
|
||||
`catalogLoading`.
|
||||
|
||||
## Псевдонимы типов
|
||||
## Формат кэша
|
||||
|
||||
`VersionManifestService::Callback` — `std::function<void(bool ok, const QString &warning)>`.
|
||||
Сочетание `ok == true` с непустым `warning` означает особый случай: данные отдали, но из
|
||||
устаревшего кэша — сеть недоступна, а показать что-то нужно.
|
||||
|
||||
## Публичные методы
|
||||
|
||||
#### explicit VersionManifestService(QObject \*parent = nullptr)
|
||||
|
||||
Создаёт сервис и его `QNetworkAccessManager`. Манифест при этом не читается — чтение кэша
|
||||
откладывается до первого обращения. Конструктор помечен `explicit`.
|
||||
|
||||
#### QList<RemoteVersionEntry> versions() const
|
||||
|
||||
Текущий список версий. Возвращает копию; пустой список означает, что данных ещё нет.
|
||||
|
||||
#### bool hasData() const
|
||||
|
||||
Есть ли хоть какие-то данные — из сети или из кэша.
|
||||
|
||||
#### bool isRefreshing() const
|
||||
|
||||
Идёт ли сейчас сетевое обновление. Интерфейс показывает по этому признаку строку загрузки вместо
|
||||
пустого списка.
|
||||
|
||||
#### QDateTime fetchedAt() const
|
||||
|
||||
Когда данные были получены. По этой отметке решается, устарел ли кэш.
|
||||
|
||||
#### std::optional<RemoteVersionEntry> find(const QString &id) const
|
||||
|
||||
Запись по идентификатору версии; `std::nullopt`, если такой версии в манифесте нет. Из неё
|
||||
установщик берёт ссылку на описание версии. Поиск идёт по внутреннему указателю, а не перебором.
|
||||
|
||||
#### void ensureLoaded(Callback callback, bool forceRefresh = false)
|
||||
|
||||
Единственная точка входа. Свежий кэш означает отложенный вызов колбэка без обращения к сети; иначе
|
||||
запускается один сетевой запрос на всех, кто успел попросить, — колбэки накапливаются и вызываются
|
||||
все разом по его завершении.
|
||||
|
||||
Параметр `forceRefresh` обходит проверку свежести кэша: так работает кнопка принудительного
|
||||
обновления.
|
||||
|
||||
Колбэк вызывается ровно один раз и всегда в потоке GUI, в том числе когда данные уже есть.
|
||||
|
||||
## Сигналы
|
||||
|
||||
#### versionsChanged()
|
||||
|
||||
Список версий изменился — пришли новые данные из сети или прочитан кэш.
|
||||
|
||||
Обработчик перечитывает `versions()` и обновляет интерфейс. В лаунчере на этот сигнал завязано
|
||||
свойство каталога версий, которое читает окно выбора.
|
||||
|
||||
#### refreshingChanged()
|
||||
|
||||
Изменился признак сетевого обновления. Обработчик показывает или убирает индикатор загрузки.
|
||||
|
||||
## Владение и время жизни
|
||||
|
||||
Класс наследует `QObject` и принимает `parent` — родитель его и удалит.
|
||||
`QNetworkAccessManager` создаётся в конструкторе с сервисом в роли родителя.
|
||||
|
||||
Отложенные колбэки хранятся в списке до завершения текущего запроса. Уничтожение сервиса с
|
||||
незавершённым запросом обрывает его вместе с менеджером сети, и накопленные колбэки не
|
||||
вызываются.
|
||||
|
||||
Ссылку на сервис держит установщик версий; уничтожать сервис раньше установщика нельзя.
|
||||
|
||||
## Потокобезопасность
|
||||
|
||||
Только поток GUI — так же, как [AuthService](AuthService.md). Чтение и запись кэша выполняются в
|
||||
вызывающем потоке, поэтому первое обращение к манифесту делает короткую файловую операцию
|
||||
синхронно.
|
||||
|
||||
## Взаимодействие с другими классами
|
||||
|
||||
`LauncherBackend` вызывает `ensureLoaded()` при открытии окна выбора версии и по кнопке
|
||||
обновления, а сигналы `versionsChanged` и `refreshingChanged` переправляет в свойства, которые
|
||||
читает QML. Список из `versions()` он сводит с установленными версиями и отдаёт в интерфейс уже
|
||||
готовыми строками.
|
||||
|
||||
[VersionInstaller](VersionInstaller.md) обращается к `find()`, чтобы получить адрес описания
|
||||
версии перед началом загрузки.
|
||||
|
||||
## Внешнее взаимодействие
|
||||
|
||||
**Сеть, исходящие запросы.** Класс скачивает манифест версий Mojang через
|
||||
`QNetworkAccessManager`. Формат — JSON поверх HTTPS, запрос инициирует лаунчер.
|
||||
|
||||
Стратегия при недоступной сети встроена в контракт колбэка: если есть устаревший кэш, он
|
||||
отдаётся с `ok == true` и заполненным `warning`, и интерфейс показывает список вместо ошибки.
|
||||
Полное отсутствие данных даёт `ok == false`.
|
||||
|
||||
**Файловый кэш.** Манифест сохраняется в файл, путь к которому даёт
|
||||
`LauncherPaths::versionManifestFile()`, вместе с отметкой времени получения.
|
||||
|
||||
Сигналы и колбэки приходят в поток GUI.
|
||||
|
||||
## Пример использования
|
||||
|
||||
```cpp
|
||||
auto *manifest = new VersionManifestService(this);
|
||||
connect(manifest, &VersionManifestService::versionsChanged, this, &Backend::rebuildCatalog);
|
||||
|
||||
manifest->ensureLoaded([this, manifest](bool ok, const QString &warning) {
|
||||
if (!ok) {
|
||||
emit catalogError(warning);
|
||||
return;
|
||||
}
|
||||
if (!warning.isEmpty())
|
||||
showStatus(warning); // список из устаревшего кэша
|
||||
|
||||
const auto entry = manifest->find(QStringLiteral("1.21.8"));
|
||||
if (entry)
|
||||
startDownload(entry->url);
|
||||
});
|
||||
```json
|
||||
{
|
||||
"fetchedAt": "2026-10-11T08:00:00Z",
|
||||
"versions": [
|
||||
{ "id": "1.21.8", "type": "release", "url": "https://...", "sha1": "...", "releaseTime": "..." }
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
Хранится только то, что нужно лаунчеру, а не весь манифест Mojang.
|
||||
|
||||
---
|
||||
|
||||
При создании этого документа использовался ИИ.
|
||||
+40
-57
@@ -1,76 +1,59 @@
|
||||
# javalocator.h — JavaLocator
|
||||
# JavaLocator
|
||||
|
||||
## Обзор
|
||||
`javalocator.h`, `javalocator.cpp`
|
||||
|
||||
Для запуска Minecraft нужна Java подходящей версии: старым версиям игры — восьмая, новым —
|
||||
семнадцатая или двадцать первая. Лаунчер умеет скачивать сборки Java сам (см.
|
||||
[JavaInstaller](JavaInstaller.md)), но сначала стоит посмотреть, что уже есть на машине.
|
||||
Поиск java, уже установленной на машине, и выбор подходящей под версию игры. Скачиванием
|
||||
занимается [JavaInstaller](JavaInstaller.md), а здесь только смотрим, что есть.
|
||||
|
||||
`JavaLocator` отвечает за поиск: собирает все доступные java, определяет их версии и выбирает
|
||||
подходящую под требование версии игры. Пространство имён используется при подготовке запуска и при
|
||||
показе списка найденных Java в настройках.
|
||||
Это пространство имён со свободными функциями. Его используют [GameLauncher](GameLauncher.md) при
|
||||
запуске, [LauncherBackend](LauncherBackend.md) для списка в настройках и для шага Java в установке
|
||||
сезонной сборки.
|
||||
|
||||
## Пространства имён
|
||||
## findAll(gameDir)
|
||||
|
||||
`JavaLocator` группирует четыре функции поиска и выбора. Состояния нет, но результат определения
|
||||
версии кэшируется внутри реализации.
|
||||
Список путей к исполняемому `java` (`java.exe` на Windows) без повторов. Порядок такой:
|
||||
|
||||
## Функции
|
||||
1. сборки, скачанные лаунчером в `<root>/java` (через `JavaRuntimeStore::installed()`);
|
||||
2. всё, что найдётся рекурсивно в `<gameDir>/runtime`: туда кладёт Java официальный лаунчер;
|
||||
3. `$JAVA_HOME/bin`;
|
||||
4. `java` из `PATH`;
|
||||
5. стандартные каталоги установки. На Windows это `Program Files`, `Program Files (x86)` и
|
||||
`ProgramW6432` с подпапками `Java`, `Eclipse Adoptium`, `Eclipse Foundation`, `AdoptOpenJDK`,
|
||||
`Amazon Corretto`, `Zulu`, `BellSoft`, `Microsoft`, `Semeru` и JBR из Android Studio. На macOS
|
||||
это `/Library/Java/JavaVirtualMachines` и такая же папка в домашнем каталоге. На Linux —
|
||||
`/usr/lib/jvm`, `/usr/java`, `/opt/java`, `/opt/jdk`.
|
||||
|
||||
#### QStringList findAll(const QString &gameDir)
|
||||
В каждом каталоге установки проверяются `bin`, `Contents/Home/bin` (так устроены JDK на macOS) и
|
||||
`jre/bin`.
|
||||
|
||||
Все java, которые удалось найти, без дублей и в порядке приоритета. Просматриваются четыре
|
||||
источника: рантайм самого Minecraft внутри папки игры, переменная `JAVA_HOME`, переменная `PATH` и
|
||||
стандартные каталоги установки JDK и JRE.
|
||||
## majorVersion(javaPath)
|
||||
|
||||
Параметр `gameDir` — папка игры; из неё берётся первый источник.
|
||||
Запускает `java -version` и достаёт мажорную версию из строки `version "..."`. Старая схема
|
||||
`1.8.0_x` даёт 8. Если процесс не ответил за 5 секунд или вывод не разобрался, возвращается 0.
|
||||
|
||||
Результат показывается в диалоге настроек списком «что нашлось в системе».
|
||||
Результат кэшируется в статической таблице на всё время работы лаунчера. Если java по тому же пути
|
||||
обновить, лаунчер узнает об этом только после перезапуска. Кэш не защищён от гонок, поэтому
|
||||
функцию можно вызывать только из одного потока, и в проекте это GUI-поток.
|
||||
|
||||
#### int majorVersion(const QString &javaPath)
|
||||
## select(gameDir, requiredMajor, preferred, &error)
|
||||
|
||||
Мажорная версия java по её пути: 8, 17, 21 и так далее. Возвращает `0`, если запустить
|
||||
исполняемый файл не удалось — путь неверен, файл не исполняемый или это не java.
|
||||
Выбирает java для запуска.
|
||||
|
||||
Результат кэшируется: определение версии требует запуска процесса, а один и тот же путь
|
||||
проверяется многократно.
|
||||
- Если `preferred` задан, он идёт первым кандидатом. Это может быть путь к файлу или к домашней
|
||||
папке JDK (тогда берётся `bin/java`). Если по этому пути ничего нет, функция сразу возвращает
|
||||
ошибку и других кандидатов не смотрит: пользователь явно указал путь, и молча подменять его
|
||||
было бы неправильно.
|
||||
- Дальше добавляется всё из `findAll()`.
|
||||
- Возвращается первый кандидат с версией не ниже `requiredMajor`. Если таких нет, в `error`
|
||||
пишется, какая самая новая java нашлась и где.
|
||||
|
||||
#### QString windowlessVariant(const QString &javaPath)
|
||||
Функция возвращает первую подходящую, а не самую новую. Поэтому скачанная лаунчером Java 17
|
||||
выиграет у системной Java 21, если игре нужна 17.
|
||||
|
||||
Заменяет `java.exe` на `javaw.exe`, чтобы игра не открывала окно консоли. На Unix возвращает вход
|
||||
без изменений — там разницы нет.
|
||||
## windowlessVariant(javaPath)
|
||||
|
||||
#### QString select(const QString &gameDir, int requiredMajor, const QString &preferred, QString \*error)
|
||||
|
||||
Выбирает java не ниже `requiredMajor`. Если задан `preferred` — путь, указанный пользователем в
|
||||
настройках, — он проверяется первым и, если подходит, побеждает. Иначе перебираются найденные
|
||||
`findAll()` варианты.
|
||||
|
||||
При неудаче возвращает пустую строку и заполняет `error` — текст объясняет, что именно не нашлось:
|
||||
подходящей версии нет вовсе или указанный пользователем путь не подошёл.
|
||||
|
||||
Функция запускает процессы для определения версий, поэтому может занять заметное время; вызывать
|
||||
её в обработчике нажатия не стоит.
|
||||
|
||||
## Зависимости
|
||||
|
||||
Подключает `QString` и `QStringList`. От классов проекта не зависит.
|
||||
|
||||
## Пример использования
|
||||
|
||||
```cpp
|
||||
QString error;
|
||||
const QString java = JavaLocator::select(gameDir,
|
||||
version.javaMajor,
|
||||
settings.javaPath,
|
||||
&error);
|
||||
if (java.isEmpty()) {
|
||||
emit launchError(error);
|
||||
return;
|
||||
}
|
||||
|
||||
process.start(JavaLocator::windowlessVariant(java), arguments);
|
||||
```
|
||||
На Windows заменяет `java.exe` на `javaw.exe` из той же папки, если он там есть. Тогда при запуске
|
||||
игры не появляется консольное окно. На других системах возвращает путь без изменений.
|
||||
|
||||
---
|
||||
|
||||
|
||||
+76
-135
@@ -1,153 +1,94 @@
|
||||
# javaruntime.h — JavaRuntime, JavaRuntimeStore и JavaRequirement
|
||||
# javaruntime.h
|
||||
|
||||
## Обзор
|
||||
`javaruntime.h`, `javaruntime.cpp`
|
||||
|
||||
`Minecraft_launcher` умеет скачивать Java сам: официальные сборки Mojang — те же, которыми игру
|
||||
запускает официальный лаунчер, — и сборки Eclipse Temurin в вариантах JDK и JRE. Скачанные сборки
|
||||
живут в папке лаунчера, по одной подпапке на сборку.
|
||||
Общие типы для Java: виды сборок, строка каталога, запись об установленной сборке, работа с папкой
|
||||
`<root>/java` и таблица «какая Java нужна какой версии игры».
|
||||
|
||||
Заголовок `javaruntime.h` — словарь этой части проекта. Он даёт перечисление видов сборок, две
|
||||
структуры (строка каталога и уже установленная сборка), пространство имён для работы с папкой
|
||||
`<root>/java` и таблицу требований версий игры к Java.
|
||||
## Виды сборок
|
||||
|
||||
Заголовок подключают [JavaRuntimeService](JavaRuntimeService.md) (каталог),
|
||||
[JavaInstaller](JavaInstaller.md) (скачивание и распаковка) и
|
||||
[LauncherBackend](LauncherBackend.md) (отдача каталога в QML).
|
||||
| `JavaRuntimeKind` | Ключ | Что это |
|
||||
|-------------------|------|---------|
|
||||
| `Mojang` | `java` | сборки, которые Mojang раздаёт для своего лаунчера |
|
||||
| `Jdk` | `jdk` | Eclipse Temurin JDK |
|
||||
| `Jre` | `jre` | Eclipse Temurin JRE |
|
||||
|
||||
## Типы
|
||||
`javaKindKey()`, `javaKindFromKey()`, `javaKindTitle()` переводят между перечислением, ключом и
|
||||
подписью для интерфейса. Ключ хранится в `runtime.json` установленной сборки и используется в QML.
|
||||
|
||||
| Имя | Вид | Описание |
|
||||
|-----|-----|----------|
|
||||
| `JavaRuntimeKind` | `enum class` | Откуда взялась сборка и что именно в ней лежит |
|
||||
| `JavaRuntimeEntry` | `struct` | Строка каталога — то, что можно скачать |
|
||||
| `InstalledJavaRuntime` | `struct` | Сборка, уже распакованная в папке лаунчера |
|
||||
## JavaRuntimeEntry
|
||||
|
||||
### JavaRuntimeKind
|
||||
Строка каталога [JavaRuntimeService](JavaRuntimeService.md), то есть то, что можно скачать.
|
||||
|
||||
| Значение | Ключ | Описание |
|
||||
|----------|------|----------|
|
||||
| `Mojang` | `java` | Тот же рантайм, которым игру запускает официальный лаунчер |
|
||||
| `Jdk` | `jdk` | Eclipse Temurin JDK: компилятор и инструменты в комплекте |
|
||||
| `Jre` | `jre` | Eclipse Temurin JRE: только то, что нужно для запуска |
|
||||
| Поле | Что это |
|
||||
|------|---------|
|
||||
| `id` | `temurin-jdk-21.0.12.1_1`, `mojang-java-runtime-delta` |
|
||||
| `kind` | |
|
||||
| `major` | 8, 17, 21… |
|
||||
| `version` | `21.0.12.1+1`, `21.0.7` |
|
||||
| `component` | только у Mojang: `java-runtime-delta` и т. п. |
|
||||
| `released` | |
|
||||
| `url` | архив Temurin или `manifest.json` компонента Mojang |
|
||||
| `checksum` | sha256 архива Temurin или sha1 манифеста Mojang |
|
||||
| `size` | размер загрузки, 0 если неизвестен |
|
||||
| `archive` | `zip`, `tar.gz` или `mojang` |
|
||||
| `architecture` | `x64`, `aarch64`; на macOS может быть не родной (см. JavaRuntimeService) |
|
||||
| `lts` | |
|
||||
|
||||
Эти же ключи служат именами категорий в окне выбора Java.
|
||||
## InstalledJavaRuntime
|
||||
|
||||
### JavaRuntimeEntry
|
||||
Скачанная сборка: `id`, `kind` (ключ), `major`, `version`, `javaPath` (абсолютный путь к
|
||||
`java`/`java.exe`), `size` (сколько заняла на диске).
|
||||
|
||||
| Поле | Тип | Описание |
|
||||
|------|-----|----------|
|
||||
| `id` | `QString` | Идентификатор сборки, например `temurin-jdk-21.0.12.1_1` или `mojang-java-runtime-delta` |
|
||||
| `kind` | `JavaRuntimeKind` | Вид сборки; по умолчанию `Jdk` |
|
||||
| `major` | `int` | Мажорная версия: 8, 17, 21… |
|
||||
| `version` | `QString` | Полная версия, например `21.0.12.1+1` или `21.0.7` |
|
||||
| `component` | `QString` | Имя компонента вида `java-runtime-delta`; только у сборок Mojang |
|
||||
| `released` | `QDateTime` | Дата выпуска; по ней каталог сортируется новыми вперёд |
|
||||
| `url` | `QUrl` | Архив Temurin либо `manifest.json` компонента Mojang |
|
||||
| `checksum` | `QString` | sha256 архива Temurin или sha1 манифеста Mojang |
|
||||
| `size` | `qint64` | Размер загрузки в байтах; `0` — неизвестен |
|
||||
| `archive` | `QString` | Формат: `zip`, `tar.gz` или `mojang` — последний означает не архив, а манифест с пофайловой загрузкой |
|
||||
| `architecture` | `QString` | `x64` или `aarch64`; на macOS бывает и не родная архитектура |
|
||||
| `lts` | `bool` | Версия с длительной поддержкой; помечается в списке |
|
||||
## JavaRuntimeStore
|
||||
|
||||
Метод `isValid()` возвращает `true`, когда заполнен `id`, `major` больше нуля и `url` корректен.
|
||||
Каждая сборка лежит в `<root>/java/<id>/`, а рядом с ней `runtime.json`:
|
||||
|
||||
### InstalledJavaRuntime
|
||||
|
||||
| Поле | Тип | Описание |
|
||||
|------|-----|----------|
|
||||
| `id` | `QString` | Идентификатор сборки, он же имя подпапки |
|
||||
| `kind` | `QString` | Вид сборки строкой: `java`, `jdk` или `jre` |
|
||||
| `major` | `int` | Мажорная версия |
|
||||
| `version` | `QString` | Полная версия |
|
||||
| `javaPath` | `QString` | Абсолютный путь к исполняемому файлу `java` или `java.exe` |
|
||||
| `size` | `qint64` | Сколько сборка заняла на диске по итогам установки |
|
||||
|
||||
## Функции
|
||||
|
||||
### Перевод видов сборок
|
||||
|
||||
#### QString javaKindKey(JavaRuntimeKind kind)
|
||||
|
||||
Строковый ключ вида сборки: `java`, `jdk` или `jre`.
|
||||
|
||||
#### std::optional<JavaRuntimeKind> javaKindFromKey(const QString &key)
|
||||
|
||||
Обратный перевод; `std::nullopt` для неизвестного ключа.
|
||||
|
||||
#### QString javaKindTitle(JavaRuntimeKind kind)
|
||||
|
||||
Человекочитаемое название вида сборки для интерфейса.
|
||||
|
||||
### JavaRuntimeStore — папка `<root>/java`
|
||||
|
||||
Устройство папки: одна подпапка на сборку плюс её описание внутри. Отдельного индекса нет
|
||||
намеренно — удалённую вручную папку не пришлось бы вычищать ещё и из общего файла.
|
||||
|
||||
#### QString dirFor(const QString &id)
|
||||
|
||||
Папка конкретной сборки внутри `<root>/java`.
|
||||
|
||||
#### QList<InstalledJavaRuntime> installed()
|
||||
|
||||
Всё, что лежит в `<root>/java` и на что нашлась java. Новые версии идут первыми.
|
||||
|
||||
#### std::optional<InstalledJavaRuntime> find(const QString &id)
|
||||
|
||||
Описание одной установленной сборки; `std::nullopt`, если такой нет.
|
||||
|
||||
#### QString locateBinary(const QString &rootDir)
|
||||
|
||||
Ищет `bin/java` в распакованном дереве. Раскладка отличается между поставщиками: у Temurin на macOS
|
||||
это `Contents/Home/bin`, у Mojang — `jre.bundle/Contents/Home/bin`.
|
||||
|
||||
#### bool writeMeta(const InstalledJavaRuntime &runtime, QString \*error)
|
||||
|
||||
Записывает описание сборки в её папку. Вызывается по завершении установки. При неудаче возвращает
|
||||
`false` и заполняет `error`.
|
||||
|
||||
#### bool remove(const QString &id, QString \*error)
|
||||
|
||||
Удаляет папку сборки целиком. При неудаче возвращает `false` и заполняет `error`.
|
||||
|
||||
#### QString sanitizeId(const QString &id)
|
||||
|
||||
Превращает идентификатор каталога в безопасное имя папки: всё, кроме букв, цифр, точки, дефиса и
|
||||
подчёркивания, заменяется. Идентификатор приходит из сети, поэтому подставлять его в путь как есть
|
||||
нельзя.
|
||||
|
||||
### JavaRequirement — какая Java нужна какой версии игры
|
||||
|
||||
Точный ответ лежит в `client.json` версии (поле `javaVersion.majorVersion`) и берётся оттуда при
|
||||
запуске. Эта таблица нужна раньше — когда версия ещё не скачана, а подсказку в каталоге показать
|
||||
надо.
|
||||
|
||||
#### int minimumFor(const QString &minecraftVersionId)
|
||||
|
||||
Минимальная мажорная версия Java для версии игры; `0`, если разобрать идентификатор версии не
|
||||
удалось.
|
||||
|
||||
#### QString coverage(int javaMajor)
|
||||
|
||||
Подпись к строке каталога вида «Minecraft 1.20.5 и новее» — какие версии игры покрывает эта версия
|
||||
Java.
|
||||
|
||||
## Зависимости
|
||||
|
||||
Подключает `QDateTime`, `QList`, `QString`, `QUrl` и `<optional>` — только Qt Core. От классов
|
||||
проекта не зависит.
|
||||
|
||||
## Пример использования
|
||||
|
||||
```cpp
|
||||
const auto runtime = JavaRuntimeStore::find(settings.javaRuntime);
|
||||
if (runtime && QFileInfo::exists(runtime->javaPath)) {
|
||||
javaPath = runtime->javaPath;
|
||||
} else {
|
||||
// сборка удалена вручную — возвращаемся к поиску в системе
|
||||
javaPath = JavaLocator::select(gameDir, version.javaMajor, settings.javaPath, &error);
|
||||
}
|
||||
```json
|
||||
{ "id": "...", "kind": "jdk", "major": 21, "version": "21.0.7", "javaPath": "bin/java", "size": 197132288 }
|
||||
```
|
||||
|
||||
`javaPath` хранится относительно папки сборки, так что папку лаунчера можно переносить.
|
||||
|
||||
- `sanitizeId(id)` — всё, кроме букв, цифр, `.`, `-`, `_`, заменяется на `_`. Ведущие точки
|
||||
убираются.
|
||||
- `dirFor(id)` — `<root>/java/<sanitizeId(id)>`.
|
||||
- `find(id)` — читает `runtime.json`. Возвращает пусто, если файла нет, `id` в нём другой или
|
||||
`java` по указанному пути не существует. Поэтому недокачанная сборка без `runtime.json` в
|
||||
списке не появится.
|
||||
- `installed()` — всё, что нашлось, от новых мажорных версий к старым.
|
||||
- `locateBinary(rootDir)` — ищет `bin/java` внутри распакованного дерева и берёт самый короткий
|
||||
путь. Нужно потому, что у архивов разная вложенность: `jdk-21/bin`, `jdk-21/Contents/Home/bin`
|
||||
и т. п.
|
||||
- `writeMeta(runtime, &error)` — пишет `runtime.json` через `QSaveFile`.
|
||||
- `remove(id, &error)` — удаляет папку сборки. Перед этим проверяет, что путь лежит внутри
|
||||
`<root>/java/`.
|
||||
|
||||
## JavaRequirement
|
||||
|
||||
`minimumFor(minecraftVersionId)` — минимальная Java для версии игры:
|
||||
|
||||
| Версия игры | Java |
|
||||
|-------------|------|
|
||||
| до 1.16 включительно, беты, альфы, всё нераспознанное | 8 |
|
||||
| 1.17 | 16 |
|
||||
| 1.18 — 1.20.4 | 17 |
|
||||
| 1.20.5 и новее | 21 |
|
||||
| версии с новой нумерацией (первое число не 1, например `26.1`) | 21 |
|
||||
| снапшоты `21wNNx` | 16 |
|
||||
| снапшоты `22w`, `23w` | 17 |
|
||||
| снапшоты `24w` и новее | 21 |
|
||||
| снапшоты до `21w` | 8 |
|
||||
|
||||
Пустая строка даёт 0. Таблица нужна там, где описания версии ещё нет на диске: в окне выбора Java
|
||||
и при установке сезонной сборки. При запуске игры используется `javaVersion.majorVersion` из
|
||||
описания версии.
|
||||
|
||||
Профили модлоадеров (`fabric-loader-...`) по этой таблице дают 8, поэтому бэкенд передаёт сюда
|
||||
`minecraftVersion` сборки, а не `resolvedVersionId`.
|
||||
|
||||
`coverage(javaMajor)` — подпись для окна выбора: какие версии игры эта Java потянет.
|
||||
|
||||
---
|
||||
|
||||
При создании этого документа использовался ИИ.
|
||||
+51
-131
@@ -1,144 +1,64 @@
|
||||
# launcherpaths.h — LauncherPaths
|
||||
# launcherpaths.h
|
||||
|
||||
## Обзор
|
||||
`launcherpaths.h`, `launcherpaths.cpp`
|
||||
|
||||
`Minecraft_launcher` хранит собственные данные отдельно от папки игры: настройки, профили, описания
|
||||
сборок, архивы содержимого `.minecraft`, скачанные сборки Java и кэши каталогов. Пространство имён
|
||||
`LauncherPaths` — единственное место, где эти пути вычисляются.
|
||||
Все пути к данным лаунчера. Пространство имён `LauncherPaths` из свободных функций, состояния нет:
|
||||
каждая функция просто собирает строку.
|
||||
|
||||
Собственная папка `BiLauncher` лежит рядом со стандартной `.minecraft`, а не в системном
|
||||
каталоге данных приложения: так все файлы лаунчера остаются там же, где сама игра, и переносятся
|
||||
вместе с ней.
|
||||
Данные лаунчера лежат в папке `BiLauncher` рядом со стандартной папкой игры, а не в системном
|
||||
каталоге данных приложения. `QStandardPaths` не используется, поэтому имя приложения и организации
|
||||
из `main.cpp` на эти пути не влияют.
|
||||
|
||||
К этому заголовку обращается почти каждый сервис проекта — везде, где нужно прочитать или записать
|
||||
файл в папке лаунчера.
|
||||
## Где что лежит
|
||||
|
||||
## Пространства имён
|
||||
`containerDir()` — папка, в которой живут и игра, и лаунчер:
|
||||
|
||||
`LauncherPaths` группирует функции, возвращающие абсолютные пути, и одну функцию создания корневой
|
||||
папки. Состояния у пространства имён нет: все функции вычисляют путь заново при каждом вызове.
|
||||
| ОС | `containerDir()` | папка игры по умолчанию | `rootDir()` |
|
||||
|----|------------------|-------------------------|-------------|
|
||||
| Windows | `%USERPROFILE%/AppData/Roaming` | `.minecraft` | `BiLauncher` |
|
||||
| macOS | `~/Library/Application Support` | `minecraft` (без точки, как у официального лаунчера) | `BiLauncher` |
|
||||
| Linux | `~` | `.minecraft` | `BiLauncher` |
|
||||
|
||||
## Функции
|
||||
На Linux папка лаунчера получается `~/BiLauncher`, без точки, то есть видимая.
|
||||
|
||||
### Корневые каталоги
|
||||
Внутри `rootDir()`:
|
||||
|
||||
#### QString containerDir()
|
||||
|
||||
Родительская папка, в которой лежит `.minecraft`, а рядом с ней — `BiLauncher`. От неё
|
||||
отсчитываются и папка игры по умолчанию, и корень данных лаунчера.
|
||||
|
||||
#### QString rootDir()
|
||||
|
||||
Корень данных лаунчера — `<containerDir>/BiLauncher`.
|
||||
|
||||
#### QString defaultMinecraftDir()
|
||||
|
||||
Стандартная папка игры. Используется, когда пользователь не задал свою в настройках.
|
||||
|
||||
#### bool ensureRootExists(QString *error = nullptr)
|
||||
|
||||
Создаёт папку лаунчера, если её ещё нет. Вызывается при каждом запуске и перед каждой записью.
|
||||
|
||||
Возвращает `false` и заполняет `error`, если папку не удалось создать или в неё не пишется. Все
|
||||
остальные функции пространства имён только считают строки и в этом смысле не могут завершиться
|
||||
неудачей — проверять доступность каталога нужно этой функцией.
|
||||
|
||||
### Файлы состояния
|
||||
|
||||
#### QString settingsFile()
|
||||
|
||||
Файл настроек запуска: папка игры, путь к Java, память, аргументы JVM, размер окна.
|
||||
|
||||
#### QString profilesFile()
|
||||
|
||||
Файл профилей игрока.
|
||||
|
||||
#### QString customBuildsFile()
|
||||
|
||||
`<root>/customBuilds.json` — пользовательские сборки.
|
||||
|
||||
#### QString legacyCustomBuildsFile()
|
||||
|
||||
`<root>/versions.json` — как сборки назывались до переименования. Читается один раз при миграции и
|
||||
больше ни для чего не нужен.
|
||||
|
||||
### Сборки и их архивы
|
||||
|
||||
#### QString buildStorageDir()
|
||||
|
||||
`<root>/builds` — архивы содержимого `.minecraft`, по одному на сборку.
|
||||
|
||||
#### QString buildDir(int buildId)
|
||||
|
||||
`<root>/builds/<id>` — папка одной сборки: её архив и, у сезонных, скачанный пак с описанием
|
||||
установленной ревизии.
|
||||
|
||||
#### QString seasonalStateFile(int buildId)
|
||||
|
||||
`<root>/builds/<id>/season.json` — какая ревизия сезонной сборки установлена и какие файлы она
|
||||
принесла. Список файлов нужен, чтобы при обновлении убрать те, что из сборки ушли.
|
||||
|
||||
### Java
|
||||
|
||||
#### QString javaDir()
|
||||
|
||||
`<root>/java` — сборки Java, скачанные лаунчером. Каждая в своей подпапке, имя подпапки —
|
||||
идентификатор сборки из каталога.
|
||||
|
||||
#### QString javaCatalogFile()
|
||||
|
||||
Слепок каталога доступных сборок Java с отметкой времени.
|
||||
|
||||
#### QString javaDownloadDir()
|
||||
|
||||
Каталог, куда качаются архивы Temurin до распаковки.
|
||||
|
||||
### Кэши и загрузки
|
||||
|
||||
#### QString cacheDir()
|
||||
|
||||
`<root>/cache` — данные, которые можно удалить без потерь.
|
||||
|
||||
#### QString versionManifestFile()
|
||||
|
||||
Слепок манифеста версий Mojang с отметкой времени.
|
||||
|
||||
#### QString seasonalCatalogFile()
|
||||
|
||||
Слепок каталога сезонных сборок с отметкой времени.
|
||||
|
||||
#### QString loaderCacheFile(const QString &loaderKey)
|
||||
|
||||
Слепок списка версий одного модлоадера. Параметр `loaderKey` принимает значения `forge`, `fabric`,
|
||||
`neoforge` и `quilt` — те же ключи, что возвращает `loaderKey()` из [modloader.h](modloader.md).
|
||||
|
||||
#### QString loaderDownloadDir()
|
||||
|
||||
Каталог, куда качаются `installer.jar` модлоадеров.
|
||||
|
||||
#### QString runtimeDir()
|
||||
|
||||
Каталог, куда качается `authlib-injector` — библиотека, подменяющая сервер авторизации при входе
|
||||
через Ely.by.
|
||||
|
||||
## Зависимости
|
||||
|
||||
Единственный подключаемый заголовок — `QString`. Пространство имён не зависит ни от одного класса
|
||||
проекта, поэтому его можно подключать откуда угодно без риска циклических зависимостей.
|
||||
|
||||
## Пример использования
|
||||
|
||||
```cpp
|
||||
QString error;
|
||||
if (!LauncherPaths::ensureRootExists(&error)) {
|
||||
qWarning() << "папка лаунчера недоступна:" << error;
|
||||
return;
|
||||
}
|
||||
|
||||
QFile file(LauncherPaths::customBuildsFile());
|
||||
if (file.open(QIODevice::WriteOnly))
|
||||
file.write(document.toJson());
|
||||
```
|
||||
BiLauncher/
|
||||
├── settings.json settingsFile()
|
||||
├── profiles.json profilesFile()
|
||||
├── customBuilds.json customBuildsFile()
|
||||
├── versions.json legacyCustomBuildsFile() — старое имя, читается только при миграции
|
||||
├── builds/ buildStorageDir()
|
||||
│ ├── index.json журнал BuildSwitcher
|
||||
│ └── <id>/ buildDir(id): архив сборки, скачанный пак
|
||||
│ └── season.json seasonalStateFile(id)
|
||||
├── java/ javaDir(): скачанные сборки Java, каждая в <id>/
|
||||
├── runtime/ runtimeDir(): authlib-injector.jar
|
||||
└── cache/ cacheDir(): всё, что можно удалить без потерь
|
||||
├── version_manifest.json versionManifestFile()
|
||||
├── java_runtimes.json javaCatalogFile()
|
||||
├── seasonal_builds.json seasonalCatalogFile()
|
||||
├── loader_<ключ>.json loaderCacheFile(key)
|
||||
├── loaders/ loaderDownloadDir(): installer.jar и журналы установщиков
|
||||
└── java/ javaDownloadDir(): архивы Temurin до распаковки
|
||||
```
|
||||
|
||||
Папка игры задаётся в настройках (`gameDir`). `defaultMinecraftDir()` используется, только если
|
||||
там пусто.
|
||||
|
||||
## ensureRootExists(&error)
|
||||
|
||||
Создаёт `rootDir`, `runtime`, `cache`, `cache/loaders`, `builds`, `java` и `cache/java`, а затем
|
||||
проверяет, что каждая из них действительно папка и в неё можно писать. `mkpath` возвращает `true`
|
||||
и для существующей папки без прав на запись, и тогда, когда на месте папки лежит файл. Без этой
|
||||
проверки сохранения потом молча не работали бы.
|
||||
|
||||
Вызывается при старте бэкенда, перед каждой записью JSON-файлов бэкенда, перед записью кэшей
|
||||
каталогов и перед установками. Остальные функции только собирают строки и ошибиться не могут.
|
||||
|
||||
`launcherpaths.cpp` подключает `localization.h` ради текста ошибки, заголовок зависит только от
|
||||
`QString`.
|
||||
|
||||
---
|
||||
|
||||
|
||||
+28
-89
@@ -1,96 +1,35 @@
|
||||
# main.cpp — точка входа
|
||||
# main.cpp
|
||||
|
||||
## Обзор
|
||||
Точка входа. Объектов предметной области здесь нет: `LauncherBackend` создаёт сам
|
||||
[Main.qml](../qml/Main.md), а `main()` только готовит приложение и загружает QML.
|
||||
|
||||
`Minecraft_launcher` — десктопный лаунчер Minecraft на Qt 6 Quick. Интерфейс написан на QML,
|
||||
вся работа — авторизация, скачивание версий, установка модлоадеров и Java, запуск игры — лежит в
|
||||
C++-классе [LauncherBackend](LauncherBackend.md) и его сервисах.
|
||||
## По порядку
|
||||
|
||||
`main.cpp` — стартовая последовательность приложения: здесь при необходимости инициализируется
|
||||
Qt WebEngine, создаётся объект приложения, читается каталог переводов, выбирается стиль
|
||||
Qt Quick Controls, загружается QML-модуль и запускается цикл событий. Файл намеренно короткий:
|
||||
ни одного объекта предметной области он не создаёт — всё, что нужно, QML заводит сам.
|
||||
1. **WebEngine.** Если лаунчер собран с Qt WebEngine (`LAUNCHER_HAS_WEBENGINE`),
|
||||
`QtWebEngineQuick::initialize()` вызывается до создания `QGuiApplication`. После создания
|
||||
приложения общий контекст OpenGL уже не выставить.
|
||||
2. **`QGuiApplication`**, а не `QApplication`: виджетов в проекте нет.
|
||||
3. **Имя и версия.** `applicationName` = `BiLauncher`, `applicationVersion` = `LAUNCHER_VERSION`
|
||||
(берётся из `project(... VERSION)` в CMake), организация `GaleonDev`, домен `galeonworx.com`. На
|
||||
пути к данным лаунчера это не влияет, см. [launcherpaths.h](launcherpaths.md).
|
||||
4. **Иконка окна.** На Windows и Linux в `QIcon` складываются все PNG из ресурса `:/icons/`
|
||||
(16–512 и `@2x`). На macOS этот шаг пропущен: там иконку берут из `icon.icns` внутри бандла.
|
||||
5. **`setDesktopFileName("bilauncher")`.** По этому имени окружения рабочего стола в Linux
|
||||
связывают окно с `bilauncher.desktop` и показывают правильную иконку в панели.
|
||||
6. **Тексты.** `Localization::instance().load()` читает `i18n/translations.json` и язык из
|
||||
`settings.json`. Это нужно сделать до создания движка: язык должен быть известен до первой
|
||||
привязки. Если каталог не загрузился, `main()` возвращает `-1` (см.
|
||||
[Localization](Localization.md)).
|
||||
7. **Стиль `Basic`.** Окна оформлены вручную, а нативный стиль Windows игнорирует свои
|
||||
`contentItem` и `background` и засыпает журнал предупреждениями.
|
||||
8. **Движок.** `QQmlApplicationEngine`. Если корневой объект не создался, приложение
|
||||
завершается с `-1`. Соединение отложенное (`Qt::QueuedConnection`), чтобы выход произошёл уже в
|
||||
цикле событий, а не посреди загрузки.
|
||||
9. `engine.loadFromModule("BiLauncher", "Main")` — загрузка по URI модуля и имени типа.
|
||||
10. `app.exec()`.
|
||||
|
||||
## Настройка приложения Qt
|
||||
|
||||
Создаётся `QGuiApplication` — не `QApplication`: интерфейс целиком на Qt Quick, виджеты не
|
||||
используются, и модуль Qt Widgets в проект не подключён.
|
||||
|
||||
До создания приложения, при сборке с Qt WebEngine, вызывается `QtWebEngineQuick::initialize()`.
|
||||
Порядок здесь принципиален: инициализация выставляет общий контекст OpenGL, а после создания
|
||||
объекта приложения это уже не действует. Вызов обёрнут в условную компиляцию по макросу
|
||||
`LAUNCHER_HAS_WEBENGINE`.
|
||||
|
||||
## Каталог переводов
|
||||
|
||||
Сразу после создания приложения вызывается `Localization::instance().load()` — он читает
|
||||
`i18n/translations.json` из ресурсов и определяет язык интерфейса. Порядок важен: язык нужно
|
||||
знать до того, как QML вычислит первую привязку, а [LauncherBackend](LauncherBackend.md),
|
||||
который ведёт `settings.json`, появляется только вместе с движком — поэтому ключ `language`
|
||||
[Localization](Localization.md) читает из файла сам.
|
||||
|
||||
Неудача — не повод продолжать: каталог вкомпилирован в бинарник, значит его отсутствие или
|
||||
поломка означают ошибку сборки. `main()` пишет причину и возвращает `-1`.
|
||||
|
||||
## Стиль Qt Quick Controls
|
||||
|
||||
Стиль Qt Quick Controls принудительно выставляется в `Basic`
|
||||
вызовом `QQuickStyle::setStyle()`. Причина в оформлении: всё окно лаунчера стилизовано вручную, а
|
||||
нативный стиль Windows игнорирует пользовательские `contentItem` и `background` и сыплет
|
||||
предупреждениями.
|
||||
|
||||
## Обработка командной строки
|
||||
|
||||
Аргументы командной строки не разбираются: `argc` и `argv` передаются в конструктор
|
||||
`QGuiApplication` и дальше не используются. Ни `QCommandLineParser`, ни собственного разбора в
|
||||
файле нет.
|
||||
|
||||
## Создание объектов верхнего уровня
|
||||
|
||||
В `main()` создаётся ровно два объекта.
|
||||
|
||||
| Объект | Тип | Роль |
|
||||
|--------|-----|------|
|
||||
| `app` | `QGuiApplication` | объект приложения и цикл событий |
|
||||
| `engine` | `QQmlApplicationEngine` | загружает и исполняет QML-модуль лаунчера |
|
||||
|
||||
Экземпляр `LauncherBackend` здесь не создаётся: тип зарегистрирован через `QML_ELEMENT`, и главное
|
||||
окно объявляет его само декларативно. Поэтому в `main.cpp` нет ни одного `#include` классов
|
||||
предметной области.
|
||||
|
||||
## Связывание и подключения
|
||||
|
||||
Единственное подключение — обработка неудачи создания корневого объекта: сигнал
|
||||
`QQmlApplicationEngine::objectCreationFailed` замыкается на лямбду, которая завершает приложение
|
||||
с кодом `-1`. Соединение создаётся с типом `Qt::QueuedConnection` и с объектом `app` в роли
|
||||
контекста, чтобы выход из приложения происходил уже внутри цикла событий, а не в разгар загрузки
|
||||
QML.
|
||||
|
||||
Контекстные свойства не задаются, начальные свойства корневому объекту не передаются: связь между
|
||||
QML и C++ идёт исключительно через зарегистрированный тип.
|
||||
|
||||
## Цикл событий
|
||||
|
||||
QML загружается вызовом `engine.loadFromModule("Minecraft_launcher", "Main")` — по URI модуля и
|
||||
имени типа, а не по пути к файлу. Модуль объявлен в `CMakeLists.txt` через `qt_add_qml_module`, а
|
||||
`Main` — это [Main.qml](../qml/Main.md), корневой элемент которого `Window` с `visible: true`,
|
||||
поэтому окно показывается само.
|
||||
|
||||
Цикл событий запускается `app.exec()`, его результат возвращается из `main()` как код завершения
|
||||
процесса.
|
||||
|
||||
## Зависимости
|
||||
|
||||
| Заголовок | Что даёт |
|
||||
|-----------|----------|
|
||||
| `QGuiApplication` | объект приложения и цикл событий для приложения без виджетов |
|
||||
| `QQmlApplicationEngine` | загрузка QML-модуля и создание корневого объекта |
|
||||
| `QQuickStyle` | выбор стиля Qt Quick Controls до загрузки QML |
|
||||
| `QtWebEngineQuick` | инициализация WebEngine; подключается только при сборке с Qt WebEngine |
|
||||
|
||||
Модули сборки: `Qt6::Quick`, `Qt6::QuickControls2`, `Qt6::Core`, `Qt6::CorePrivate`, `Qt6::Gui`,
|
||||
`Qt6::Network` и опционально `Qt6::WebEngineQuick`. Макрос `LAUNCHER_HAS_WEBENGINE` определяется в
|
||||
`CMakeLists.txt` только тогда, когда `find_package` нашёл `Qt6WebEngineQuick`.
|
||||
Аргументы командной строки не разбираются. Контекстных свойств нет: QML общается с C++ только
|
||||
через зарегистрированные типы `LauncherBackend` и `Loc`.
|
||||
|
||||
---
|
||||
|
||||
|
||||
+74
-107
@@ -1,135 +1,102 @@
|
||||
# minecraftversion.h — MinecraftVersion и VersionLoader
|
||||
# minecraftversion.h
|
||||
|
||||
## Обзор
|
||||
`minecraftversion.h`, `minecraftversion.cpp`
|
||||
|
||||
Каждая версия Minecraft описывается файлом `versions/<id>/<id>.json` — в нём главный класс,
|
||||
аргументы запуска, список библиотек, индекс ресурсов и требуемая версия Java. Файлы образуют
|
||||
цепочку: профиль модлоадера наследуется от версии игры через поле `inheritsFrom`, а та, в свою
|
||||
очередь, может наследоваться дальше.
|
||||
Чтение описаний версий из `<gameDir>/versions/<id>/<id>.json` и всё, что с ними связано: правила
|
||||
Mojang, нативные библиотеки под текущую платформу, список установленного, удаление.
|
||||
|
||||
Заголовок `minecraftversion.h` даёт две вещи: структуры, описывающие версию в разобранном виде, и
|
||||
пространство имён `VersionLoader` — чтение, разворачивание цепочки наследования и работа с
|
||||
установленными версиями на диске.
|
||||
Профиль модлоадера обычно содержит только отличия от базовой версии и ссылается на неё через
|
||||
`inheritsFrom`. `VersionLoader::load()` проходит эту цепочку и сводит её в одну структуру
|
||||
`MinecraftVersion`, с которой уже работают [GameLauncher](GameLauncher.md) и
|
||||
[VersionInstaller](VersionInstaller.md).
|
||||
|
||||
Заголовок подключают [GameLauncher](GameLauncher.md) (собирает из версии командную строку),
|
||||
[VersionInstaller](VersionInstaller.md) (по списку библиотек понимает, что качать) и
|
||||
[LauncherBackend](LauncherBackend.md) (проверяет комплектность и удаляет версии).
|
||||
## MinecraftLibrary
|
||||
|
||||
## Типы
|
||||
Одна библиотека, к которой уже применены правила.
|
||||
|
||||
| Имя | Вид | Описание |
|
||||
|-----|-----|----------|
|
||||
| `MinecraftLibrary` | `struct` | Одна библиотека из `client.json` с уже разрешёнными правилами |
|
||||
| `MinecraftVersion` | `struct` | `client.json`, «схлопнутый» по всей цепочке `inheritsFrom` |
|
||||
| Поле | Что это |
|
||||
|------|---------|
|
||||
| `name` | Maven-координаты, например `org.lwjgl:lwjgl:3.3.1:natives-windows` |
|
||||
| `path` | путь относительно `libraries/` |
|
||||
| `url` | откуда качать; может быть пустым |
|
||||
| `sha1`, `size` | для проверки; могут отсутствовать |
|
||||
| `native` | распаковывается в `natives`, в classpath не идёт |
|
||||
| `extractExclude` | префиксы путей внутри jar, которые не распаковываются |
|
||||
|
||||
### MinecraftLibrary
|
||||
## MinecraftVersion
|
||||
|
||||
| Поле | Тип | Описание |
|
||||
|------|-----|----------|
|
||||
| `name` | `QString` | Maven-координаты, например `org.lwjgl:lwjgl:3.3.1:natives-windows` |
|
||||
| `path` | `QString` | Путь относительно `.minecraft/libraries` |
|
||||
| `url` | `QString` | Откуда качать, если файла нет; может быть пустым — тогда файл должен уже лежать на месте |
|
||||
| `sha1` | `QString` | Контрольная сумма для проверки скачанного |
|
||||
| `size` | `qint64` | Размер в байтах; `0` — неизвестен |
|
||||
| `native` | `bool` | Библиотека распаковывается в `natives`, а не кладётся в classpath |
|
||||
| `extractExclude` | `QStringList` | Префиксы путей внутри архива, которые не распаковываются |
|
||||
| Поле | Что это |
|
||||
|------|---------|
|
||||
| `id` | имя папки в `versions/` |
|
||||
| `mainClass` | |
|
||||
| `type` | `release`, `snapshot` и т. д.; по умолчанию `release` |
|
||||
| `assetIndexId` | id индекса ресурсов |
|
||||
| `assetsKind` | поле `assets`: `legacy`, `pre-1.6` или тот же id |
|
||||
| `clientJarPath` | абсолютный путь к клиентскому jar; часто это jar родителя |
|
||||
| `javaMajor` | из `javaVersion.majorVersion`; по умолчанию 8 |
|
||||
| `jvmArgs`, `gameArgs` | с ещё не подставленными `${...}` |
|
||||
| `libraries` | |
|
||||
| `loggingArgument`, `loggingConfigPath` | аргумент log4j и путь к его xml в `assets/log_configs` |
|
||||
| `supportsQuickPlay` | среди аргументов игры есть `quickPlayMultiplayer` (1.20+) |
|
||||
| `hasCustomResolutionArgs` | описание само передаёт `resolution_width` |
|
||||
|
||||
### MinecraftVersion
|
||||
`isValid()` возвращает `true`, если заданы `id` и `mainClass`.
|
||||
|
||||
| Поле | Тип | Описание |
|
||||
|------|-----|----------|
|
||||
| `id` | `QString` | Имя папки в `versions`, оно же значение аргумента `--version` |
|
||||
| `mainClass` | `QString` | Главный класс, который запускает java |
|
||||
| `type` | `QString` | `release`, `snapshot` или `modified`; идёт в `--versionType` |
|
||||
| `assetIndexId` | `QString` | Идентификатор индекса ресурсов для `--assetIndex` |
|
||||
| `assetsKind` | `QString` | Поле `assets` версии: `legacy`, `pre-1.6` либо тот же идентификатор |
|
||||
| `clientJarPath` | `QString` | Абсолютный путь к `<id>.jar`; может лежать у родительской версии |
|
||||
| `javaMajor` | `int` | Требуемая мажорная версия Java; по умолчанию `8` |
|
||||
| `jvmArgs` | `QStringList` | Аргументы JVM ещё с неподставленными подстановками вида `${...}` |
|
||||
| `gameArgs` | `QStringList` | Аргументы игры, тоже с неподставленными подстановками |
|
||||
| `libraries` | `QList<MinecraftLibrary>` | Библиотеки версии с уже применёнными правилами |
|
||||
| `loggingArgument` | `QString` | Аргумент вида `-Dlog4j.configurationFile=${path}` |
|
||||
| `loggingConfigPath` | `QString` | Абсолютный путь к xml-конфигурации журнала; пуст, если конфигурации нет |
|
||||
| `supportsQuickPlay` | `bool` | Версия 1.20 и новее понимает `--quickPlayMultiplayer` — так лаунчер подключается к серверу сразу при запуске |
|
||||
| `hasCustomResolutionArgs` | `bool` | Версия принимает аргументы размера окна |
|
||||
## Как сводится цепочка
|
||||
|
||||
Метод `isValid()` возвращает `true`, когда заполнены и `id`, и `mainClass`: именно этим проверяется
|
||||
успешность чтения версии.
|
||||
`load(gameDir, versionId, features, &error)` идёт от запрошенной версии к родителям по
|
||||
`inheritsFrom`. Ограничения: не больше 10 звеньев, без циклов. Если файл отсутствует или не
|
||||
разбирается, это ошибка, и возвращается невалидный объект.
|
||||
|
||||
## Функции
|
||||
Дальше поля применяются от корня к потомку:
|
||||
|
||||
Пространство имён `VersionLoader`.
|
||||
- `mainClass`, `type`, `assets`, `assetIndex`, `javaVersion`, `logging` — потомок перекрывает
|
||||
родителя;
|
||||
- `minecraftArguments` (старый формат, одна строка) **заменяет** `gameArgs` целиком;
|
||||
- `arguments.jvm` и `arguments.game` (новый формат) **дописываются** к тому, что дал родитель.
|
||||
|
||||
#### QStringList installedVersions(const QString &gameDir)
|
||||
Библиотеки собираются в обратном порядке, от потомка к корню. Повторы убираются по ключу
|
||||
`группа:артефакт[:классификатор]`, без версии. Поэтому если Forge приносит свою версию библиотеки,
|
||||
она вытесняет ванильную.
|
||||
|
||||
Версии, реально установленные в `<gameDir>/versions`: есть и папка, и файл `<id>.json`. Одной
|
||||
только папки недостаточно — она остаётся после неудачной установки.
|
||||
Если среди JVM-аргументов нет `-cp`, значит это старая версия без блока `arguments.jvm`. Тогда
|
||||
добавляются аргументы, которые официальный лаунчер подставляет сам: `-Djava.library.path`, бренд и
|
||||
версия лаунчера, `-cp ${classpath}`.
|
||||
|
||||
#### QStringList dependentsOf(const QString &gameDir, const QString &versionId)
|
||||
Клиентский jar берётся первый найденный по цепочке, от потомка к корню. Если его нет нигде,
|
||||
указывается путь у корня. `GameLauncher::missingFiles()` потом скажет, что его не хватает.
|
||||
|
||||
Установленные профили, у которых `inheritsFrom` равен `versionId`. Без базовой версии они не
|
||||
запустятся, поэтому список показывается пользователю перед удалением версии.
|
||||
`features` нужен для правил вида `"features": {"has_custom_resolution": true}`. Бэкенд передаёт
|
||||
`has_custom_resolution`, если в настройках задан размер окна.
|
||||
|
||||
#### qint64 installedSize(const QString &gameDir, const QString &versionId)
|
||||
## Правила и натив
|
||||
|
||||
Размер `<gameDir>/versions/<id>` в байтах; `0`, если папки нет. Используется в предупреждении об
|
||||
удалении — версия весит десятки мегабайт, и стоит показать, сколько освободится.
|
||||
`rulesAllow(rules, features)` реализует алгоритм Mojang: правила проверяются по порядку,
|
||||
действует последнее совпавшее; пустой список означает «разрешено». Правило совпадает, если подошли
|
||||
`features`, `os.name`, `os.arch` и регулярное выражение `os.version` (сверяется с
|
||||
`QSysInfo::productVersion()`).
|
||||
|
||||
#### bool remove(const QString &gameDir, const QString &versionId, QString \*error)
|
||||
Нативные библиотеки встречаются в двух форматах:
|
||||
|
||||
Сносит `<gameDir>/versions/<id>`. Отсутствие папки считается успехом. При неудаче возвращает
|
||||
`false` и заполняет `error`.
|
||||
- старый: блок `natives` с классификатором под ОС, `${arch}` заменяется на `32` или `64`;
|
||||
- новый (LWJGL 3.3+): отдельные библиотеки с классификатором `natives-<ос>[-<арх>]`.
|
||||
|
||||
Библиотеки и ресурсы в `libraries/` и `assets/` не трогаются: они общие для всех версий.
|
||||
`pickNativesForCurrentArch()` оставляет для каждого артефакта один классификатор. Лучше всего
|
||||
точный (`nativeClassifier()`, например `natives-macos-arm64`), потом общий для ОС
|
||||
(`natives-macos`), потом какой попадётся.
|
||||
|
||||
#### MinecraftVersion load(const QString &gameDir, const QString &versionId, const QSet<QString> &features, QString \*error)
|
||||
`osName()` возвращает `windows`, `osx` или `linux`. `osArch()` возвращает архитектуру из
|
||||
`QSysInfo`, причём `i386` и `i686` сводятся к `x86`.
|
||||
|
||||
Читает версию и разворачивает всю цепочку `inheritsFrom` в один объект. Параметр `features` —
|
||||
набор включённых возможностей, влияющих на применение правил (например, запрошен ли пользовательский
|
||||
размер окна).
|
||||
## Установленные версии
|
||||
|
||||
При ошибке возвращает объект, у которого `isValid()` даёт `false`, и заполняет `error`.
|
||||
|
||||
#### bool rulesAllow(const QJsonArray &rules, const QSet<QString> &features = {})
|
||||
|
||||
Стандартный алгоритм Mojang для блоков `rules`: правила применяются по порядку, побеждает последнее
|
||||
совпавшее. Используется и для библиотек, и для аргументов запуска.
|
||||
|
||||
#### QString nativeClassifier()
|
||||
|
||||
Классификатор нативных библиотек для текущей машины: `natives-windows`, `natives-macos-arm64`,
|
||||
`natives-linux` и подобные. По нему из списка библиотек отбираются те, что нужно распаковать.
|
||||
|
||||
#### QString osName()
|
||||
|
||||
Имя операционной системы в терминах Mojang — оно подставляется в правила и аргументы.
|
||||
|
||||
#### QString osArch()
|
||||
|
||||
Архитектура в терминах Mojang.
|
||||
|
||||
## Зависимости
|
||||
|
||||
Подключает `QJsonArray`, `QList`, `QSet`, `QString` и `QStringList` — только Qt Core. От классов
|
||||
проекта не зависит.
|
||||
|
||||
## Пример использования
|
||||
|
||||
```cpp
|
||||
QString error;
|
||||
const MinecraftVersion version = VersionLoader::load(gameDir, versionId, features, &error);
|
||||
if (!version.isValid()) {
|
||||
emit launchError(error);
|
||||
return;
|
||||
}
|
||||
|
||||
QStringList classpath;
|
||||
for (const MinecraftLibrary &library : version.libraries) {
|
||||
if (!library.native)
|
||||
classpath << gameDir + "/libraries/" + library.path;
|
||||
}
|
||||
classpath << version.clientJarPath;
|
||||
```
|
||||
- `installedVersions(gameDir)` — подпапки `versions/`, в которых есть `<id>.json`, по алфавиту.
|
||||
- `dependentsOf(gameDir, id)` — установленные профили с `inheritsFrom == id`. Без базовой версии они
|
||||
не запустятся, поэтому их перечисляют в предупреждении об удалении.
|
||||
- `installedSize(gameDir, id)` — размер папки в байтах.
|
||||
- `remove(gameDir, id, &error)` — удаляет `versions/<id>`. Сначала проверяет, что id не пустой,
|
||||
не начинается с точки и не содержит слэшей, а итоговый путь лежит внутри `versions/`. Если папки
|
||||
нет, это не ошибка.
|
||||
|
||||
---
|
||||
|
||||
|
||||
+29
-72
@@ -1,86 +1,43 @@
|
||||
# modloader.h — ModLoader
|
||||
# modloader.h
|
||||
|
||||
## Обзор
|
||||
Перечисление модлоадеров, запись одной версии модлоадера и перевод между перечислением и
|
||||
строковым ключом. Только заголовок, `.cpp` нет.
|
||||
|
||||
`Minecraft_launcher` умеет ставить четыре модлоадера: Minecraft Forge, Fabric Loader, NeoForge и
|
||||
Quilt Loader. Заголовок `modloader.h` — общий словарь для всей этой части проекта: перечисление
|
||||
самих лоадеров, описание одной их сборки и функции перевода между перечислением и строковым
|
||||
ключом.
|
||||
## ModLoader
|
||||
|
||||
Лоадеры взаимоисключающи: игра запускается ровно с одним профилем в `<gameDir>/versions`, поэтому в
|
||||
сборке лаунчера хранится один ключ лоадера, а не набор.
|
||||
`Forge`, `Fabric`, `NeoForge`, `Quilt`.
|
||||
|
||||
Заголовок подключают [ModLoaderVersionService](ModLoaderVersionService.md),
|
||||
[ModLoaderInstaller](ModLoaderInstaller.md) и [LauncherBackend](LauncherBackend.md).
|
||||
Модлоадеры взаимоисключающие: игра запускается с одним профилем из `versions/`, поэтому у сборки
|
||||
может быть не больше одного модлоадера. [LoaderRow](../qml/LoaderRow.md) снимает отметку с
|
||||
остальных, когда пользователь отмечает один.
|
||||
|
||||
## Типы
|
||||
## Ключи и названия
|
||||
|
||||
| Имя | Вид | Описание |
|
||||
|-----|-----|----------|
|
||||
| `ModLoader` | `enum class` | Модлоадеры, которые лаунчер умеет ставить |
|
||||
| `LoaderVersionEntry` | `struct` | Одна сборка модлоадера под конкретную версию игры |
|
||||
| Значение | `loaderKey()` | `loaderTitle()` |
|
||||
|----------|---------------|-----------------|
|
||||
| `Forge` | `forge` | Minecraft Forge |
|
||||
| `Fabric` | `fabric` | Fabric Loader |
|
||||
| `NeoForge` | `neoforge` | NeoForge |
|
||||
| `Quilt` | `quilt` | Quilt Loader |
|
||||
|
||||
### ModLoader
|
||||
Ключ используется в QML, в имени файла кэша (`cache/loader_<ключ>.json`) и в поле `loader` записи
|
||||
сборки в `customBuilds.json`. Если его поменять, старые сборки и кэши перестанут читаться.
|
||||
|
||||
| Значение | Ключ | Название | Описание |
|
||||
|----------|------|----------|----------|
|
||||
| `Forge` | `forge` | Minecraft Forge | Старейший загрузчик; ставится собственным установщиком, который собирает часть файлов на месте |
|
||||
| `Fabric` | `fabric` | Fabric Loader | Лёгкий загрузчик; профиль версии формируется из метаданных без запуска установщика |
|
||||
| `NeoForge` | `neoforge` | NeoForge | Ответвление Forge; ставится так же собственным установщиком |
|
||||
| `Quilt` | `quilt` | Quilt Loader | Ответвление Fabric; ставится так же, как Fabric |
|
||||
`loaderFromKey(key)` возвращает `std::nullopt` для любой другой строки, в том числе пустой. В
|
||||
бэкенде это значит «ванильная сборка».
|
||||
|
||||
Перечисление объявлено как `enum class`, поэтому неявного приведения к целому нет.
|
||||
Названия не переводятся: это имена собственные.
|
||||
|
||||
### LoaderVersionEntry
|
||||
## LoaderVersionEntry
|
||||
|
||||
| Поле | Тип | Описание |
|
||||
|------|-----|----------|
|
||||
| `loaderVersion` | `QString` | Версия самого лоадера — например `47.4.0`, `0.19.3` или `21.1.248` |
|
||||
| `gameVersion` | `QString` | Версия Minecraft, под которую эта сборка — например `1.20.1` |
|
||||
| `versionId` | `QString` | Предсказанный идентификатор профиля `versions/<id>`. У Forge пуст: он выясняется только после работы установщика |
|
||||
| `installerUrl` | `QUrl` | Адрес установщика; заполнен только у Forge и NeoForge |
|
||||
| `recommended` | `bool` | Сборка помечена авторами как рекомендуемая; по умолчанию `false` |
|
||||
| `stable` | `bool` | Сборка стабильна, а не тестовая; по умолчанию `true` |
|
||||
|
||||
## Функции
|
||||
|
||||
#### QString loaderKey(ModLoader loader)
|
||||
|
||||
Строковый ключ лоадера: `forge`, `fabric`, `neoforge` или `quilt`. Один и тот же ключ используется
|
||||
в трёх местах — в интерфейсе, в имени файла кэша на диске и в поле `loader` файла
|
||||
`customBuilds.json`, — поэтому менять его нельзя без миграции сохранённых сборок.
|
||||
|
||||
Объявлена `inline` в заголовке.
|
||||
|
||||
#### QString loaderTitle(ModLoader loader)
|
||||
|
||||
Человекочитаемое название лоадера для интерфейса: «Minecraft Forge», «Fabric Loader», «NeoForge»,
|
||||
«Quilt Loader». Объявлена `inline`.
|
||||
|
||||
#### std::optional<ModLoader> loaderFromKey(const QString &key)
|
||||
|
||||
Обратный перевод: ключ в перечисление. Возвращает `std::nullopt` для неизвестного ключа, в том
|
||||
числе для пустой строки — а пустая строка в сборке означает чистую ваниль без лоадера. Объявлена
|
||||
`inline`.
|
||||
|
||||
## Зависимости
|
||||
|
||||
Подключает `QString`, `QUrl` и `<optional>`. От классов проекта не зависит и сам подключается
|
||||
всюду, где речь идёт о модлоадерах.
|
||||
|
||||
## Пример использования
|
||||
|
||||
```cpp
|
||||
const auto loader = loaderFromKey(build.loader);
|
||||
if (!loader) {
|
||||
// сборка без модлоадера — запускаем чистую ваниль
|
||||
return;
|
||||
}
|
||||
|
||||
qInfo() << "ставим" << loaderTitle(*loader)
|
||||
<< "версии" << entry.loaderVersion
|
||||
<< "под Minecraft" << entry.gameVersion;
|
||||
```
|
||||
| Поле | Что это |
|
||||
|------|---------|
|
||||
| `loaderVersion` | `47.4.0`, `0.19.3`, `21.1.248` |
|
||||
| `gameVersion` | `1.20.1` |
|
||||
| `versionId` | ожидаемое имя профиля в `versions/`. У Fabric и Quilt оно известно заранее (`fabric-loader-<версия>-<игра>`), у Forge и NeoForge пусто: имя становится известно только после работы установщика |
|
||||
| `installerUrl` | ссылка на `installer.jar`, только у Forge и NeoForge |
|
||||
| `recommended` | у Forge помечено как recommended в `promotions_slim.json` |
|
||||
| `stable` | не альфа, не бета, не rc/pre |
|
||||
|
||||
---
|
||||
|
||||
|
||||
+40
-53
@@ -1,75 +1,62 @@
|
||||
# zlibreference.h — ZlibReference
|
||||
# zlibreference.h
|
||||
|
||||
## Обзор
|
||||
`zlibreference.h`, `zlibreference.cpp`
|
||||
|
||||
`Minecraft_launcher` умеет ставить модлоадеры Forge и NeoForge, а их установщики — это Java-программы,
|
||||
которые собирают часть jar-файлов прямо на машине пользователя и сверяют sha1 каждого собранного
|
||||
файла с эталоном из `install_profile.json`.
|
||||
Обход одной конкретной проблемы на Linux: Forge и NeoForge не ставятся на системах, где вместо
|
||||
zlib стоит zlib-ng.
|
||||
|
||||
Эталон посчитан на обычном zlib. Дистрибутивы вроде CachyOS и Fedora подставляют вместо него
|
||||
zlib-ng: сжатие корректное, но побайтово другое — поэтому установка падает на любой версии игры
|
||||
сообщением «Processor failed, invalid outputs». Сменить Java не выйдет: сборки OpenJDK под Linux
|
||||
берут `libz.so.1` из системы.
|
||||
## В чём проблема
|
||||
|
||||
`ZlibReference` — обход этой проблемы. Рядом с лаунчером лежит собранный обычный zlib, и процессу
|
||||
установщика он подсовывается через `LD_PRELOAD`. Системный zlib-ng при этом остаётся на месте:
|
||||
подмена живёт ровно один процесс.
|
||||
Установщики Forge и NeoForge собирают часть jar прямо на машине пользователя и сверяют sha1
|
||||
каждого собранного файла с эталоном из `install_profile.json`. Эталон посчитан на обычном zlib.
|
||||
CachyOS, Fedora и некоторые другие дистрибутивы ставят вместо него zlib-ng в режиме совместимости.
|
||||
Он сжимает корректно, но результат побайтово другой. Поэтому установка падает на любой версии игры
|
||||
с сообщением `Processor failed, invalid outputs`.
|
||||
|
||||
Пространство имён используется установщиком модлоадеров при подготовке окружения дочернего
|
||||
процесса Java.
|
||||
Другая Java не помогает: сборки OpenJDK под Linux берут `libz.so.1` из системы.
|
||||
|
||||
## Пространства имён
|
||||
## Что сделано
|
||||
|
||||
`ZlibReference` группирует три функции: проверку системной библиотеки, поиск собранного эталона и
|
||||
подготовку окружения процесса. Состояния нет.
|
||||
В репозитории лежат исходники zlib 1.3.1 (каталог `zlib/`). На Linux CMake собирает из них цель
|
||||
`launcher_zlib_reference` (`libz.so.1`). В дереве сборки она лежит в `<папка бинарника>/zlib/`, при
|
||||
установке — в `<prefix>/lib/BiLauncher/`. Сам лаунчер с ней не линкуется. Библиотека
|
||||
подкладывается через `LD_PRELOAD` только процессу установщика, системный zlib-ng не трогается.
|
||||
|
||||
Исходники лежат в репозитории, а не скачиваются при сборке, по двум причинам: сборка не зависит от
|
||||
сети, и версия зафиксирована. От версии зависит побайтовый результат сжатия.
|
||||
|
||||
## Функции
|
||||
|
||||
#### bool systemIsZlibNg()
|
||||
На Windows и macOS все три функции ничего не делают: `systemIsZlibNg()` возвращает `false`,
|
||||
`bundledPath()` пустую строку, `applyTo()` — `true`.
|
||||
|
||||
Отвечает на вопрос, окажется ли `libz.so.1`, который достанется процессу java, библиотекой zlib-ng.
|
||||
От ответа зависит, нужна ли подмена вообще: на системе с обычным zlib она бессмысленна.
|
||||
### systemIsZlibNg()
|
||||
|
||||
#### QString bundledPath()
|
||||
Открывает `libz.so.1` через `dlopen`, то есть ту же библиотеку, которую загрузчик выдаст процессу
|
||||
java, и смотрит, есть ли `zlib-ng` в строке `zlibVersion()`. Результат вычисляется один раз за
|
||||
запуск.
|
||||
|
||||
Путь к собранному рядом эталонному `libz.so.1` или пустая строка, если его нет. Библиотека
|
||||
собирается целью `launcher_zlib_reference` в `CMakeLists.txt` из исходников каталога `zlib/`,
|
||||
которые лежат в репозитории, чтобы сборка не зависела от сети, а версия была зафиксирована — от
|
||||
неё зависит побайтовый результат сжатия. Собирается только на Linux: на Windows и macOS проблемы
|
||||
подмены системного zlib нет.
|
||||
### bundledPath()
|
||||
|
||||
#### bool applyTo(QProcessEnvironment &env, QString *note = nullptr)
|
||||
Ищет эталонную библиотеку по порядку:
|
||||
|
||||
Дописывает `LD_PRELOAD` в переданное окружение, если подмена нужна. Изменяет `env` на месте.
|
||||
1. `<applicationDirPath>/zlib/libz.so.1` — запуск из дерева сборки;
|
||||
2. `<applicationDirPath>/../lib/BiLauncher/libz.so.1` — установленный пакет, tar.gz и AppImage;
|
||||
3. `LAUNCHER_ZLIB_INSTALL_DIR/libz.so.1` — путь, заданный при конфигурации CMake.
|
||||
|
||||
Возвращает `true`, когда окружение готово, — в том числе в случае, когда подменять нечего:
|
||||
система с обычным zlib или платформа, где вопрос не стоит. `false` означает, что подмена нужна, но
|
||||
эталонной библиотеки нет на месте.
|
||||
Второй вариант рассчитан на `CMAKE_INSTALL_LIBDIR=lib`. Поэтому CI на Ubuntu передаёт его явно,
|
||||
иначе библиотека легла бы в `lib/x86_64-linux-gnu`.
|
||||
|
||||
Необязательный параметр `note` заполняется строкой, пригодной и для журнала, и для текста ошибки:
|
||||
она объясняет, была ли подмена применена и почему.
|
||||
### applyTo(env, ¬e)
|
||||
|
||||
## Зависимости
|
||||
- zlib-ng в системе нет — подменять нечего, возвращает `true`, `note` пуст.
|
||||
- zlib-ng есть, эталон найден — дописывает его путь в начало `LD_PRELOAD` (чужое содержимое
|
||||
переменной сохраняется), возвращает `true`, в `note` пишет, что подставлено.
|
||||
- zlib-ng есть, эталона нет — возвращает `false`, в `note` пишет предупреждение.
|
||||
|
||||
Подключает `QString`; `QProcessEnvironment` объявлен вперёд и используется только по ссылке.
|
||||
Реализация опирается на макрос `LAUNCHER_ZLIB_INSTALL_DIR`, который `CMakeLists.txt` определяет на
|
||||
Linux — это путь установки библиотеки в системе, помимо каталога рядом с исполняемым файлом.
|
||||
|
||||
## Пример использования
|
||||
|
||||
```cpp
|
||||
QProcessEnvironment env = QProcessEnvironment::systemEnvironment();
|
||||
|
||||
QString note;
|
||||
if (!ZlibReference::applyTo(env, ¬e)) {
|
||||
emit failed(note);
|
||||
return;
|
||||
}
|
||||
|
||||
QProcess installer;
|
||||
installer.setProcessEnvironment(env);
|
||||
installer.start(javaPath, arguments);
|
||||
```
|
||||
[ModLoaderInstaller](ModLoaderInstaller.md) пишет `note` в журнал установщика. Если `applyTo()`
|
||||
вернула `false`, а установщик упал с ошибкой контрольных сумм, к ошибке добавляется прямая
|
||||
подсказка про zlib-ng.
|
||||
|
||||
---
|
||||
|
||||
|
||||
Reference in new issue
Block a user