1 parent
1943996608
commit
f10b654953
34 files changed
+2099
-4576
No files matched your search
+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, если его там ещё нет.
|
||||
|
||||
---
|
||||
|
||||
|
||||
Reference in new issue
Block a user