diff --git a/doc/.DS_Store b/doc/.DS_Store new file mode 100644 index 0000000..3953cb1 Binary files /dev/null and b/doc/.DS_Store differ diff --git a/doc/cpp/AuthService.md b/doc/cpp/AuthService.md new file mode 100644 index 0000000..8038788 --- /dev/null +++ b/doc/cpp/AuthService.md @@ -0,0 +1,171 @@ +# AuthService + +## Обзор класса + +`Minecraft_launcher` поддерживает три способа входа: офлайн-профиль без пароля, учётную запись +Ely.by и учётную запись Microsoft. `AuthService` закрывает первые два: это Yggdrasil-клиент Ely.by +плюс офлайн-режим. За третий отвечает [MsaAuthService](MsaAuthService.md). + +Результат любого способа — структура `AuthResult`, объявленная в этом же заголовке. Она содержит +ровно то, что подставляется в аргументы запуска вида `${auth_*}`, поэтому дальше запуск игры идёт +по общему пути независимо от того, как пользователь вошёл. + +Класс также умеет скачивать `authlib-injector` — библиотеку, которая перенаправляет обращения игры +к серверу авторизации на Ely.by. + +## Место в проекте и зависимости + +Экземпляр создаётся и принадлежит [LauncherBackend](LauncherBackend.md); других владельцев нет. +Заголовок подключает [MsaAuthService](MsaAuthService.md) — ради общей структуры `AuthResult` — и +[GameLauncher](GameLauncher.md) через поля `LaunchOptions`, которые заполняются из результата +авторизации. + +Требования сборки: `Qt6::Core` (`QDateTime`, `QJsonObject`, `QString`) и `Qt6::Network` +(`QNetworkAccessManager`). + +## Иерархия и роль + +Наследует `QObject`: даёт мета-объектную систему, сигнал `progress` и владение по родителю. +Виртуальных методов базового класса не переопределяет. + +## Публичные структуры + +### AuthResult + +Результат авторизации — то, что подставляется в `${auth_*}` аргументы запуска. + +| Поле | Тип | Описание | +|------|-----|----------| +| `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` | + +## Псевдонимы типов + +`AuthService::Callback` — `std::function`. Все сетевые методы +асинхронные: колбэк вызывается ровно один раз и всегда в потоке GUI. + +## Публичные методы + +#### explicit AuthService(QObject \*parent = nullptr) + +Создаёт сервис и его `QNetworkAccessManager`. Конструктор помечен `explicit`. + +#### static AuthResult offline(const QString &nickname) + +Готовит результат для офлайн-профиля без единого сетевого запроса. UUID выводится из ника так же, +как это делает сам Minecraft в офлайне: `UUID.nameUUIDFromBytes(("OfflinePlayer:" + name) +.getBytes(UTF_8))`. Благодаря этому один и тот же ник всегда даёт один и тот же UUID, и прогресс +на сервере не теряется. + +#### static QString generateClientToken() + +Случайный `clientToken` лаунчера. Генерируется один раз на профиль и хранится вместе с ним: +Yggdrasil связывает выданный `accessToken` именно с этим значением. + +#### void loginElyBy(const QString &login, const QString &password, const QString &clientToken, const QString &accessToken, Callback callback) + +Полный цикл входа в Ely.by: сначала проверка имеющегося токена, затем его продление, и только при +неудаче — авторизация по паролю. Пароль можно оставить пустым, если уже есть рабочий +`accessToken`, — тогда пользователю не придётся вводить его заново. + +По ходу работы испускает `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); + }); +``` + +--- + +При создании этого документа использовался ИИ. diff --git a/doc/cpp/BuildArchiveWorker.md b/doc/cpp/BuildArchiveWorker.md new file mode 100644 index 0000000..e22eeb7 --- /dev/null +++ b/doc/cpp/BuildArchiveWorker.md @@ -0,0 +1,166 @@ +# BuildArchiveWorker + +## Обзор класса + +Смена сборки в лаунчере — это перекладывание содержимого `.minecraft`: текущее упаковывается в +архив, папка вычищается, на её место распаковывается архив другой сборки. Речь о гигабайтах модов, +конфигов и миров, и гонять их в потоке GUI нельзя — окно замерзало бы на всё время смены. + +`BuildArchiveWorker` — исполнитель этих операций в отдельном потоке. Он умеет четыре вещи: +упаковать, вычистить, распаковать и докатить пак сезонной сборки поверх уже разложенного +содержимого. + +Класс намеренно ничего не знает ни о сборках, ни о путях лаунчера: он принимает готовые пути и +списки. Порядок шагов и восстановление после сбоя — дело [BuildSwitcher](BuildSwitcher.md). + +## Место в проекте и зависимости + +Создаётся и целиком управляется [BuildSwitcher](BuildSwitcher.md), который же и переносит его в +собственный `QThread`. Больше к классу никто не обращается. + +Требования сборки: `Qt6::Core` и `Qt6::CorePrivate` — последний нужен ради `QZipReader` и +`QZipWriter`, которыми читаются и пишутся архивы сборок. + +## Иерархия и роль + +Наследует `QObject`: мета-объектная система, слоты, сигналы и возможность жить в отдельном потоке +через `moveToThread()`. Виртуальных методов базового класса не переопределяет. + +Объект живёт в своём `QThread` и вызывается только через очередь сигналов — прямых вызовов его +слотов из потока GUI быть не должно. + +## Публичные методы + +#### explicit BuildArchiveWorker(QObject \*parent = nullptr) + +Создаёт исполнителя. Конструктор помечен `explicit`. + +#### void requestCancel() + +Просит прервать текущую операцию. Вызывается из потока GUI прямо во время работы — это +единственный метод класса, предназначенный для вызова снаружи рабочего потока. + +Отмена не прерывает операцию мгновенно: признак проверяется между файлами. Хранится он в +`QAtomicInt`, поэтому запись из одного потока и чтение из другого безопасны без блокировок. + +#### 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)); +``` + +--- + +При создании этого документа использовался ИИ. diff --git a/doc/cpp/BuildSwitcher.md b/doc/cpp/BuildSwitcher.md new file mode 100644 index 0000000..ba79429 --- /dev/null +++ b/doc/cpp/BuildSwitcher.md @@ -0,0 +1,210 @@ +# BuildSwitcher + +## Обзор класса + +`BuildSwitcher` меняет активную сборку: содержимое `.minecraft` уезжает в архив своей сборки, +папка чистится, на её место разворачивается архив выбранной. Он же докатывает пак сезонной +сборки поверх уже разложенного содержимого. + +Порядок шагов подчинён одному правилу: пока новый архив не записан целиком и не переименован на +место, из `.minecraft` не удаляется ничего. Отметка о начатом переключении пишется в +`builds/index.json` до первого разрушающего действия, поэтому обрыв питания или принудительное +завершение процесса всегда обнаружим на следующем запуске. + +Сами файловые операции выполняет [BuildArchiveWorker](BuildArchiveWorker.md) в отдельном потоке; +`BuildSwitcher` — это порядок шагов, учёт состояния и восстановление после сбоя. + +## Место в проекте и зависимости + +Экземпляр создаётся и принадлежит [LauncherBackend](LauncherBackend.md). Класс создаёт и целиком +владеет своим `QThread` и объектом [BuildArchiveWorker](BuildArchiveWorker.md). + +Пути к папкам сборок и файлу состояния даёт [launcherpaths.h](launcherpaths.md) +(`buildStorageDir()`, `buildDir()`). + +Требования сборки: `Qt6::Core` (`QThread`, `QJsonObject`) и `Qt6::CorePrivate` — опосредованно, +через исполнителя. + +## Иерархия и роль + +Наследует `QObject`: мета-объектная система, три сигнала и владение по родителю. Объявлен +виртуальный деструктор — класс обязан корректно остановить свой поток. + +## Публичные методы + +#### explicit BuildSwitcher(QObject \*parent = nullptr) + +Создаёт переключатель, рабочий поток и исполнителя, переносит второй в первый и запускает поток. +Конструктор помечен `explicit`. + +#### bool isRunning() const + +Идёт ли сейчас переключение или раскатка пака. + +#### QString stage() const + +Текущий этап словами — заголовок панели прогресса. + +#### QString status() const + +Строка состояния: что обрабатывается сейчас. + +#### double fraction() const + +Доля выполнения от `0` до `1` либо `-1`, пока итог неизвестен. + +#### static QStringList sharedTopLevel() + +Общие для всех сборок каталоги верхнего уровня. Они не архивируются и не чистятся: принадлежат +лаунчеру, а не какой-либо сборке. Список передаётся исполнителю как `excludeTop` при упаковке и +как `keepTop` при очистке. + +#### void switchTo(int fromBuildId, const QString &fromName, int toBuildId, const QString &toName, const QString &gameDir) + +Переключает активную сборку. Проходит четыре этапа: упаковка текущего содержимого во временный +файл, фиксация архива переименованием на место, очистка папки игры и распаковка архива целевой +сборки. + +Имена сборок нужны для текста в панели прогресса, идентификаторы — для путей к архивам. + +#### 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); +} +``` + +--- + +При создании этого документа использовался ИИ. diff --git a/doc/cpp/GameLauncher.md b/doc/cpp/GameLauncher.md new file mode 100644 index 0000000..da007a2 --- /dev/null +++ b/doc/cpp/GameLauncher.md @@ -0,0 +1,200 @@ +# GameLauncher + +## Обзор класса + +`GameLauncher` — то, ради чего существует весь остальной лаунчер: он готовит и запускает JVM с +Minecraft. К моменту его вызова уже известно всё — версия разобрана, файлы скачаны, пользователь +авторизован, — и класс превращает это в командную строку и дочерний процесс. + +Кроме самого запуска класс умеет проверять комплектность `.minecraft` и распаковывать нативные +библиотеки, без которых игра не стартует. + +Один экземпляр — одна игра одновременно. + +## Место в проекте и зависимости + +Подключает [minecraftversion.h](minecraftversion.md): разобранная версия — половина входных данных +запуска, вторая половина приходит структурой `LaunchOptions` из этого же заголовка. + +Экземпляр создаётся и принадлежит [LauncherBackend](LauncherBackend.md), который заполняет +`LaunchOptions` из настроек, выбранной сборки и результата авторизации. + +Требования сборки: `Qt6::Core` (`QProcess`, `QStringList`) и `Qt6::CorePrivate` — последний нужен +ради `QZipReader`, которым распаковываются нативные библиотеки LWJGL. Зависимость от приватного +модуля привязывает проект к конкретной версии Qt; в `CMakeLists.txt` это осознанный выбор, и +предупреждение о нём отключено. + +## Иерархия и роль + +Наследует `QObject`: мета-объектная система, четыре сигнала и владение по родителю. Виртуальных +методов базового класса не переопределяет. + +## Публичные структуры + +### LaunchOptions + +Всё, что лаунчер знает к моменту нажатия кнопки запуска. + +| Поле | Тип | По умолчанию | Описание | +|------|-----|--------------|----------| +| `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` | `KishkaLauncher` | Имя лаунчера, которое видит игра | +| `launcherVersion` | `QString` | `1.0` | Версия лаунчера | + +## Публичные методы + +#### explicit GameLauncher(QObject \*parent = nullptr) + +Создаёт объект. Процесс игры при этом не запускается. Конструктор помечен `explicit`. + +#### bool isRunning() const + +Идёт ли сейчас игра. От этого зависит доступность кнопки запуска в интерфейсе. + +#### static QStringList missingFiles(const LaunchOptions &options, const MinecraftVersion &version, int limit = 12) + +Проверяет `.minecraft` на комплектность и возвращает описания недостающих файлов. Пустой список +означает, что всё на месте и сборку можно запускать. + +Параметр `limit` ограничивает длину списка: перечислять все отсутствующие файлы у неустановленной +версии бессмысленно, важен сам факт и пара примеров. + +Метод статический, ничего не меняет и вызывается интерфейсом для строки состояния сборки. + +#### static QStringList buildArguments(const LaunchOptions &options, const MinecraftVersion &version, const QString &nativesDir) + +Собирает аргументы ровно в том порядке, в котором их ждёт 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); +``` + +--- + +При создании этого документа использовался ИИ. diff --git a/doc/cpp/JavaInstaller.md b/doc/cpp/JavaInstaller.md new file mode 100644 index 0000000..dbb798e --- /dev/null +++ b/doc/cpp/JavaInstaller.md @@ -0,0 +1,201 @@ +# JavaInstaller + +## Обзор класса + +`JavaInstaller` ставит сборку Java в `/java/`. Как и +[ModLoaderInstaller](ModLoaderInstaller.md), он прячет за одним фасадом два разных пути. + +**Temurin** отдаёт один архив: лаунчер качает его, сверяет sha256 и распаковывает. **Mojang** +отдаёт манифест с деревом файлов: лаунчер качает файлы по отдельности, как это делает официальный +лаунчер. + +Набор геттеров прогресса повторяет [VersionInstaller](VersionInstaller.md): панель загрузки в +интерфейсе читает их одинаково, независимо от того, кто сейчас работает. + +## Место в проекте и зависимости + +Подключает [javaruntime.h](javaruntime.md): на вход установщик принимает запись каталога +`JavaRuntimeEntry`, а результат записывает через `JavaRuntimeStore`. + +Экземпляр создаётся и принадлежит [LauncherBackend](LauncherBackend.md); записи каталога приходят +от [JavaRuntimeService](JavaRuntimeService.md). + +Требования сборки: `Qt6::Core` (`QCryptographicHash`, `QSaveFile`, `QProcess`, `QTimer`), +`Qt6::CorePrivate` (`QZipReader` для распаковки zip) и `Qt6::Network`. + +## Иерархия и роль + +Наследует `QObject`: мета-объектная система, пять сигналов и владение по родителю. Объявлен +виртуальный деструктор — класс владеет незавершёнными загрузками, открытым архивом и процессом +распаковки. + +## Публичные структуры + +### JavaFileTask + +Один файл рантайма Mojang. + +| Поле | Тип | По умолчанию | Описание | +|------|-----|--------------|----------| +| `url` | `QUrl` | — | Откуда качать | +| `path` | `QString` | — | Абсолютный путь назначения | +| `sha1` | `QString` | — | Контрольная сумма файла | +| `size` | `qint64` | `0` | Размер в байтах | +| `executable` | `bool` | `false` | Файлу нужно выставить право на исполнение — иначе `bin/java` не запустится | +| `attempts` | `int` | `0` | Сколько попыток уже сделано | + +### JavaActiveDownload + +Файл в процессе скачивания: задача, сетевой ответ, открытый `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`, состояние распаковки — как `std::unique_ptr`, +активные загрузки — как `std::shared_ptr` с собственным `QSaveFile` внутри. +Незавершённая запись отменяется вместе с уничтожением объекта, и испорченный файл не попадает на +место назначения. + +Процесс `tar`, используемый для распаковки архивов `tar.gz`, создаётся по ходу работы и +завершается в деструкторе. + +## Потокобезопасность + +Только поток GUI. Отдельного потока у класса нет намеренно: распаковка zip идёт по кускам по +таймеру — держать поток GUI занятым на всю сотню мегабайт нельзя, а заводить поток ради одной +операции незачем. Распаковка `tar.gz` отдана внешнему процессу, который работает параллельно сам. + +## Взаимодействие с другими классами + +`LauncherBackend` вызывает `install()` с записью, полученной от +[JavaRuntimeService](JavaRuntimeService.md), и переправляет сигналы прогресса в те же свойства, +что и остальные установщики. Сигнал `finished` бэкенд переправляет в QML под собственным именем — +на него подписан диалог настроек, чтобы обновить строку выбранной сборки, когда та докачается. + +Раскладку папки `/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); +``` + +--- + +При создании этого документа использовался ИИ. diff --git a/doc/cpp/JavaRuntimeService.md b/doc/cpp/JavaRuntimeService.md new file mode 100644 index 0000000..2aea5c5 --- /dev/null +++ b/doc/cpp/JavaRuntimeService.md @@ -0,0 +1,156 @@ +# JavaRuntimeService + +## Обзор класса + +`JavaRuntimeService` — каталог сборок Java, которые лаунчер умеет скачать: качает, кэширует в +папке лаунчера и отдаёт из кэша, пока тот не устарел. Устроен так же, как +[VersionManifestService](VersionManifestService.md). + +Источников два, и они дополняют друг друга. **Mojang** (категория «Java») — ровно тот рантайм, +которым запускает игру официальный лаунчер: версий немного, зато они заведомо совместимы. +**Eclipse Temurin** (категории JDK и JRE) — свежие сборки всех мажорных версий, включая те, до +которых Mojang ещё не дошёл. + +Отдаются только сборки под текущие операционную систему и архитектуру: выбрать заведомо +неработающую нечем. Исключение — macOS на Apple Silicon, где под Java 8 и 16 сборок `aarch64` нет +вовсе и приходится брать `x64`, который работает через Rosetta. + +## Место в проекте и зависимости + +Подключает [javaruntime.h](javaruntime.md): перечисление `JavaRuntimeKind` и структура +`JavaRuntimeEntry` приходят оттуда. + +Экземпляр создаётся и принадлежит [LauncherBackend](LauncherBackend.md). Записями каталога +пользуется [JavaInstaller](JavaInstaller.md). + +Путь к файлу кэша даёт `LauncherPaths::javaCatalogFile()` из [launcherpaths.h](launcherpaths.md). + +Требования сборки: `Qt6::Core` (`QDateTime`, `QHash`, `QSet`) и `Qt6::Network` +(`QNetworkAccessManager`). + +## Иерархия и роль + +Наследует `QObject`: мета-объектная система, два сигнала и владение по родителю. Виртуальных +методов базового класса не переопределяет. + +## Псевдонимы типов + +`JavaRuntimeService::Callback` — `std::function`. Как и в +остальных каталогах лаунчера, `ok == true` с непустым `warning` означает данные из устаревшего +кэша. + +## Публичные методы + +#### explicit JavaRuntimeService(QObject \*parent = nullptr) + +Создаёт сервис и его `QNetworkAccessManager`. Кэш читается лениво. Конструктор помечен `explicit`. + +#### void ensureLoaded(Callback callback, bool forceRefresh = false) + +Единственная точка входа. Свежий кэш означает отложенный вызов колбэка без сети; иначе запускается +обновление. Параметр `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); +}); +``` + +--- + +При создании этого документа использовался ИИ. diff --git a/doc/cpp/LauncherBackend.md b/doc/cpp/LauncherBackend.md new file mode 100644 index 0000000..c89d33d --- /dev/null +++ b/doc/cpp/LauncherBackend.md @@ -0,0 +1,541 @@ +# LauncherBackend + +## Обзор класса + +`LauncherBackend` — единственный класс проекта, видимый из QML, и центр всего приложения. Интерфейс +лаунчера ничего не знает ни о сети, ни о файлах, ни о процессах: он читает свойства этого класса, +вызывает его методы и слушает его сигналы. + +Сам бэкенд почти ничего не делает руками. Он владеет двенадцатью сервисами — авторизация, запуск +игры, каталоги версий, модлоадеров, Java и сезонных сборок, три установщика, загрузчик паков и +переключатель сборок — и отвечает за то, чтобы они работали в правильном порядке. Кроме того, он +хранит состояние лаунчера: профили игрока, пользовательские сборки и настройки запуска, которые +читает и пишет в файлы папки лаунчера. + +Ещё одна его задача — приводить данные к виду, удобному QML. Каталоги отдаются в интерфейс уже +сведёнными с локальным состоянием: строка версии знает, скачана ли она, строка сезонной сборки — +установлена ли и не устарела ли. Окно показывает статус, не считая ничего само. + +## Место в проекте и зависимости + +Единственный экземпляр создаётся декларативно в [Main.qml](../qml/Main.md); в `main.cpp` он не +упоминается. + +Владеет двенадцатью сервисами, каждому из которых посвящена своя страница: + +| Поле | Класс | Роль | +|------|-------|------| +| `m_auth` | [AuthService](AuthService.md) | вход через Ely.by и офлайн | +| `m_msa` | [MsaAuthService](MsaAuthService.md) | вход через Microsoft | +| `m_launcher` | [GameLauncher](GameLauncher.md) | запуск JVM с игрой | +| `m_manifest` | [VersionManifestService](VersionManifestService.md) | каталог версий Mojang | +| `m_installer` | [VersionInstaller](VersionInstaller.md) | установка версии игры | +| `m_loaderMeta` | [ModLoaderVersionService](ModLoaderVersionService.md) | списки версий модлоадеров | +| `m_loaderInstaller` | [ModLoaderInstaller](ModLoaderInstaller.md) | установка модлоадера | +| `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). + +Требования сборки: `Qt6::Core`, `Qt6::Gui`, `Qt6::Network`, `Qt6::CorePrivate` (опосредованно) и +`Qt6::Quick` — класс зарегистрирован в QML-модуле `Minecraft_launcher`, объявленном в +`CMakeLists.txt`. + +## Иерархия и роль + +Наследует `QObject`: мета-объектная система, свойства, сигналы и владение по родителю. Объявлен +виртуальный деструктор. Виртуальных методов базового класса не переопределяет. + +## Свойства Q_PROPERTY + +### Профили и сборки + +| Свойство | Тип | READ | WRITE | NOTIFY | Описание | +|----------|-----|------|-------|--------|----------| +| `profileNames` | `QStringList` | `profileNames` | — | `profilesChanged` | Имена профилей игрока в порядке добавления. Только для чтения; модель выпадающего списка профилей | +| `customBuildNames` | `QStringList` | `customBuildNames` | — | `customBuildsChanged` | Имена пользовательских сборок. Только для чтения; модель списка в окне сборок | +| `activeBuildIndex` | `int` | `activeBuildIndex` | `setActiveBuildIndex` | `activeBuildChanged` | Сборка, которую запускает кнопка игры. Хранится по идентификатору сборки, а не по индексу: удаление соседней записи не должно переназначать активную. Запись в свойство запускает смену сборки | +| `activeBuildName` | `QString` | `activeBuildName` | — | `activeBuildChanged` | Имя активной сборки для подписи на кнопке. Только для чтения | +| `installedVersions` | `QStringList` | `installedVersions` | — | `installedVersionsChanged` | Версии, реально установленные в папке игры. Только для чтения | + +### Занятость и смена сборки + +| Свойство | Тип | READ | WRITE | NOTIFY | Описание | +|----------|-----|------|-------|--------|----------| +| `gameRunning` | `bool` | `gameRunning` | — | `gameRunningChanged` | Игра запущена. Только для чтения; выключает кнопку запуска | +| `switching` | `bool` | `switching` | — | `switchChanged` | Идёт архивация или распаковка `.minecraft` при смене сборки. Отдельно от `busy`, потому что на это время блокируется ещё и список сборок. Только для чтения | +| `switchProgress` | `double` | `switchProgress` | — | `switchChanged` | Доля выполнения смены сборки от `0` до `1`; `-1` — итог неизвестен. Только для чтения | +| `switchStage` | `QString` | `switchStage` | — | `switchChanged` | Этап смены сборки словами. Только для чтения | +| `switchStatus` | `QString` | `switchStatus` | — | `switchChanged` | Строка состояния смены сборки. Только для чтения | +| `busy` | `bool` | `busy` | — | `busyChanged` | Лаунчер занят: пока идёт загрузка версии, кнопка запуска гаснет. Только для чтения | +| `microsoftAvailable` | `bool` | `microsoftAvailable` | — | — | Собран ли лаунчер с Qt WebEngine. Константное свойство: без WebEngine окно входа Microsoft показать нечем, и интерфейс не должен предлагать этот путь | + +### Каталог версий + +| Свойство | Тип | READ | WRITE | NOTIFY | Описание | +|----------|-----|------|-------|--------|----------| +| `versionCatalog` | `QVariantList` | `versionCatalog` | — | `versionCatalogChanged` | Объединённый список для выбора версии: установленные, разделитель, затем весь каталог Mojang. Строка содержит поля `id`, `label`, `category`, `installed` и `search`. Только для чтения | +| `catalogLoading` | `bool` | `catalogLoading` | — | `catalogLoadingChanged` | Идёт загрузка манифеста версий. Только для чтения | + +### Каталог Java + +| Свойство | Тип | READ | WRITE | NOTIFY | Описание | +|----------|-----|------|-------|--------|----------| +| `javaCatalog` | `QVariantList` | `javaCatalog` | — | `javaCatalogChanged` | Сборки Java для окна выбора: скачиваемые из сети плюс те, что уже лежат в папке лаунчера. Строка содержит поля `id`, `label`, `kind`, `major`, `installed`, `downloadable`, `lts`, `sizeMb`, `detail`, `coverage` и `search`. Только для чтения | +| `javaCatalogLoading` | `bool` | `javaCatalogLoading` | — | `javaCatalogLoadingChanged` | Идёт загрузка каталога Java. Только для чтения | + +### Сезонные сборки + +| Свойство | Тип | READ | WRITE | NOTIFY | Описание | +|----------|-----|------|-------|--------|----------| +| `seasonalCatalog` | `QVariantList` | `seasonalCatalog` | — | `seasonalCatalogChanged` | Готовые сборки с сервера. Строки уже сведены с локальными записями: окно показывает статус, не считая ничего само. Только для чтения | +| `seasonalCatalogLoading` | `bool` | `seasonalCatalogLoading` | — | `seasonalCatalogLoadingChanged` | Идёт загрузка каталога сезонных сборок. Только для чтения | +| `seasonalCatalogError` | `QString` | `seasonalCatalogError` | — | `seasonalCatalogChanged` | Текст ошибки обращения к серверу сборок; пусто — всё в порядке. Только для чтения | +| `seasonalInstalling` | `bool` | `seasonalInstalling` | — | `seasonalInstallingChanged` | Идёт установка сезонной сборки: окно не даёт начать вторую. Только для чтения | + +### Загрузка + +Одного сигнала на все свойства загрузки достаточно: установщик уже ограничивает частоту, а QML всё +равно перечитывает их разом. + +| Свойство | Тип | READ | WRITE | NOTIFY | Описание | +|----------|-----|------|-------|--------|----------| +| `downloading` | `bool` | `downloading` | — | `downloadChanged` | Идёт какая-либо загрузка. Только для чтения | +| `downloadProgress` | `double` | `downloadProgress` | — | `downloadChanged` | Доля выполнения от `0` до `1`; `-1` — итог неизвестен. Только для чтения | +| `downloadVersion` | `QString` | `downloadVersion` | — | `downloadChanged` | Что именно качается — версия, модлоадер, сборка Java или пак. Только для чтения | +| `downloadStatus` | `QString` | `downloadStatus` | — | `downloadChanged` | Строка состояния загрузки. Только для чтения | +| `downloadBytesDone` | `qint64` | `downloadBytesDone` | — | `downloadChanged` | Принято байт. Только для чтения | +| `downloadBytesTotal` | `qint64` | `downloadBytesTotal` | — | `downloadChanged` | Ожидаемый объём в байтах; `0` — неизвестен. Только для чтения | + +Панель загрузки одна на все четыре источника: свойства отдают числа того установщика или +загрузчика, который работает сейчас. + +## Методы Q_INVOKABLE + +Все перечисленные ниже методы вызываются из QML. + +### Профили + +#### void addProfile(const QString &name, const QString &login, const QString &password, const QString &authType = "offline") + +Добавляет профиль игрока. Параметр `authType` принимает значения `offline`, `elyby` и `microsoft`. +Для офлайн-профиля пароль не нужен, для профиля Microsoft не нужны ни логин, ни пароль. Испускает +`profilesChanged`. + +#### void updateProfile(int index, const QString &name, const QString &login, const QString &password, const QString &authType = "offline") + +Перезаписывает профиль по индексу теми же полями. Испускает `profilesChanged`. + +#### QVariantMap profileAt(int index) const + +Данные профиля для диалога редактирования: имя, логин, пароль, тип, а также признак наличия +действующей сессии Microsoft и ник, полученный при официальной авторизации. + +#### void removeProfile(int index) + +Удаляет профиль. Испускает `profilesChanged`. + +### Вход через Microsoft + +#### void startMicrosoftLogin(int profileIndex) + +Начинает вход в аккаунт Microsoft: испускает `microsoftLoginUrlReady` с адресом страницы входа. + +Значение `-1` в `profileIndex` означает, что вход ещё не привязан к профилю: профиль создастся по +нику, который вернут Minecraft Services. + +#### void finishMicrosoftLogin(const QString &code) + +Завершает вход по коду авторизации, перехваченному окном браузера. Итог приходит сигналом +`microsoftLoginSucceeded` или `microsoftLoginFailed`. + +#### void cancelMicrosoftLogin() + +Сбрасывает начатую сессию входа. Вызывается, когда пользователь закрыл окно или адрес возврата +пришёл без кода. + +#### QVariantMap inspectMicrosoftRedirect(const QString &url) const + +Разбирает адрес, на который встроенное окно возвращается после входа. Возвращает карту с полями +`matched` (является ли адрес адресом возврата), `code` и `error`. + +Разбор живёт в C++, чтобы правила совпадения не разъезжались с теми, по которым сервис сам строит +`redirect_uri`. + +### Сборки + +#### void addCustomBuild(const QString &name, const QString &serverUrl, const QString &minecraftVersion = QString()) + +Создаёт пользовательскую сборку. Испускает `customBuildsChanged`. + +#### void updateCustomBuild(int index, const QVariantMap &fields) + +Мержит в сборку только присланные ключи: `name`, `serverUrl`, `minecraftVersion`, `loader`, +`loaderVersion`, `resolvedVersionId`. Остальные поля остаются как были — это и позволяет карточке +сборки сохранять правки по одному полю за раз. + +#### QVariantMap customBuildAt(int index) const + +Данные сборки для карточки редактирования. + +#### QVariantMap customBuildRemovalInfo(int index) const + +Что именно потеряется при удалении сборки — для текста предупреждения. Возвращает имя сборки и три +признака: есть ли у неё архив, активна ли она сейчас и последняя ли она. + +#### void removeCustomBuild(int index) + +Удаляет сборку вместе с её архивом. Испускает `customBuildsChanged`. + +#### void installCustomBuild(int index) + +Докачивает то, чего не хватает выбранной сборке: версию игры и, если он выбран, модлоадер. +Вынесено отдельной кнопкой, потому что карточка сборки сохраняет правки по ходу редактирования и +установка не должна начинаться сама при каждой правке. + +#### QStringList checkInstallation(int buildIndex) const + +Проверка комплектности без запуска — для подсказки в интерфейсе. Пустой список означает, что +сборку можно запускать; иначе возвращаются описания недостающих файлов. + +### Запуск игры + +#### void launchGame(int profileIndex, int buildIndex) + +Главная кнопка. Проверяет выбор профиля и версии, комплектность `.minecraft`, при необходимости +авторизуется и стартует игру. + +Авторизация асинхронна, поэтому метод возвращается сразу; дальнейший ход виден по сигналам +`launchProgress`, `launched`, `launchError` и `twoFactorRequired`. + +#### void submitTwoFactorCode(const QString &code) + +Продолжает прерванный запуск, отдавая одноразовый код двухфакторной аутентификации. Вызывается +после сигнала `twoFactorRequired`. + +#### void cancelPendingLaunch() + +Отменяет запуск, остановленный на ожидании кода двухфакторной аутентификации. + +#### void stopGame() + +Завершает процесс игры. + +### Каталог версий + +#### void refreshVersionCatalog(bool force = false) + +Обновляет каталог версий. Вызывается при открытии окна выбора версии: свежий кэш отвечает без +сети. Параметр `force` обходит проверку свежести. + +#### bool isVersionInstalled(const QString &versionId) const + +Установлена ли версия в папке игры. + +#### void installVersion(const QString &versionId) + +Ставит версию игры в фоне. Ход виден по свойствам загрузки. + +#### QVariantMap versionRemovalInfo(const QString &versionId) const + +Что потеряется при удалении версии — для текста предупреждения. Возвращает признак установки, +занимаемый объём в мегабайтах, список зависящих профилей модлоадеров и список сборок, которые эту +версию используют. + +#### void removeVersion(const QString &versionId) + +Удаляет файлы версии из `versions/`. Библиотеки и ресурсы остаются: они общие для всех версий. +Испускает `installedVersionsChanged`. + +#### void cancelDownload() + +Отменяет текущую загрузку. + +### Модлоадеры + +#### QVariantList loaderVersions(const QString &loaderKey, const QString &gameVersion) const + +Версии модлоадера для выбранной версии игры. Возвращает список карт с полями `version`, `label`, +`recommended` и `stable`. + +Несовместимых строк в списке нет — отбор заложен в сам источник данных, поэтому проверять +совместимость вызывающему коду не нужно. Пустой список означает, что лоадер эту версию игры не +поддерживает. + +Параметр `loaderKey` принимает значения `forge`, `fabric`, `neoforge` и `quilt`. + +#### void refreshLoaderVersions(const QString &loaderKey, const QString &gameVersion, bool force = false) + +Запрашивает обновление списка версий лоадера. Результат приходит сигналом `loaderVersionsChanged`. + +#### bool loaderVersionsLoading(const QString &loaderKey, const QString &gameVersion) const + +Идёт ли сейчас запрос по этой паре. Позволяет интерфейсу отличить «ещё грузим» от «не +поддерживается». + +#### void installLoaderForBuild(int index) + +Ставит модлоадер, выбранный в сборке, и записывает получившийся профиль в `resolvedVersionId`. + +### Настройки + +#### QVariantMap settings() const + +Настройки запуска одной картой: `gameDir`, `javaPath`, `javaRuntime`, `minMemoryMb`, +`maxMemoryMb`, `jvmArgs`, `windowWidth`, `windowHeight`, `fullscreen`, `language` и вычисленный +`resolvedGameDir`. + +#### void updateSettings(const QVariantMap &values) + +Записывает настройки и сохраняет их на диск. Испускает `settingsChanged`. + +Смену `language` после записи пробрасывает в [Localization](Localization.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++: + +```qml +import QtQuick +import Minecraft_launcher + +Window { + id: window + visible: true + + LauncherBackend { + id: backend + + onLaunchError: (message) => console.warn(message) + onTwoFactorRequired: (profileName) => twoFactorDialog.open() + onMicrosoftLoginUrlReady: (url) => window.openMicrosoftLogin(url) + } + + Button { + text: backend.activeBuildName + enabled: !backend.busy && !backend.gameRunning + onClicked: backend.launchGame(profileBox.currentIndex, backend.activeBuildIndex) + } +} +``` + +--- + +При создании этого документа использовался ИИ. diff --git a/doc/cpp/Localization.md b/doc/cpp/Localization.md new file mode 100644 index 0000000..38dab30 --- /dev/null +++ b/doc/cpp/Localization.md @@ -0,0 +1,164 @@ +# Localization + +`localization.h` / `localization.cpp` + +Единственный источник всех текстов интерфейса. Каталог — [`i18n/translations.json`](#каталог), +он лежит в ресурсах и правится руками. Штатных `.ts`/`.qm` в проекте нет намеренно: `lupdate`, +`lrelease` и Linguist не нужны, а все переводы видны в одном файле. + +Язык переключается на лету: при смене все привязки QML перевычисляются, перезапуск не требуется. + +## Доступ из QML + +Синглтон зарегистрирован декларативно (`QML_NAMED_ELEMENT(Loc)` + `QML_SINGLETON`) и доступен +в любом файле модуля без импорта. + +```qml +Text { text: Loc.t.settings.title } +Text { text: Loc.t.java.progress.downloading.arg(label) } +DarkCombo { model: Loc.t.profile.authTypes } +``` + +Точечный путь `Loc.t.a.b.c` — это обращение к вложенным объектам дерева, которое строится из +плоских ключей каталога разбиением по точке. + +**Почему свойство, а не метод.** Вызов `Q_INVOKABLE` не регистрирует зависимость привязки, и +`text: Loc.t("ключ")` никогда бы не обновился при смене языка. Чтение `Q_PROPERTY` с сигналом +`NOTIFY` зависимость регистрирует: `languageChanged` перевычисляет все привязки, которые читали +`Loc.t`. Сегменты после `t` — обычные обращения к членам JS-объекта, отслеживать их не нужно, +потому что при смене языка дерево заменяется целиком. + +Тип свойства — `QJSValue`, а не `QVariantMap`: `QVariantMap` пересобирался бы в новый JS-объект +при каждом чтении, а привязок в проекте полторы сотни. `QJSValue` строится один раз на смену +языка. + +Из тела JS-функции `Loc.t` читается так же — это просто чтение свойства: + +```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` нет. + +```cpp +#include "localization.h" + +emit launchError(Loc::text("launch.error.noProfile")); +emit launchProgress(Loc::text("java.progress.downloading").arg(label)); +const QStringList kinds = Loc::list("profile.authTypes"); +``` + +Ключа нет — возвращается сам ключ, а в отладочной сборке ещё и `qWarning`: строка вида +`launch.error.noProfile` в интерфейсе сразу бросается в глаза. + +### Потоки + +`Loc::text()` и `Loc::list()` можно звать из любого потока. Каталог заполняется ровно один раз +в `load()`, который отрабатывает в `main()` до того, как [BuildSwitcher](BuildSwitcher.md) +создаст свой поток; дальше он только читается, а копирование `QString` из хэша безопасно само по +себе. Единственное, что меняется на ходу, — индекс текущего языка, и он `QAtomicInt`. В худшем +случае сообщение, которое собиралось в момент переключения, уедет на прежнем языке. + +`setLanguage()` и рассылка `languageChanged` — только поток GUI; это проверяется `Q_ASSERT`. + +## Каталог + +`i18n/translations.json` попадает в ресурсы через список `RESOURCES` в `qt_add_qml_module`, +поэтому читается по пути `:/qt/qml/Minecraft_launcher/i18n/translations.json`. + +```json +{ + "_meta": { + "languages": ["ru", "en"], + "displayNames": { "ru": "Русский", "en": "English" } + }, + "strings": { + "settings.title": { "ru": "Настройки запуска", "en": "Launch settings" }, + "java.progress.downloading": { "ru": "Загрузка Java «%1»…", "en": "Downloading Java \"%1\"…" }, + "profile.authTypes": { + "ru": ["Офлайн (без пароля)", "Ely.by", "Microsoft (лицензия)"], + "en": ["Offline (no password)", "Ely.by", "Microsoft (licensed)"] + } + } +} +``` + +Ключи плоские, языки рядом: забытый перевод виден на соседней строке, правка пары — один участок +файла, а третий язык добавляется колонкой без правок загрузчика. + +**Имя ключа — `<домен>.<вид>.<имя>`**, сегменты в 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.*`. + +Подстановки `%1`/`%2` сохраняются дословно: их одинаково понимают `QString::arg()` и +QML-овский `String.arg()`. **Набор `%N` в `ru` и `en` обязан совпадать** — порядок слов может +отличаться, состав нет. Множественного числа формат не поддерживает; в коде сейчас нет ни одной +строки, которой оно требуется. + +## Как добавить строку + +1. Добавить запись в `strings` файла `i18n/translations.json` — сразу с `ru` и `en`. +2. Сослаться на неё: `Loc.t.домен.вид.имя` в QML или `Loc::text("домен.вид.имя")` в C++. +3. Прогнать `python3 tools/check_translations.py`. + +## Как добавить язык + +1. Дописать код в `_meta.languages` и название в `_meta.displayNames`. +2. Добавить колонку с этим кодом в каждую запись `strings`. +3. Добавить код в `stLanguage.codes` и пункт в модель комбобокса в [Main.qml](../qml/Main.md), + а также ключ `settings.language.<код>` с эндонимом (название языка не переводится — оно + одинаково во всех колонках). +4. При необходимости поправить `systemLanguage()` в `localization.cpp`: сейчас он выбирает + русский для русской системной локали и английский во всех остальных случаях. + +## Выбор языка и его хранение + +Ключ настройки — `language`, значения `"system"`, `"ru"`, `"en"`, по умолчанию `"system"`. +Хранится в `settings.json` рядом с остальными настройками; в интерфейсе — первым пунктом +диалога «Настройки запуска», применяется по кнопке «Сохранить». + +Круг замкнут в одну сторону, циклической зависимости нет: + +``` +main.cpp ──► Localization::load() ──► LauncherPaths::settingsFile() (только чтение, один раз) +LauncherBackend::updateSettings() ──► Localization::setLanguage() (в одну сторону) +``` + +`Localization` ничего не знает про [LauncherBackend](LauncherBackend.md) — писать `settings.json` +по-прежнему может только он. Читать настройки самому приходится потому, что язык нужен раньше, +чем QML вычислит первую привязку, а бэкенд появляется только вместе с движком. + +Неизвестное значение (файл правили руками) откатывается на `"system"` с предупреждением. +Отсутствующий или испорченный каталог — ошибка на старте: `main()` пишет причину и возвращает +`-1`. Файл вкомпилирован в бинарник, так что это может быть только ошибка сборки, а лаунчер, +у которого все подписи выглядят как точечные ключи, хуже, чем лаунчер, который сказал, почему +не запустился. + +**Известное ограничение.** Уже сложенные в поля C++ строки не перепереводятся: +`LauncherBackend::m_storageIssues` собирается при старте, а `stage()`/`status()` у +[BuildSwitcher](BuildSwitcher.md) заменяются на следующем тике прогресса. Все они +диагностические и короткоживущие. + +## Проверка + +`tools/check_translations.py` — только чтение, ненулевой код возврата при любой ошибке: + +- каждый `Loc::text("…")` и `Loc::list("…")` из C++ есть в каталоге и совпадает по типу значения; +- каждый путь `Loc.t.a.b.c` из QML разворачивается в существующий ключ; +- наборы ключей у всех языков совпадают, пустых значений нет, типы одинаковы; +- наборы `%N` совпадают по языкам, длины списков равны; +- не осталось ни одного `tr(`, `qsTr(` или `QCoreApplication::translate`; +- ключи, на которые никто не ссылается, — предупреждением. diff --git a/doc/cpp/ModLoaderInstaller.md b/doc/cpp/ModLoaderInstaller.md new file mode 100644 index 0000000..10e8df5 --- /dev/null +++ b/doc/cpp/ModLoaderInstaller.md @@ -0,0 +1,191 @@ +# ModLoaderInstaller + +## Обзор класса + +`ModLoaderInstaller` ставит модлоадер в `.minecraft`. Под одним фасадом он прячет два совершенно +разных пути установки. + +**Fabric и Quilt** отдают готовое описание версии: лаунчер кладёт его в +`versions//.json` и передаёт дальше [VersionInstaller](VersionInstaller.md), который по +полю `inheritsFrom` сам поставит ванильную версию и библиотеки лоадера. + +**Forge и NeoForge** так не умеют: их установка — это патч клиентского jar. Поэтому лаунчер +запускает официальный `installer.jar` найденной Java в headless-режиме и смотрит, какой профиль +появился в `versions`. Процессу установщика при этом подсовывается эталонный zlib — см. +[ZlibReference](zlibreference.md). + +Оба пути начинаются одинаково: пока ванильная версия не скачана целиком, ставить лоадер некуда. + +Набор геттеров прогресса намеренно повторяет `VersionInstaller`: панель загрузки в интерфейсе +читает их одинаково, независимо от того, кто сейчас работает. + +## Место в проекте и зависимости + +Подключает [modloader.h](modloader.md). В конструктор принимает +[ModLoaderVersionService](ModLoaderVersionService.md) — за адресом установщика — и +[VersionInstaller](VersionInstaller.md) — за базовой версией и за докачиванием того, что +`installer.jar` не положил. + +Экземпляр создаётся и принадлежит [LauncherBackend](LauncherBackend.md). + +Требования сборки: `Qt6::Core` (`QProcess`, `QFile`) и `Qt6::Network` (`QNetworkAccessManager`, +`QNetworkReply`). + +## Иерархия и роль + +Наследует `QObject`: мета-объектная система, шесть сигналов и владение по родителю. Объявлен +виртуальный деструктор — класс владеет процессом установщика и открытым файлом журнала. + +## Публичные методы + +#### ModLoaderInstaller(ModLoaderVersionService \*meta, VersionInstaller \*versionInstaller, QObject \*parent = nullptr) + +Создаёт установщик поверх двух сервисов. Ни один из них не переходит во владение установщика — +оба обязаны пережить его. + +#### bool isRunning() const + +Идёт ли установка прямо сейчас. + +#### QString label() const + +Подпись текущей установки для интерфейса — название лоадера с версиями. + +#### 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/`. +Он важен именно для 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); +``` + +--- + +При создании этого документа использовался ИИ. diff --git a/doc/cpp/ModLoaderVersionService.md b/doc/cpp/ModLoaderVersionService.md new file mode 100644 index 0000000..b142ee5 --- /dev/null +++ b/doc/cpp/ModLoaderVersionService.md @@ -0,0 +1,152 @@ +# ModLoaderVersionService + +## Обзор класса + +Каждый модлоадер публикует свой список версий, и у каждого он устроен по-своему. `ModLoaderVersionService` +приводит все четыре к одному виду: качает списки, кэширует в папке лаунчера и отдаёт из кэша, пока +тот не устарел. Устроен так же, как [VersionManifestService](VersionManifestService.md). + +Главная особенность класса — в том, чего в нём нет: отдельной проверки совместимости с версией +игры. Совместимость заложена в структуру данных. Fabric и Quilt отдают список сразу под нужную +версию игры, а `maven-metadata` Forge и NeoForge раскладывается по версиям игры при разборе. +Версии игры, под которую сборок нет, соответствует пустой список — выбрать несовместимый лоадер +физически нечем. + +## Место в проекте и зависимости + +Подключает [modloader.h](modloader.md): перечисление `ModLoader` и структура `LoaderVersionEntry` +приходят оттуда. + +Экземпляр создаётся и принадлежит [LauncherBackend](LauncherBackend.md). Ссылку на него получает +[ModLoaderInstaller](ModLoaderInstaller.md) — из записи списка он берёт адрес `installer.jar`. + +Путь к файлу кэша даёт `LauncherPaths::loaderCacheFile()` из [launcherpaths.h](launcherpaths.md). + +Требования сборки: `Qt6::Core` (`QDateTime`, `QHash`, `QSet`) и `Qt6::Network` +(`QNetworkAccessManager`). + +## Иерархия и роль + +Наследует `QObject`: мета-объектная система, два сигнала и владение по родителю. Виртуальных +методов базового класса не переопределяет. + +## Псевдонимы типов + +`ModLoaderVersionService::Callback` — `std::function`. +Как и у сервиса манифеста, `ok == true` с непустым `warning` означает, что данные отдали из +устаревшего кэша. + +## Публичные методы + +#### explicit ModLoaderVersionService(QObject \*parent = nullptr) + +Создаёт сервис и его `QNetworkAccessManager`. Кэши читаются лениво, при первом обращении к +конкретному лоадеру. Конструктор помечен `explicit`. + +#### void ensureLoaded(ModLoader loader, const QString &gameVersion, Callback callback, bool forceRefresh = false) + +Единственная точка входа. Свежий кэш означает отложенный вызов колбэка без сети; иначе запускается +один сетевой запрос на всех, кто успел попросить. + +Свежесть считается по-разному в зависимости от лоадера: Fabric и Quilt спрашиваются по каждой +версии игры отдельно, поэтому отметка времени у них своя на каждую версию; Forge и NeoForge +приходят одним `maven-metadata` на все версии сразу, и отметка у них одна на весь лоадер. + +Параметр `forceRefresh` обходит проверку свежести. + +#### QList<LoaderVersionEntry> versions(ModLoader loader, const QString &gameVersion) const + +Список сборок лоадера под конкретную версию игры. Новые сборки идут первыми, поэтому первая строка +— самая свежая; именно её интерфейс подставляет по умолчанию. + +Пустой список означает, что лоадер эту версию игры не поддерживает. + +#### bool isRefreshing(ModLoader loader, const QString &gameVersion) const + +Идёт ли сейчас запрос по этой паре. Интерфейс по этому признаку отличает «ещё грузим» от «не +поддерживается» — оба случая выглядят пустым списком. + +#### std::optional<LoaderVersionEntry> find(ModLoader loader, const QString &gameVersion, const QString &loaderVersion) const + +Запись по версии лоадера; `std::nullopt`, если такой нет. Из неё установщик берёт ссылку на +`installer.jar`. + +## Сигналы + +#### versionsChanged(const QString &loaderKey, const QString &gameVersion) + +Список версий изменился. Параметры сужают событие до конкретной пары: `loaderKey` принимает +значения `forge`, `fabric`, `neoforge`, `quilt`. + +Обработчик должен сверить оба параметра со своим текущим состоянием и перечитать `versions()`, +только если они совпадают, — иначе обновление относится к другой строке лоадера. Именно так +поступает [LoaderRow](../qml/LoaderRow.md). + +#### refreshingChanged() + +Изменился признак сетевого обновления у какой-либо пары. Обработчик перечитывает +`isRefreshing()` для интересующей его пары. + +## Владение и время жизни + +Класс наследует `QObject` и принимает `parent` — родитель его и удалит. +`QNetworkAccessManager` создаётся в конструкторе с сервисом в роли родителя. + +Отложенные колбэки хранятся по ключу запроса до завершения соответствующего обращения к сети. +Уничтожение сервиса с незавершённым запросом обрывает его, и накопленные колбэки не вызываются. + +Ссылку на сервис держит установщик модлоадеров; уничтожать сервис раньше установщика нельзя. + +## Потокобезопасность + +Только поток GUI. Чтение и запись кэша выполняются синхронно в вызывающем потоке. + +## Взаимодействие с другими классами + +`LauncherBackend` оборачивает сервис тремя методами, доступными из QML: получить список, запросить +обновление и узнать, идёт ли загрузка. Сигнал `versionsChanged` он переправляет в QML под тем же +именем, поэтому строка лоадера в карточке сборки подписывается прямо на него. + +[ModLoaderInstaller](ModLoaderInstaller.md) обращается к `find()` за адресом установщика перед +началом установки. + +## Внешнее взаимодействие + +**Сеть, исходящие запросы.** Класс обращается к четырём разным источникам метаданных, и форматы +ответов различаются: у Fabric и Quilt это JSON, у Forge и NeoForge — XML `maven-metadata`. Разбор +разделён на две функции соответственно. + +Испорченный ответ разбирается в пустой результат, и хороший кэш им не затирается — это сознательное +решение: лучше показать вчерашний список, чем стереть его из-за сбоя на сервере. + +При недоступной сети данные отдаются из устаревшего кэша с `ok == true` и заполненным `warning`. + +Все сигналы и колбэки приходят в поток GUI. + +## Пример использования + +```cpp +auto *loaders = new ModLoaderVersionService(this); + +connect(loaders, &ModLoaderVersionService::versionsChanged, + this, [this](const QString &key, const QString &game) { + if (key == loaderKey(ModLoader::Fabric) && game == m_gameVersion) + emit fabricVersionsChanged(); + }); + +loaders->ensureLoaded(ModLoader::Fabric, QStringLiteral("1.21.1"), + [this, loaders](bool ok, const QString &warning) { + if (!ok) { + showStatus(warning); + return; + } + const auto list = loaders->versions(ModLoader::Fabric, + QStringLiteral("1.21.1")); + if (!list.isEmpty()) + selectVersion(list.first().loaderVersion); + }); +``` + +--- + +При создании этого документа использовался ИИ. diff --git a/doc/cpp/MsaAuthService.md b/doc/cpp/MsaAuthService.md new file mode 100644 index 0000000..6d2ef10 --- /dev/null +++ b/doc/cpp/MsaAuthService.md @@ -0,0 +1,161 @@ +# MsaAuthService + +## Обзор класса + +`MsaAuthService` — авторизация через учётную запись Microsoft, то есть вход с лицензионной копией +игры. Это та же цепочка, что и в официальном лаунчере: OAuth2 → Xbox Live → XSTS → Minecraft +Services → проверка лицензии. + +Результат отдаётся тем же `AuthResult`, что и [AuthService](AuthService.md) для Ely.by и офлайна, +поэтому запуск игры дальше идёт по общему пути и ничего не знает о способе входа. + +Класс не показывает окно входа сам: страницу Microsoft открывает +[MicrosoftLoginDialog](../qml/MicrosoftLoginDialog.md) на стороне QML, а сервис даёт ему адрес +страницы и разбирает адрес возврата. + +## Место в проекте и зависимости + +Подключает `authservice.h` — ради общей структуры `AuthResult`. Экземпляр создаётся и принадлежит +[LauncherBackend](LauncherBackend.md). + +Требования сборки: `Qt6::Core` (`QJsonObject`, `QString`, `QUrl`) и `Qt6::Network` +(`QNetworkAccessManager`). + +Сам класс собирается всегда и не зависит от Qt WebEngine — от наличия WebEngine зависит только +окно, в котором показывается страница входа. Поэтому в сборке без WebEngine сервис существует, но +воспользоваться им нельзя: показать страницу нечем. + +## Иерархия и роль + +Наследует `QObject`: мета-объектная система, сигнал `progress`, владение по родителю. Виртуальных +методов базового класса не переопределяет. + +## Псевдонимы типов + +`MsaAuthService::Callback` — `std::function`. Колбэк вызывается ровно +один раз и в потоке GUI. + +## Публичные методы + +#### explicit MsaAuthService(QObject \*parent = nullptr) + +Создаёт сервис и его `QNetworkAccessManager`. Конструктор помечен `explicit`. + +#### static QString clientId() + +Идентификатор приложения лаунчера в Microsoft. Игра ждёт его в `${clientid}`: официальный лаунчер +подставляет туда именно идентификатор приложения, а не случайный токен сессии. + +#### 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); + }); +} +``` + +--- + +При создании этого документа использовался ИИ. diff --git a/doc/cpp/SeasonalBuildService.md b/doc/cpp/SeasonalBuildService.md new file mode 100644 index 0000000..914fe9e --- /dev/null +++ b/doc/cpp/SeasonalBuildService.md @@ -0,0 +1,175 @@ +# SeasonalBuildService + +## Обзор класса + +Кроме сборок, которые пользователь собирает сам, лаунчер умеет ставить готовые сезонные сборки с +собственного файлового сервера: набор модов под конкретную версию игры и модлоадер, подготовленный +заранее и выдаваемый целиком. + +`SeasonalBuildService` — каталог этих сборок: скачивает `index.json` с файлового сервера, кэширует +в папке лаунчера и отдаёт из кэша, пока тот не устарел. Устройство повторяет +[VersionManifestService](VersionManifestService.md) — включая то, что пустой разбор считается +испорченным ответом и хороший кэш им не затирается. + +## Место в проекте и зависимости + +Экземпляр создаётся и принадлежит [LauncherBackend](LauncherBackend.md). Записи каталога +используются [SeasonalPackDownloader](SeasonalPackDownloader.md) — оттуда берётся адрес архива и +его контрольная сумма. + +Путь к файлу кэша даёт `LauncherPaths::seasonalCatalogFile()` из +[launcherpaths.h](launcherpaths.md). + +Требования сборки: `Qt6::Core` (`QDate`, `QDateTime`, `QHash`, `QUrl`) и `Qt6::Network` +(`QNetworkAccessManager`). + +## Иерархия и роль + +Наследует `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`. +Сочетание `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; + } + if (!warning.isEmpty()) + showStatus(warning); +}); +``` + +--- + +При создании этого документа использовался ИИ. diff --git a/doc/cpp/SeasonalPackDownloader.md b/doc/cpp/SeasonalPackDownloader.md new file mode 100644 index 0000000..56a0651 --- /dev/null +++ b/doc/cpp/SeasonalPackDownloader.md @@ -0,0 +1,162 @@ +# SeasonalPackDownloader + +## Обзор класса + +`SeasonalPackDownloader` скачивает один архив сезонной сборки в файл. Задача узкая и отдельная по +двум причинам: пак — это сотни мегабайт, поэтому он пишется потоком, а не держится в памяти; и его +sha256 обязательно сверяется, потому что распаковывать битую загрузку поверх рабочей `.minecraft` +нельзя. + +Распаковкой класс не занимается — это делает [BuildSwitcher](BuildSwitcher.md) в отдельном потоке +вместе с остальными операциями над содержимым папки игры. + +Набор геттеров прогресса повторяет [VersionInstaller](VersionInstaller.md) и +[JavaInstaller](JavaInstaller.md): панель загрузки в интерфейсе читает их одинаково, независимо от +того, кто сейчас работает. + +## Место в проекте и зависимости + +Экземпляр создаётся и принадлежит [LauncherBackend](LauncherBackend.md). Адрес архива, его размер +и контрольную сумму даёт запись каталога от [SeasonalBuildService](SeasonalBuildService.md). + +Требования сборки: `Qt6::Core` (`QCryptographicHash`, `QSaveFile`, `QTimer`) и `Qt6::Network` +(`QNetworkAccessManager`, `QNetworkReply`). + +## Иерархия и роль + +Наследует `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`: незавершённая запись отменяется вместе +с уничтожением объекта, и повреждённый архив не попадает на место назначения. Сетевой ответ +создаётся по ходу работы и закрывается в деструкторе. + +## Потокобезопасность + +Только поток 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); +``` + +--- + +При создании этого документа использовался ИИ. diff --git a/doc/cpp/VersionInstaller.md b/doc/cpp/VersionInstaller.md new file mode 100644 index 0000000..540e9f0 --- /dev/null +++ b/doc/cpp/VersionInstaller.md @@ -0,0 +1,211 @@ +# 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`, а файл внутри каждой — как +`std::unique_ptr`: незавершённая запись отменяется вместе с уничтожением объекта, и +испорченный файл не попадает на место назначения. + +## Потокобезопасность + +Только поток 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); +``` + +--- + +При создании этого документа использовался ИИ. diff --git a/doc/cpp/VersionManifestService.md b/doc/cpp/VersionManifestService.md new file mode 100644 index 0000000..51639c6 --- /dev/null +++ b/doc/cpp/VersionManifestService.md @@ -0,0 +1,167 @@ +# VersionManifestService + +## Обзор класса + +Каталог версий Minecraft — это манифест Mojang: около тысячи записей от альф 2010 года до +свежайших снапшотов. `VersionManifestService` отвечает за него целиком: скачивает манифест, +кэширует в папке лаунчера и отдаёт из кэша, пока тот не устарел. + +Класс нужен двум потребителям: окну выбора версии, которому нужен весь список, и установщику, +которому по идентификатору версии нужна ссылка на её описание. + +## Место в проекте и зависимости + +Экземпляр создаётся и принадлежит [LauncherBackend](LauncherBackend.md). Ссылку на него получает +[VersionInstaller](VersionInstaller.md) — установщик берёт из манифеста адрес описания версии. + +Путь к файлу кэша даёт `LauncherPaths::versionManifestFile()` из +[launcherpaths.h](launcherpaths.md). + +Требования сборки: `Qt6::Core` (`QDateTime`, `QHash`, `QList`, `QUrl`) и `Qt6::Network` +(`QNetworkAccessManager`). + +## Иерархия и роль + +Наследует `QObject`: мета-объектная система, два сигнала и владение по родителю. Виртуальных +методов базового класса не переопределяет. + +## Публичные структуры + +### RemoteVersionEntry + +Одна строка манифеста Mojang. + +| Поле | Тип | Описание | +|------|-----|----------| +| `id` | `QString` | Идентификатор версии: `1.21.8`, `25w33a`, `b1.7.3` | +| `type` | `QString` | Категория: `release`, `snapshot`, `old_beta` или `old_alpha` | +| `url` | `QUrl` | Адрес `.json` с описанием версии | +| `sha1` | `QString` | Контрольная сумма самого описания | +| `releaseTime` | `QDateTime` | Дата выпуска; по ней список сортируется новыми вперёд | + +Категории `type` — те же ключи, по которым окно выбора версии делит каталог на вкладки; версии, не +попавшие ни в одну из четырёх, интерфейс относит к категории «прочие». + +## Псевдонимы типов + +`VersionManifestService::Callback` — `std::function`. +Сочетание `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); +}); +``` + +--- + +При создании этого документа использовался ИИ. diff --git a/doc/cpp/javalocator.md b/doc/cpp/javalocator.md new file mode 100644 index 0000000..96e97ff --- /dev/null +++ b/doc/cpp/javalocator.md @@ -0,0 +1,77 @@ +# javalocator.h — JavaLocator + +## Обзор + +Для запуска Minecraft нужна Java подходящей версии: старым версиям игры — восьмая, новым — +семнадцатая или двадцать первая. Лаунчер умеет скачивать сборки Java сам (см. +[JavaInstaller](JavaInstaller.md)), но сначала стоит посмотреть, что уже есть на машине. + +`JavaLocator` отвечает за поиск: собирает все доступные java, определяет их версии и выбирает +подходящую под требование версии игры. Пространство имён используется при подготовке запуска и при +показе списка найденных Java в настройках. + +## Пространства имён + +`JavaLocator` группирует четыре функции поиска и выбора. Состояния нет, но результат определения +версии кэшируется внутри реализации. + +## Функции + +#### QStringList findAll(const QString &gameDir) + +Все java, которые удалось найти, без дублей и в порядке приоритета. Просматриваются четыре +источника: рантайм самого Minecraft внутри папки игры, переменная `JAVA_HOME`, переменная `PATH` и +стандартные каталоги установки JDK и JRE. + +Параметр `gameDir` — папка игры; из неё берётся первый источник. + +Результат показывается в диалоге настроек списком «что нашлось в системе». + +#### int majorVersion(const QString &javaPath) + +Мажорная версия java по её пути: 8, 17, 21 и так далее. Возвращает `0`, если запустить +исполняемый файл не удалось — путь неверен, файл не исполняемый или это не java. + +Результат кэшируется: определение версии требует запуска процесса, а один и тот же путь +проверяется многократно. + +#### QString windowlessVariant(const QString &javaPath) + +Заменяет `java.exe` на `javaw.exe`, чтобы игра не открывала окно консоли. На Unix возвращает вход +без изменений — там разницы нет. + +#### 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); +``` + +--- + +При создании этого документа использовался ИИ. diff --git a/doc/cpp/javaruntime.md b/doc/cpp/javaruntime.md new file mode 100644 index 0000000..dd79306 --- /dev/null +++ b/doc/cpp/javaruntime.md @@ -0,0 +1,153 @@ +# javaruntime.h — JavaRuntime, JavaRuntimeStore и JavaRequirement + +## Обзор + +`Minecraft_launcher` умеет скачивать Java сам: официальные сборки Mojang — те же, которыми игру +запускает официальный лаунчер, — и сборки Eclipse Temurin в вариантах JDK и JRE. Скачанные сборки +живут в папке лаунчера, по одной подпапке на сборку. + +Заголовок `javaruntime.h` — словарь этой части проекта. Он даёт перечисление видов сборок, две +структуры (строка каталога и уже установленная сборка), пространство имён для работы с папкой +`/java` и таблицу требований версий игры к Java. + +Заголовок подключают [JavaRuntimeService](JavaRuntimeService.md) (каталог), +[JavaInstaller](JavaInstaller.md) (скачивание и распаковка) и +[LauncherBackend](LauncherBackend.md) (отдача каталога в QML). + +## Типы + +| Имя | Вид | Описание | +|-----|-----|----------| +| `JavaRuntimeKind` | `enum class` | Откуда взялась сборка и что именно в ней лежит | +| `JavaRuntimeEntry` | `struct` | Строка каталога — то, что можно скачать | +| `InstalledJavaRuntime` | `struct` | Сборка, уже распакованная в папке лаунчера | + +### JavaRuntimeKind + +| Значение | Ключ | Описание | +|----------|------|----------| +| `Mojang` | `java` | Тот же рантайм, которым игру запускает официальный лаунчер | +| `Jdk` | `jdk` | Eclipse Temurin JDK: компилятор и инструменты в комплекте | +| `Jre` | `jre` | Eclipse Temurin JRE: только то, что нужно для запуска | + +Эти же ключи служат именами категорий в окне выбора Java. + +### JavaRuntimeEntry + +| Поле | Тип | Описание | +|------|-----|----------| +| `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` | Версия с длительной поддержкой; помечается в списке | + +Метод `isValid()` возвращает `true`, когда заполнен `id`, `major` больше нуля и `url` корректен. + +### 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 — папка `/java` + +Устройство папки: одна подпапка на сборку плюс её описание внутри. Отдельного индекса нет +намеренно — удалённую вручную папку не пришлось бы вычищать ещё и из общего файла. + +#### QString dirFor(const QString &id) + +Папка конкретной сборки внутри `/java`. + +#### QList<InstalledJavaRuntime> installed() + +Всё, что лежит в `/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` и `` — только 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); +} +``` + +--- + +При создании этого документа использовался ИИ. diff --git a/doc/cpp/launcherpaths.md b/doc/cpp/launcherpaths.md new file mode 100644 index 0000000..ba4f44a --- /dev/null +++ b/doc/cpp/launcherpaths.md @@ -0,0 +1,145 @@ +# launcherpaths.h — LauncherPaths + +## Обзор + +`Minecraft_launcher` хранит собственные данные отдельно от папки игры: настройки, профили, описания +сборок, архивы содержимого `.minecraft`, скачанные сборки Java и кэши каталогов. Пространство имён +`LauncherPaths` — единственное место, где эти пути вычисляются. + +Собственная папка `galeonLauncher` лежит рядом со стандартной `.minecraft`, а не в системном +каталоге данных приложения: так все файлы лаунчера остаются там же, где сама игра, и переносятся +вместе с ней. + +К этому заголовку обращается почти каждый сервис проекта — везде, где нужно прочитать или записать +файл в папке лаунчера. + +## Пространства имён + +`LauncherPaths` группирует функции, возвращающие абсолютные пути, и одну функцию создания корневой +папки. Состояния у пространства имён нет: все функции вычисляют путь заново при каждом вызове. + +## Функции + +### Корневые каталоги + +#### QString containerDir() + +Родительская папка, в которой лежит `.minecraft`, а рядом с ней — `galeonLauncher`. От неё +отсчитываются и папка игры по умолчанию, и корень данных лаунчера. + +#### QString rootDir() + +Корень данных лаунчера — `/galeonLauncher`. + +#### QString defaultMinecraftDir() + +Стандартная папка игры. Используется, когда пользователь не задал свою в настройках. + +#### bool ensureRootExists(QString *error = nullptr) + +Создаёт папку лаунчера, если её ещё нет. Вызывается при каждом запуске и перед каждой записью. + +Возвращает `false` и заполняет `error`, если папку не удалось создать или в неё не пишется. Все +остальные функции пространства имён только считают строки и в этом смысле не могут завершиться +неудачей — проверять доступность каталога нужно этой функцией. + +### Файлы состояния + +#### QString settingsFile() + +Файл настроек запуска: папка игры, путь к Java, память, аргументы JVM, размер окна. + +#### QString profilesFile() + +Файл профилей игрока. + +#### QString customBuildsFile() + +`/customBuilds.json` — пользовательские сборки. + +#### QString legacyCustomBuildsFile() + +`/versions.json` — как сборки назывались до переименования. Читается один раз при миграции и +больше ни для чего не нужен. + +### Сборки и их архивы + +#### QString buildStorageDir() + +`/builds` — архивы содержимого `.minecraft`, по одному на сборку. + +#### QString buildDir(int buildId) + +`/builds/` — папка одной сборки: её архив и, у сезонных, скачанный пак с описанием +установленной ревизии. + +#### QString seasonalStateFile(int buildId) + +`/builds//season.json` — какая ревизия сезонной сборки установлена и какие файлы она +принесла. Список файлов нужен, чтобы при обновлении убрать те, что из сборки ушли. + +### Java + +#### QString javaDir() + +`/java` — сборки Java, скачанные лаунчером. Каждая в своей подпапке, имя подпапки — +идентификатор сборки из каталога. + +#### QString javaCatalogFile() + +Слепок каталога доступных сборок Java с отметкой времени. + +#### QString javaDownloadDir() + +Каталог, куда качаются архивы Temurin до распаковки. + +### Кэши и загрузки + +#### QString cacheDir() + +`/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()); +``` + +--- + +При создании этого документа использовался ИИ. diff --git a/doc/cpp/main.md b/doc/cpp/main.md new file mode 100644 index 0000000..7b522cf --- /dev/null +++ b/doc/cpp/main.md @@ -0,0 +1,97 @@ +# main.cpp — точка входа + +## Обзор + +`Minecraft_launcher` — десктопный лаунчер Minecraft на Qt 6 Quick. Интерфейс написан на QML, +вся работа — авторизация, скачивание версий, установка модлоадеров и Java, запуск игры — лежит в +C++-классе [LauncherBackend](LauncherBackend.md) и его сервисах. + +`main.cpp` — стартовая последовательность приложения: здесь при необходимости инициализируется +Qt WebEngine, создаётся объект приложения, читается каталог переводов, выбирается стиль +Qt Quick Controls, загружается QML-модуль и запускается цикл событий. Файл намеренно короткий: +ни одного объекта предметной области он не создаёт — всё, что нужно, QML заводит сам. + +## Настройка приложения 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`. + +--- + +При создании этого документа использовался ИИ. diff --git a/doc/cpp/minecraftversion.md b/doc/cpp/minecraftversion.md new file mode 100644 index 0000000..f45290e --- /dev/null +++ b/doc/cpp/minecraftversion.md @@ -0,0 +1,136 @@ +# minecraftversion.h — MinecraftVersion и VersionLoader + +## Обзор + +Каждая версия Minecraft описывается файлом `versions//.json` — в нём главный класс, +аргументы запуска, список библиотек, индекс ресурсов и требуемая версия Java. Файлы образуют +цепочку: профиль модлоадера наследуется от версии игры через поле `inheritsFrom`, а та, в свою +очередь, может наследоваться дальше. + +Заголовок `minecraftversion.h` даёт две вещи: структуры, описывающие версию в разобранном виде, и +пространство имён `VersionLoader` — чтение, разворачивание цепочки наследования и работа с +установленными версиями на диске. + +Заголовок подключают [GameLauncher](GameLauncher.md) (собирает из версии командную строку), +[VersionInstaller](VersionInstaller.md) (по списку библиотек понимает, что качать) и +[LauncherBackend](LauncherBackend.md) (проверяет комплектность и удаляет версии). + +## Типы + +| Имя | Вид | Описание | +|-----|-----|----------| +| `MinecraftLibrary` | `struct` | Одна библиотека из `client.json` с уже разрешёнными правилами | +| `MinecraftVersion` | `struct` | `client.json`, «схлопнутый» по всей цепочке `inheritsFrom` | + +### MinecraftLibrary + +| Поле | Тип | Описание | +|------|-----|----------| +| `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` | Префиксы путей внутри архива, которые не распаковываются | + +### MinecraftVersion + +| Поле | Тип | Описание | +|------|-----|----------| +| `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` | Абсолютный путь к `.jar`; может лежать у родительской версии | +| `javaMajor` | `int` | Требуемая мажорная версия Java; по умолчанию `8` | +| `jvmArgs` | `QStringList` | Аргументы JVM ещё с неподставленными подстановками вида `${...}` | +| `gameArgs` | `QStringList` | Аргументы игры, тоже с неподставленными подстановками | +| `libraries` | `QList` | Библиотеки версии с уже применёнными правилами | +| `loggingArgument` | `QString` | Аргумент вида `-Dlog4j.configurationFile=${path}` | +| `loggingConfigPath` | `QString` | Абсолютный путь к xml-конфигурации журнала; пуст, если конфигурации нет | +| `supportsQuickPlay` | `bool` | Версия 1.20 и новее понимает `--quickPlayMultiplayer` — так лаунчер подключается к серверу сразу при запуске | +| `hasCustomResolutionArgs` | `bool` | Версия принимает аргументы размера окна | + +Метод `isValid()` возвращает `true`, когда заполнены и `id`, и `mainClass`: именно этим проверяется +успешность чтения версии. + +## Функции + +Пространство имён `VersionLoader`. + +#### QStringList installedVersions(const QString &gameDir) + +Версии, реально установленные в `/versions`: есть и папка, и файл `.json`. Одной +только папки недостаточно — она остаётся после неудачной установки. + +#### QStringList dependentsOf(const QString &gameDir, const QString &versionId) + +Установленные профили, у которых `inheritsFrom` равен `versionId`. Без базовой версии они не +запустятся, поэтому список показывается пользователю перед удалением версии. + +#### qint64 installedSize(const QString &gameDir, const QString &versionId) + +Размер `/versions/` в байтах; `0`, если папки нет. Используется в предупреждении об +удалении — версия весит десятки мегабайт, и стоит показать, сколько освободится. + +#### bool remove(const QString &gameDir, const QString &versionId, QString \*error) + +Сносит `/versions/`. Отсутствие папки считается успехом. При неудаче возвращает +`false` и заполняет `error`. + +Библиотеки и ресурсы в `libraries/` и `assets/` не трогаются: они общие для всех версий. + +#### MinecraftVersion load(const QString &gameDir, const QString &versionId, const QSet<QString> &features, QString \*error) + +Читает версию и разворачивает всю цепочку `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; +``` + +--- + +При создании этого документа использовался ИИ. diff --git a/doc/cpp/modloader.md b/doc/cpp/modloader.md new file mode 100644 index 0000000..fac7928 --- /dev/null +++ b/doc/cpp/modloader.md @@ -0,0 +1,87 @@ +# modloader.h — ModLoader + +## Обзор + +`Minecraft_launcher` умеет ставить четыре модлоадера: Minecraft Forge, Fabric Loader, NeoForge и +Quilt Loader. Заголовок `modloader.h` — общий словарь для всей этой части проекта: перечисление +самих лоадеров, описание одной их сборки и функции перевода между перечислением и строковым +ключом. + +Лоадеры взаимоисключающи: игра запускается ровно с одним профилем в `/versions`, поэтому в +сборке лаунчера хранится один ключ лоадера, а не набор. + +Заголовок подключают [ModLoaderVersionService](ModLoaderVersionService.md), +[ModLoaderInstaller](ModLoaderInstaller.md) и [LauncherBackend](LauncherBackend.md). + +## Типы + +| Имя | Вид | Описание | +|-----|-----|----------| +| `ModLoader` | `enum class` | Модлоадеры, которые лаунчер умеет ставить | +| `LoaderVersionEntry` | `struct` | Одна сборка модлоадера под конкретную версию игры | + +### ModLoader + +| Значение | Ключ | Название | Описание | +|----------|------|----------|----------| +| `Forge` | `forge` | Minecraft Forge | Старейший загрузчик; ставится собственным установщиком, который собирает часть файлов на месте | +| `Fabric` | `fabric` | Fabric Loader | Лёгкий загрузчик; профиль версии формируется из метаданных без запуска установщика | +| `NeoForge` | `neoforge` | NeoForge | Ответвление Forge; ставится так же собственным установщиком | +| `Quilt` | `quilt` | Quilt Loader | Ответвление Fabric; ставится так же, как Fabric | + +Перечисление объявлено как `enum class`, поэтому неявного приведения к целому нет. + +### LoaderVersionEntry + +| Поле | Тип | Описание | +|------|-----|----------| +| `loaderVersion` | `QString` | Версия самого лоадера — например `47.4.0`, `0.19.3` или `21.1.248` | +| `gameVersion` | `QString` | Версия Minecraft, под которую эта сборка — например `1.20.1` | +| `versionId` | `QString` | Предсказанный идентификатор профиля `versions/`. У 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` и ``. От классов проекта не зависит и сам подключается +всюду, где речь идёт о модлоадерах. + +## Пример использования + +```cpp +const auto loader = loaderFromKey(build.loader); +if (!loader) { + // сборка без модлоадера — запускаем чистую ваниль + return; +} + +qInfo() << "ставим" << loaderTitle(*loader) + << "версии" << entry.loaderVersion + << "под Minecraft" << entry.gameVersion; +``` + +--- + +При создании этого документа использовался ИИ. diff --git a/doc/cpp/zlibreference.md b/doc/cpp/zlibreference.md new file mode 100644 index 0000000..8c71b01 --- /dev/null +++ b/doc/cpp/zlibreference.md @@ -0,0 +1,76 @@ +# zlibreference.h — ZlibReference + +## Обзор + +`Minecraft_launcher` умеет ставить модлоадеры Forge и NeoForge, а их установщики — это Java-программы, +которые собирают часть jar-файлов прямо на машине пользователя и сверяют sha1 каждого собранного +файла с эталоном из `install_profile.json`. + +Эталон посчитан на обычном zlib. Дистрибутивы вроде CachyOS и Fedora подставляют вместо него +zlib-ng: сжатие корректное, но побайтово другое — поэтому установка падает на любой версии игры +сообщением «Processor failed, invalid outputs». Сменить Java не выйдет: сборки OpenJDK под Linux +берут `libz.so.1` из системы. + +`ZlibReference` — обход этой проблемы. Рядом с лаунчером лежит собранный обычный zlib, и процессу +установщика он подсовывается через `LD_PRELOAD`. Системный zlib-ng при этом остаётся на месте: +подмена живёт ровно один процесс. + +Пространство имён используется установщиком модлоадеров при подготовке окружения дочернего +процесса Java. + +## Пространства имён + +`ZlibReference` группирует три функции: проверку системной библиотеки, поиск собранного эталона и +подготовку окружения процесса. Состояния нет. + +## Функции + +#### bool systemIsZlibNg() + +Отвечает на вопрос, окажется ли `libz.so.1`, который достанется процессу java, библиотекой zlib-ng. +От ответа зависит, нужна ли подмена вообще: на системе с обычным zlib она бессмысленна. + +#### QString bundledPath() + +Путь к собранному рядом эталонному `libz.so.1` или пустая строка, если его нет. Библиотека +собирается целью `launcher_zlib_reference` в `CMakeLists.txt` из исходников каталога `zlib/`, +которые лежат в репозитории, чтобы сборка не зависела от сети, а версия была зафиксирована — от +неё зависит побайтовый результат сжатия. Собирается только на Linux: на Windows и macOS проблемы +подмены системного zlib нет. + +#### bool applyTo(QProcessEnvironment &env, QString *note = nullptr) + +Дописывает `LD_PRELOAD` в переданное окружение, если подмена нужна. Изменяет `env` на месте. + +Возвращает `true`, когда окружение готово, — в том числе в случае, когда подменять нечего: +система с обычным zlib или платформа, где вопрос не стоит. `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); +``` + +--- + +При создании этого документа использовался ИИ. diff --git a/doc/index.md b/doc/index.md new file mode 100644 index 0000000..91be0fe --- /dev/null +++ b/doc/index.md @@ -0,0 +1,165 @@ +# Minecraft Launcher — справочник по исходному коду + +Десктопный лаунчер Minecraft на Qt 6. Интерфейс написан на QML, вся работа — авторизация, +скачивание версий, установка модлоадеров и Java, запуск игры — лежит в C++. + +Документация разделена на две части: [QML-компоненты](#qml-компоненты) и +[C++-классы](#c-классы). + +## Как устроено приложение + +Слоёв четыре, и каждый общается только с соседними: + +1. **QML.** [Main](qml/Main.md) — единственное настоящее окно и точка входа. Оно держит + единственный экземпляр `LauncherBackend` и раздаёт его вложенным диалогам через свойство + `backend`. Ни один QML-файл не обращается к сервисам напрямую. +2. **Фасад.** [LauncherBackend](cpp/LauncherBackend.md) — главный класс, видимый из QML. + Хранит профили, сборки и настройки, владеет двенадцатью сервисами и сводит их состояние к + свойствам, которые читает интерфейс. Каталоги он отдаёт уже сведёнными с локальным состоянием, + поэтому окна показывают статус, не считая ничего сами. +3. **Сервисы.** Каталоги (версий, модлоадеров, Java, сезонных сборок), установщики, службы + авторизации, переключатель сборок и запуск игры. Каждый занят одним делом и ничего не знает об + интерфейсе. +Особняком стоит [Localization](cpp/Localization.md) — второй и последний тип, видимый из QML. +Это синглтон `Loc`, через который проходят все тексты интерфейса: `Loc.t.домен.вид.имя` в QML и +`Loc::text("домен.вид.имя")` в C++. Он ни от чего не зависит и доступен всем слоям сразу. + +4. **Внешний мир.** Сеть (Mojang, Ely.by, Microsoft, Adoptium, файловый сервер сборок), файловая + система (`.minecraft` и папка лаунчера) и дочерние процессы (java, установщики модлоадеров, + `tar`). + +Почти весь код работает в потоке GUI: параллелизм даёт асинхронная сеть, а не потоки. Единственное +исключение — операции над содержимым `.minecraft` (гигабайты модов и миров), вынесенные в +отдельный поток к [BuildArchiveWorker](cpp/BuildArchiveWorker.md). + +## QML-компоненты + +| Компонент | Описание | +|-----------|----------| +| [Main](qml/Main.md) | Главное окно и точка входа: экран запуска, профили, плашка сообщений, панели прогресса и внутренние диалоги | +| [BuildsDialog](qml/BuildsDialog.md) | Пользовательские сборки: список слева, карточка редактирования справа | +| [VersionPickerDialog](qml/VersionPickerDialog.md) | Выбор версии Minecraft: категории, поиск, удаление скачанных версий | +| [JavaPickerDialog](qml/JavaPickerDialog.md) | Выбор сборки Java; подтверждение при необходимости сразу начинает загрузку | +| [SeasonalBuildsDialog](qml/SeasonalBuildsDialog.md) | Каталог готовых сезонных сборок таблицей и установка одной кнопкой | +| [MicrosoftLoginDialog](qml/MicrosoftLoginDialog.md) | Окно входа в аккаунт Microsoft; собирается только с Qt WebEngine | +| [LoaderRow](qml/LoaderRow.md) | Одна строка модлоадера в карточке сборки: чекбокс и список версий | +| [ProgressPanel](qml/ProgressPanel.md) | Плашка хода долгой операции в левом нижнем углу | +| [DarkCombo](qml/DarkCombo.md) | Выпадающий список в тёмном стиле окна | +| [LabelledField](qml/LabelledField.md) | Подпись и поле ввода одной колонкой | + +## C++-классы + +### Точка входа + +| Файл | Описание | +|------|----------| +| [main.cpp](cpp/main.md) | Инициализация WebEngine, объект приложения, каталог переводов, стиль `Basic`, загрузка QML-модуля | + +### Фасад + +| Класс | Описание | +|-------|----------| +| [Localization](cpp/Localization.md) | Синглтон `Loc`: все тексты интерфейса в одном файле `i18n/translations.json`, переключение языка на лету | +| [LauncherBackend](cpp/LauncherBackend.md) | Единственный тип, видимый из QML: 26 свойств, 42 вызываемых метода, состояние лаунчера и порядок работы всех сервисов | + +### Авторизация и запуск + +| Класс | Описание | +|-------|----------| +| [AuthService](cpp/AuthService.md) | Вход через Ely.by и офлайн-режим; структура `AuthResult` | +| [MsaAuthService](cpp/MsaAuthService.md) | Вход через аккаунт Microsoft: OAuth2 → Xbox Live → XSTS → Minecraft Services | +| [GameLauncher](cpp/GameLauncher.md) | Сборка командной строки, распаковка нативных библиотек и запуск JVM | + +### Версии игры + +| Класс | Описание | +|-------|----------| +| [VersionManifestService](cpp/VersionManifestService.md) | Каталог версий Mojang с кэшем в папке лаунчера | +| [VersionInstaller](cpp/VersionInstaller.md) | Фоновая установка версии: jar, библиотеки, индекс ресурсов и сами ресурсы | + +### Модлоадеры + +| Класс | Описание | +|-------|----------| +| [ModLoaderVersionService](cpp/ModLoaderVersionService.md) | Списки версий Forge, Fabric, NeoForge и Quilt; совместимость заложена в структуру данных | +| [ModLoaderInstaller](cpp/ModLoaderInstaller.md) | Два пути установки под одним фасадом: готовое описание версии либо запуск `installer.jar` | + +### Java + +| Класс | Описание | +|-------|----------| +| [JavaRuntimeService](cpp/JavaRuntimeService.md) | Каталог сборок Mojang и Eclipse Temurin под текущую платформу | +| [JavaInstaller](cpp/JavaInstaller.md) | Установка сборки Java: архив Temurin либо дерево файлов Mojang | + +### Сборки и их содержимое + +| Класс | Описание | +|-------|----------| +| [BuildSwitcher](cpp/BuildSwitcher.md) | Порядок шагов смены активной сборки и восстановление после прерванной операции | +| [BuildArchiveWorker](cpp/BuildArchiveWorker.md) | Упаковка, очистка, распаковка и раскатка пака в отдельном потоке | + +### Сезонные сборки + +| Класс | Описание | +|-------|----------| +| [SeasonalBuildService](cpp/SeasonalBuildService.md) | Каталог готовых сборок с файлового сервера | +| [SeasonalPackDownloader](cpp/SeasonalPackDownloader.md) | Загрузка архива сборки потоком с проверкой sha256 | + +### Общие типы и утилиты + +| Файл | Описание | +|------|----------| +| [minecraftversion.h](cpp/minecraftversion.md) | Структуры версии и библиотеки, чтение и разворачивание цепочки `inheritsFrom` | +| [javaruntime.h](cpp/javaruntime.md) | Виды сборок Java, папка `/java` и таблица требований версий игры | +| [modloader.h](cpp/modloader.md) | Перечисление модлоадеров, запись версии и перевод ключей | +| [launcherpaths.h](cpp/launcherpaths.md) | Все пути к данным лаунчера в одном месте | +| [javalocator.h](cpp/javalocator.md) | Поиск установленной в системе Java и выбор подходящей версии | +| [zlibreference.h](cpp/zlibreference.md) | Подмена zlib-ng эталонным zlib для установщиков Forge и NeoForge | + +## Сборка + +Требуется Qt 6.8 или новее. Обязательные модули: `Quick`, `QuickControls2`, `Core`, `CorePrivate`, +`Gui`, `Network`. `CorePrivate` нужен ради `QZipReader` и `QZipWriter` — ими распаковываются +нативные библиотеки LWJGL и архивы сборок; привязка к версии Qt из-за приватного модуля — +осознанный выбор, и предупреждение о ней в `CMakeLists.txt` отключено. + +`Qt6::WebEngineQuick` необязателен, и это принципиально: модуль ставится отдельной галочкой в +установщике Qt и тянет за собой WebChannel с Positioning, которых в типовой установке нет. Если +сделать его обязательным, у любого, кто их не поставил, проект перестанет конфигурироваться +целиком — вместе с офлайном и Ely.by. Когда модуль найден, определяется макрос +`LAUNCHER_HAS_WEBENGINE`, а [MicrosoftLoginDialog.qml](qml/MicrosoftLoginDialog.md) добавляется в +QML-модуль; без модуля лаунчер собирается и работает как обычно, только вход через Microsoft +сообщает, что эта сборка его не умеет. В QML различие видно через свойство +`backend.microsoftAvailable`. + +QML-модуль объявлен как `qt_add_qml_module` с URI `Minecraft_launcher`; точка входа — +`engine.loadFromModule("Minecraft_launcher", "Main")`. Туда же, в список `RESOURCES`, попадает +каталог переводов `i18n/translations.json`. + +### Тексты интерфейса + +Все подписи, сообщения и ошибки лежат в одном файле `i18n/translations.json` и достаются через +синглтон `Loc` — подробности, правила именования ключей и порядок добавления языка описаны в +[Localization](cpp/Localization.md). Штатные `.ts`/`.qm` не используются, `lupdate` и `lrelease` +в сборке не участвуют. Целостность каталога проверяет `python3 tools/check_translations.py`. + +### Каталог `zlib/` + +В репозитории лежат исходники обычного zlib версии 1.3.1 — это чужой upstream-код, и +документацией он не покрыт. Он нужен вот зачем. + +Установщики Forge и NeoForge сверяют sha1 каждого jar, который сами же и собирают, с эталоном из +`install_profile.json`. Эталон посчитан на обычном zlib, а дистрибутивы вроде CachyOS и Fedora +подставляют вместо него zlib-ng: сжатие корректное, но побайтово другое, поэтому установка падает +на любой версии игры сообщением «Processor failed, invalid outputs». Сменить Java не выйдет — +сборки OpenJDK под Linux берут `libz.so.1` из системы. + +Поэтому на Linux собирается цель `launcher_zlib_reference` из этих исходников, и готовая +библиотека подсовывается через `LD_PRELOAD` только процессу установщика — см. +[zlibreference.h](cpp/zlibreference.md). Сам лаунчер с ней не линкуется, системный zlib-ng +остаётся на месте. Исходники хранятся в репозитории, чтобы сборка не зависела от сети, а версия +была зафиксирована: от неё зависит побайтовый результат сжатия. + +--- + +При создании этого документа использовался ИИ. diff --git a/doc/qml/BuildsDialog.md b/doc/qml/BuildsDialog.md new file mode 100644 index 0000000..59719ee --- /dev/null +++ b/doc/qml/BuildsDialog.md @@ -0,0 +1,179 @@ +# BuildsDialog + +## Обзор компонента + +`Minecraft_launcher` — десктопный лаунчер Minecraft на Qt Quick. Сборка в нём — это именованный +набор «версия игры + модлоадер + адрес сервера» вместе с собственным содержимым `.minecraft`: +модами, конфигами и мирами. Активная сборка одна, её содержимое лежит в `.minecraft`, остальные +хранятся в архивах и разворачиваются при переключении. + +`BuildsDialog` — окно управления этими сборками: слева список со сменой активной, справа карточка +выбранной — имя, сервер, версия Minecraft и модлоадеры. Отсюда же сборка ставится (скачивание +версии игры и модлоадера) и удаляется. + +Карточка сохраняет правки по ходу редактирования, отдельной кнопки «Сохранить» нет: иначе +появляется неочевидное несохранённое состояние, пока пользователь переключается между сборками в +левом списке. + +## Место в проекте и зависимости + +В начале файла объявлена `pragma ComponentBehavior: Bound`. Импортирует `QtQuick`, +`QtQuick.Controls 2.15` и `QtQuick.Layouts 2.15`. + +Использует три компонента из того же QML-модуля: + +- [LabelledField](LabelledField.md) — поля названия сборки и адреса сервера; +- [LoaderRow](LoaderRow.md) — по одной строке на каждый из четырёх модлоадеров; +- [VersionPickerDialog](VersionPickerDialog.md) — вложенное окно выбора версии Minecraft. + +Работает с C++-типом `LauncherBackend` (`launcherbackend.h`, `QML_ELEMENT` в модуле +`Minecraft_launcher`) через свойство `backend`. Читает свойства `customBuildNames`, +`activeBuildIndex`, `switching` и `busy`; вызывает `customBuildAt()`, `addCustomBuild()`, +`updateCustomBuild()`, `customBuildRemovalInfo()`, `removeCustomBuild()`, `checkInstallation()`, +`installCustomBuild()` и `refreshVersionCatalog()`; слушает сигнал `installedVersionsChanged`. + +Объявлен в `QML_FILES` модуля (`CMakeLists.txt`); использует ресурс `images/Trash.svg`. +Инстанцируется в [Main](Main.md). + +## Иерархия и роль + +Корневой тип — `Dialog`: модальный, 880×600 px, нулевой внутренний отступ, тёмный фон со +скруглением и акцентной рамкой. Пока идёт смена активной сборки (`backend.switching`), окно +закрывается только кнопкой: архивация и распаковка `.minecraft` не должны прерываться случайным +щелчком мимо. + +Содержимое — `RowLayout` из двух частей: + +- **Слева** (260 px) список сборок и строка «+ Новая сборка» под ним. Строка списка показывает имя, + пометку «активна» у активной сборки и корзину, появляющуюся при наведении. Вся колонка + выключается на время смены активной сборки. +- **Справа** карточка выбранной сборки внутри `Flickable` — она может не поместиться по высоте. + Карточка выключается и приглушается, когда сборка не выбрана или идёт переключение. + +Карточка сверху вниз: название, адрес сервера, поле версии Minecraft, панель модлоадеров и строка +состояния комплектности. Поле версии — не выпадающий список, а прямоугольник, который только +показывает выбор и открывает отдельное окно: версий около тысячи, и разбираться в них удобнее в +окне с категориями. + +Отдельно объявлен вложенный `Dialog` подтверждения удаления шириной 420 px с красной рамкой, +привязанный к тому же родителю, что и само окно. + +Подвал несёт три кнопки: «Установить», «Сделать активной» и «Закрыть». + +## Свойства + +| Свойство | Тип | По умолчанию | Обязательное | Описание | +|----------|-----|--------------|--------------|----------| +| `backend` | `var` | — | **Да** | Экземпляр `LauncherBackend` — хранилище сборок и исполнитель установки, удаления и переключения. | +| `editIndex` | `int` | `-1` | Нет | Индекс сборки, открытой в карточке справа. Значение `-1` означает, что карточка пуста и выключена. С активной сборкой не связан: активную меняет отдельная кнопка, чтобы случайный клик по списку не запускал архивацию `.minecraft`. | +| `loading` | `bool` | `false` | Нет | Признак того, что карточка сейчас заполняется данными сборки. Пока он выставлен, обработчики полей не должны писать обратно — иначе открытие сборки немедленно перезаписывало бы её. | + +### Внутренняя панель модлоадеров + +Панель `loaderPanel` — это `Column` с четырьмя строками [LoaderRow](LoaderRow.md) и собственным +маленьким API. Лоадеры несовместимы между собой: игра запускается ровно с одним профилем +`versions/`, поэтому отметка одного снимает остальные, а «ничего не отмечено» — это чистая +ваниль. + +| Свойство панели | Тип | Описание | +|-----------------|-----|----------| +| `rows` | `var` | Массив из четырёх строк лоадеров в порядке показа: Minecraft Forge (`forge`), Fabric Loader (`fabric`), NeoForge (`neoforge`), Quilt Loader (`quilt`). | + +Функции панели: `applyBuild(loader, loaderVersion)` раздаёт данные сборки всем строкам; +`selectedRow()` возвращает отмеченную строку или `null`; `keepOnly(row)` очищает все строки, +кроме переданной; `commitSelection()` сохраняет выбор в сборку, записывая ключ лоадера, его версию +и пустой `resolvedVersionId` — профиль появится только после установки, а до неё сборка +запускается на чистой ванили. + +## Сигналы + +Собственных сигналов компонент не объявляет: все изменения уходят прямо в бэкенд, и о них +остальной интерфейс узнаёт по его сигналам. + +## Методы + +#### selectBuild(int index) : void + +Открывает сборку с указанным индексом в карточке. Читает данные через `customBuildAt()`, +выставляет `loading` на время заполнения полей, раздаёт лоадеры панели через `applyBuild()` и в +конце обновляет строку состояния. + +#### askRemove(int index) : void + +Спрашивает подтверждение перед удалением: оно необратимо и уносит с собой архив сборки. Запрашивает +`customBuildRemovalInfo()` и наполняет окно подтверждения именем сборки и тремя признаками — есть +ли у неё архив, активна ли она сейчас и последняя ли она. Индекс запоминается в самом окне +подтверждения, потому что к моменту ответа строка списка под курсором может быть уже другой. + +Окно подтверждения объясняет последствия по этим признакам: вместе со сборкой удалится её архив, и +моды, конфиги и миры восстановить будет нельзя; активная сборка владеет содержимым `.minecraft`, +оно будет очищено, а на его место развернётся следующая сборка; для последней сборки содержимое +`.minecraft` остаётся на месте. + +#### performRemove(int index) : void + +Удаляет сборку через бэкенд и восстанавливает состояние карточки: если сборок не осталось, +сбрасывает `editIndex` в `-1`, иначе открывает соседнюю — ту же позицию или последнюю оставшуюся. + +#### commit(var fields) : void + +Сохраняет часть полей сборки: передаёт `QVariantMap` в `updateCustomBuild()` и обновляет строку +состояния. Ничего не делает во время заполнения карточки (`loading`) и при пустом `editIndex` — +это и есть защита от записи при открытии сборки. + +Вызывается по завершении правки каждого поля: имени, адреса сервера, версии Minecraft и выбора +модлоадера. + +#### refreshStatus() : void + +Пересчитывает строку комплектности под карточкой. Спрашивает у бэкенда `checkInstallation()` и +показывает либо сообщение о готовности к запуску, либо число недостающих файлов вместе с первым из +них. При пустом `editIndex` очищает строку. + +#### newBuildName() : string + +Придумывает имя для новой сборки: перебирает «Сборка 1», «Сборка 2» и так далее, пока не найдёт +свободное среди существующих имён. + +## Взаимодействие с другими компонентами + +**Со стороны родителя.** [Main](Main.md) задаёт `backend` и открывает окно. Начальное состояние +окно выбирает само в обработчике `onAboutToShow`: просит обновить каталог версий и открывает +активную сборку, а при пустом списке оставляет карточку выключенной. + +**Внутрь — к вложенным компонентам.** Поля [LabelledField](LabelledField.md) сообщают о правке +сигналом `editingFinished`, и карточка сразу вызывает `commit()` с обрезанным по краям значением. +Поле версии открывает [VersionPickerDialog](VersionPickerDialog.md) вызовом `openFor()` и получает +результат сигналом `versionChosen`; запись выбранной версии сама вызывает `commit()` через +обработчик изменения. Строки [LoaderRow](LoaderRow.md) получают `gameVersion` привязкой к +выбранной версии игры, поэтому смена версии Minecraft автоматически перезапрашивает списки версий +лоадеров. + +**Наружу — к бэкенду.** Кнопка «Установить» вызывает `installCustomBuild()`, кнопка «Сделать +активной» пишет в свойство `activeBuildIndex`. Обе выключены, пока лаунчер занят (`busy`), а +кнопка активации — ещё и когда выбранная сборка уже активна. Ход установки и переключения +показывает плашка [ProgressPanel](ProgressPanel.md) главного окна, а не это окно. + +**Обратная связь от бэкенда.** Окно подписано на `installedVersionsChanged` через `Connections`: +установка версии или модлоадера меняет комплектность сборки, и строка состояния должна это +заметить, не дожидаясь переоткрытия окна. + +## Пример использования + +```qml +BuildsDialog { + id: buildsDialog + parent: Overlay.overlay + anchors.centerIn: parent + backend: launcherBackend +} + +Button { + text: qsTr("Сборки") + onClicked: buildsDialog.open() +} +``` + +--- + +При создании этого документа использовался ИИ. diff --git a/doc/qml/DarkCombo.md b/doc/qml/DarkCombo.md new file mode 100644 index 0000000..ba8fa29 --- /dev/null +++ b/doc/qml/DarkCombo.md @@ -0,0 +1,89 @@ +# DarkCombo + +## Обзор компонента + +`Minecraft_launcher` — десктопный лаунчер Minecraft на Qt Quick. Всё окно оформлено вручную в +тёмной теме, а стиль Qt Quick Controls принудительно выставлен в `Basic` (`main.cpp`), потому что +нативные стили игнорируют пользовательские `contentItem` и `background`. Из-за этого каждый +стандартный элемент управления, попадающий в интерфейс, приходится переопределять самому. + +`DarkCombo` — как раз такое переопределение: выпадающий список в общем тёмном стиле окна. +Компонент не добавляет логики, он целиком про внешний вид — фон, рамку, стрелку, делегат строки и +всплывающую панель. К нему обращаются всюду, где нужен выпадающий список внутри диалогов: выбор +версии модлоадера, выбор Java, поля в настройках. + +## Место в проекте и зависимости + +Импортирует `QtQuick` и `QtQuick.Controls 2.15`; собственных C++-типов не использует. + +Объявлен в `QML_FILES` модуля `Minecraft_launcher` (см. `CMakeLists.txt`), поэтому доступен по +имени `DarkCombo` в любом файле того же модуля без явного импорта. + +Используется в [LoaderRow](LoaderRow.md) — список версий модлоадера — и дважды в [Main](Main.md): +в выпадающих списках профиля и сборки на главном окне. + +Компонент ссылается на ресурсы `images/Profile_Box/Asset_23.svg` и +`images/Profile_Box/Asset_24.svg` — они перечислены в `RESOURCES` того же QML-модуля, отдельного +подключения не требуют. + +## Иерархия и роль + +Корневой тип — `ComboBox` из Qt Quick Controls. Всё поведение (модель, `currentIndex`, +`activated`, `displayText`, клавиатурная навигация) наследуется без изменений; `DarkCombo` +переопределяет только четыре визуальных слота базового типа: + +| Слот | Что даёт `DarkCombo` | +|------|----------------------| +| `indicator` | стрелка из SVG-ресурса вместо двойного шеврона Basic-стиля; при открытом списке (`down`) картинка меняется | +| `contentItem` | текст текущего значения белым, с обрезкой справа многоточием и отступом под стрелку | +| `background` | тёмный прямоугольник со скруглением 6 px; рамка подсвечивается акцентным цветом при фокусе | +| `delegate` | строка списка: белый текст, подсветка фона у элемента под курсором | +| `popup` | всплывающая панель шириной с сам комбобокс, высотой не более 220 px, с вертикальным индикатором прокрутки | + +## Свойства + +Собственных свойств компонент не объявляет — доступен весь набор свойств `ComboBox` +(`model`, `currentIndex`, `currentText`, `displayText`, `editable` и прочие). + +Внутри `delegate` объявлены два обязательных свойства делегата, они относятся к строке списка, +а не к самому комбобоксу: + +| Свойство | Тип | По умолчанию | Обязательное | Описание | +|----------|-----|--------------|--------------|----------| +| `modelData` | `var` | — | **Да** | Значение строки модели; выводится текстом в строке списка. Модель ожидается плоской — списком строк, а не объектов. | +| `index` | `int` | — | **Да** | Позиция строки; сравнивается с `highlightedIndex` комбобокса, чтобы подсветить строку под курсором. | + +## Сигналы + +Собственных сигналов нет. Наследуются сигналы `ComboBox`, из которых на практике используется +`activated(int index)` — выбор строки пользователем (в отличие от `currentIndexChanged`, он не +срабатывает при программной смене значения). + +## Методы + +Собственных функций нет. + +## Взаимодействие с другими компонентами + +Компонент самодостаточен и ничего не знает ни о бэкенде, ни о родителе: модель приходит извне +через `model`, результат выбора родитель получает через унаследованный `activated`. Так, +[LoaderRow](LoaderRow.md) передаёт в `model` список подписей версий модлоадера и в обработчике +`activated` переводит индекс обратно в номер версии. + +Поскольку модель ожидается списком строк, вызывающий код обычно сам приводит массив объектов к +массиву подписей перед присваиванием. + +## Пример использования + +```qml +DarkCombo { + width: 200 + height: 32 + model: ["1.21.1", "1.20.6", "1.20.4"] + onActivated: (index) => console.log("выбрано:", model[index]) +} +``` + +--- + +При создании этого документа использовался ИИ. diff --git a/doc/qml/JavaPickerDialog.md b/doc/qml/JavaPickerDialog.md new file mode 100644 index 0000000..1880e95 --- /dev/null +++ b/doc/qml/JavaPickerDialog.md @@ -0,0 +1,149 @@ +# JavaPickerDialog + +## Обзор компонента + +`Minecraft_launcher` — десктопный лаунчер Minecraft на Qt Quick. Разным версиям игры нужны разные +версии Java, и лаунчер умеет скачивать их сам: официальные сборки Mojang и сборки Temurin в двух +вариантах — JDK и JRE. Держать в голове, какая Java нужна какой версии игры, пользователь не +обязан. + +`JavaPickerDialog` — окно выбора сборки Java: слева типы сборок, справа сами версии с поиском. +Устроено так же, как [VersionPickerDialog](VersionPickerDialog.md), с одним принципиальным +отличием: выбранное здесь ещё и качается. У версий игры загрузку начинает сама сборка, а сборка +Java, которой нет на диске, запуску ничем не поможет — поэтому кнопка подтверждения при +необходимости сразу ставит выбранную сборку в очередь на скачивание. + +Окно также показывает, какая версия Java нужна выбранной версии игры, помечает сборки, которые +для неё слишком старые, и позволяет удалять скачанное прямо из списка. + +## Место в проекте и зависимости + +В начале файла объявлена `pragma ComponentBehavior: Bound`. Импортирует `QtQuick`, +`QtQuick.Controls 2.15` и `QtQuick.Layouts 2.15`. + +Работает с C++-типом `LauncherBackend` (`launcherbackend.h`, `QML_ELEMENT` в модуле +`Minecraft_launcher`), передаваемым снаружи в свойство `backend`. Читает свойства `javaCatalog` и +`javaCatalogLoading`, вызывает `refreshJavaCatalog()`, `installJavaRuntime()` и +`removeJavaRuntime()`. + +Объявлен в `QML_FILES` модуля (`CMakeLists.txt`); использует ресурс `images/Trash.svg`. +Инстанцируется в диалоге настроек внутри [Main](Main.md) — сборка Java выбирается один раз для +всего лаунчера, а не отдельно для каждой сборки Minecraft. + +Каталог приходит из C++ уже отсортированным — новые сверху, скачанные первыми, — поэтому окно +только отбирает записи и порядок не трогает. Запись каталога содержит поля `id`, `label`, `kind` +(тип сборки), `major` (мажорная версия Java числом), `installed`, `downloadable`, `lts`, `sizeMb`, +`detail`, `coverage` и `search`. + +## Иерархия и роль + +Корневой тип — `Dialog`: модальный, 720×480 px, нулевой внутренний отступ, закрывается по Escape и +щелчку мимо, оформление тёмное со скруглением и акцентной рамкой. + +Содержимое — `RowLayout` из колонки типов шириной 170 px и области версий, разделённых линией. +Каждый пункт колонки типов показывает название и пояснение мелким шрифтом, а под списком типов — +подсказка о требовании выбранной версии игры, видимая только когда это требование известно. + +Область версий повторяет устройство окна выбора версии Minecraft: поле поиска сверху, флажок +«Только скачанные» снизу, `ListView` между ними. Строка списка выше обычной (44 px), потому что +содержит две строки текста: подпись сборки с меткой LTS и строку подробностей, где через точку +собраны описание, покрытие версий игры, размер в мегабайтах и — при необходимости — +предупреждение о том, что сборки не хватит. + +Пустое состояние объясняется текстом по центру и различает загрузку каталога, отсутствие +соединения и пустой результат поиска. + +## Свойства + +| Свойство | Тип | По умолчанию | Обязательное | Описание | +|----------|-----|--------------|--------------|----------| +| `backend` | `var` | — | **Да** | Экземпляр `LauncherBackend` — источник каталога Java, исполнитель установки и удаления. | +| `selectedId` | `string` | `""` | Нет | Идентификатор выбранной сборки. Окно открывается с текущей сборкой, и по «Отмене» выбор возвращается к ней. | +| `category` | `string` | `"java"` | Нет | Ключ активного типа сборок. Допустимые значения: `java` (сборки Mojang), `jdk` (Temurin с инструментами), `jre` (Temurin, только запуск). | +| `filterText` | `string` | `""` | Нет | Текст поиска; сравнивается в нижнем регистре с полем `search` записи внутри активного типа. | +| `installedOnly` | `bool` | `false` | Нет | Показывать только скачанные сборки. | +| `requiredMajor` | `int` | `0` | Нет | Мажорная версия Java, ниже которой выбранной версии игры не запуститься. Значение `0` означает, что версия игры не выбрана и предупреждать не о чем: подсказка в колонке типов скрывается, пометка «слишком старая» не ставится. | +| `categories` | `var` (только чтение) | список из трёх записей | Нет | Описание колонки типов: массив объектов с полями `key`, `title` и `hint`. Задаёт порядок пунктов и набор допустимых значений `category`. | +| `visibleEntries` | `var` (только чтение) | вычисляется | Нет | Отобранные строки каталога: записи активного типа, прошедшие флажок «только скачанные» и текст поиска. Модель списка. | +| `selectedEntry` | `var` (только чтение) | вычисляется | Нет | Полная запись выбранной сборки из каталога или `null`, если ничего не выбрано. Ищется по всему каталогу, а не по видимым строкам, поэтому выбор не теряется при смене фильтра. От неё зависят подпись в подвале и надпись на кнопке подтверждения. | + +## Сигналы + +#### runtimeChosen(string runtimeId) + +Сборка Java подтверждена — кнопкой, двойным щелчком по строке или клавишей Enter в поле поиска. В +параметре приходит идентификатор сборки. + +Обработчик записывает выбранную сборку в настройки лаунчера — сборка Java общая для всех сборок +Minecraft, а требование конкретной версии игры влияет только на пометки в списке. Сигнал испускается и для ещё не скачанной сборки — загрузка при +этом начинается сама, и обработчику ждать её завершения не нужно. + +## Методы + +#### openFor(string runtimeId, int required) : void + +Открывает окно на переданной сборке. Запоминает её в `selectedId`, выставляет `requiredMajor` из +второго аргумента (отсутствующее или нулевое значение означает «требование неизвестно»), очищает +поиск, переключается на тип именно этой сборки, просит бэкенд обновить каталог и прокручивает +список к выбранной строке. + +#### categoryOf(string runtimeId) : string + +Возвращает тип сборки по каталогу; для неизвестного идентификатора — `java`. + +#### indexOfSelected() : int + +Позиция выбранной сборки в `visibleEntries` или `-1`, если под текущим фильтром её не видно. + +#### revealSelected() : void + +Выставляет текущий индекс списка на выбранную сборку и прокручивает список так, чтобы строка +оказалась по центру. + +#### acceptSelection() : void + +Подтверждает выбор. Ничего не делает, если `selectedEntry` пуст. Иначе испускает +`runtimeChosen()`, а затем — если сборка не установлена, но доступна для скачивания, — вызывает +`installJavaRuntime()` у бэкенда и закрывает окно. Именно поэтому кнопка подтверждения называется +«Скачать» для отсутствующей сборки и «Выбрать» для уже скачанной. + +## Взаимодействие с другими компонентами + +**Со стороны родителя.** Вызывающий код задаёт `backend`, открывает окно вызовом `openFor()` с +текущей сборкой и требуемой мажорной версией Java (её отдаёт `requiredJavaMajor()` бэкенда) и +подписывается на `runtimeChosen()`. + +**Со стороны бэкенда.** `javaCatalog` и `javaCatalogLoading` — привязки, пересчитывающие модель и +текст пустого состояния при каждом обновлении каталога. `refreshJavaCatalog()` вызывается при +открытии окна, `installJavaRuntime()` — при подтверждении отсутствующей сборки, +`removeJavaRuntime()` — по щелчку на корзине в строке. Ход самой загрузки окно не показывает: за +это отвечает плашка [ProgressPanel](ProgressPanel.md) в главном окне. + +**Удаление.** Корзина в строке доступна только у скачанных сборок; сборки весят по двести +мегабайт, и удалять их нужно прямо здесь, иначе папка лаунчера растёт молча. Если удалена была +выбранная сборка, выбор снимается. + +**Слишком старые сборки.** Сборка ниже требования игры остаётся доступной для выбора, но +помечается в строке подробностей: она может пригодиться для другой сборки Minecraft. + +## Пример использования + +```qml +JavaPickerDialog { + id: javaPicker + x: (window.width - width) / 2 + y: (window.height - height) / 2 + backend: launcherBackend + onRuntimeChosen: (runtimeId) => settingsDialog.javaRuntimeId = runtimeId +} + +MouseArea { + anchors.fill: javaField + onClicked: javaPicker.openFor(settingsDialog.javaRuntimeId, + launcherBackend.requiredJavaMajor(launcherBackend.activeBuildIndex)) +} +``` + +--- + +При создании этого документа использовался ИИ. diff --git a/doc/qml/LabelledField.md b/doc/qml/LabelledField.md new file mode 100644 index 0000000..0c87099 --- /dev/null +++ b/doc/qml/LabelledField.md @@ -0,0 +1,76 @@ +# LabelledField + +## Обзор компонента + +`Minecraft_launcher` — десктопный лаунчер Minecraft на Qt Quick с полностью самостоятельно +оформленным тёмным интерфейсом. В диалоге настроек и в редакторе сборок много однотипных полей +ввода: подпись сверху, поле под ней. `LabelledField` собирает эту пару в один компонент, чтобы +отступы, цвета и подсветка фокуса не переписывались в каждом месте заново. + +Компонент нужен там, где пользователь вводит короткое значение: имя профиля, объём памяти, путь, +аргументы запуска. + +## Место в проекте и зависимости + +Импортирует `QtQuick` и `QtQuick.Controls 2.15`; C++-типы не используются. + +Объявлен в `QML_FILES` модуля `Minecraft_launcher` (`CMakeLists.txt`) и доступен по имени внутри +модуля без импорта. Применяется в диалоге настроек и в карточке сборки — см. [Main](Main.md) и +[BuildsDialog](BuildsDialog.md). + +## Иерархия и роль + +Корневой тип — `Column` с расстоянием 3 px между элементами. Колонка содержит два потомка: +`Text` с подписью (серый, 11 px) и `TextField` фиксированной высоты 32 px с тёмным фоном, +скруглением 6 px и рамкой, которая при фокусе поля меняет цвет на акцентный. + +Ширина поля привязана к ширине самой колонки, поэтому размер задаётся снаружи одним свойством +`width` корневого элемента. Высоту `Column` вычисляет сам. + +## Свойства + +| Свойство | Тип | По умолчанию | Обязательное | Описание | +|----------|-----|--------------|--------------|----------| +| `text` | `string` (алиас на `text` внутреннего поля) | `""` | Нет | Содержимое поля ввода. Работает в обе стороны: чтение возвращает введённое значение, запись подставляет новое. | +| `validator` | `var` (алиас на `validator` внутреннего поля) | `null` | Нет | Валидатор ввода — например `IntValidator` для числовых полей. Ограничивает то, что пользователь может набрать. | +| `label` | `string` | `""` | Нет | Текст подписи над полем. | +| `placeholder` | `string` | `""` | Нет | Подсказка, показываемая в пустом поле приглушённым цветом. | + +## Сигналы + +#### editingFinished() + +Проброшен из внутреннего `TextField`: срабатывает, когда правка закончена — поле потеряло фокус +или пользователь нажал Enter. Промежуточные нажатия клавиш сигнала не вызывают. + +Обработчик обычно сохраняет введённое значение: читает `text` и передаёт его в бэкенд или в +модель родительского диалога. Именно из-за этой семантики поля настроек сохраняются по завершении +правки, а не на каждый символ. + +## Методы + +Собственных функций нет. + +## Взаимодействие с другими компонентами + +Компонент не знает ни о бэкенде, ни о содержащем его диалоге. Родитель задаёт `label`, +`placeholder`, начальный `text` и при необходимости `validator`, а затем подписывается на +`editingFinished`, чтобы записать значение. Двусторонней привязки к бэкенду внутри компонента нет +— решение о том, когда и куда сохранять, целиком за родителем. + +## Пример использования + +```qml +LabelledField { + width: parent.width + label: "Оперативная память, МБ" + placeholder: "2048" + text: String(settings.memoryMb) + validator: IntValidator { bottom: 512; top: 32768 } + onEditingFinished: settings.memoryMb = parseInt(text) +} +``` + +--- + +При создании этого документа использовался ИИ. diff --git a/doc/qml/LoaderRow.md b/doc/qml/LoaderRow.md new file mode 100644 index 0000000..ee3ab5f --- /dev/null +++ b/doc/qml/LoaderRow.md @@ -0,0 +1,150 @@ +# LoaderRow + +## Обзор компонента + +`Minecraft_launcher` — десктопный лаунчер Minecraft на Qt Quick. Кроме чистой игры он умеет +ставить модлоадеры: Forge, Fabric, NeoForge и Quilt. В карточке сборки лоадер выбирается +чекбоксом, а под ним — конкретная версия лоадера. + +`LoaderRow` — одна такая строка: чекбокс с названием лоадера и, когда он отмечен, выпадающий +список версий именно под выбранную версию Minecraft. Компонент берёт на себя всю возню со +списком версий: подтягивает его из кэша, обновляет по сети, следит, чтобы выбранная версия +всегда существовала под текущую версию игры, и словами объясняет случай «лоадер эту версию игры +не поддерживает» вместо показа пустого списка. + +Совместимость компонент не проверяет и проверять не должен: бэкенд отдаёт список, уже собранный +под конкретную версию игры, поэтому несовместимой строки в нём не бывает. Пустой список — это и +есть отсутствие поддержки. + +## Место в проекте и зависимости + +Импортирует `QtQuick` и `QtQuick.Controls 2.15`. + +Использует компонент [DarkCombo](DarkCombo.md) из того же QML-модуля — выпадающий список версий в +тёмном стиле окна. + +Работает с C++-типом `LauncherBackend` (`launcherbackend.h`, зарегистрирован через `QML_ELEMENT` +в модуле `Minecraft_launcher`), который передаётся снаружи в свойство `backend`. Из него +компонент вызывает `loaderVersions()`, `refreshLoaderVersions()` и `loaderVersionsLoading()`, а +также слушает сигнал `loaderVersionsChanged`. + +Объявлен в `QML_FILES` модуля `Minecraft_launcher` (`CMakeLists.txt`). Инстанцируется в +[BuildsDialog](BuildsDialog.md) — по одной строке на каждый поддерживаемый лоадер. + +## Иерархия и роль + +Корневой тип — `Column` с расстоянием 4 px. Внутри три потомка, видимость которых +взаимоисключающая по нижней части: + +- `CheckBox` с полностью переопределённым индикатором (квадрат со скруглением и галочкой) и + подписью. Выключен, пока не выбрана версия игры. +- [DarkCombo](DarkCombo.md) со списком версий лоадера — виден, только когда чекбокс отмечен и + список непустой. +- Текстовая строка на месте списка — видна, когда чекбокс отмечен, а список пуст. Пока идёт + запрос, она серая и говорит о загрузке; когда запрос закончен, она красноватая и сообщает, что + лоадер не поддерживает выбранную версию Minecraft. + +Ширина внутренних элементов считается от ширины колонки, поэтому снаружи достаточно задать +`width`. + +## Свойства + +| Свойство | Тип | По умолчанию | Обязательное | Описание | +|----------|-----|--------------|--------------|----------| +| `backend` | `var` | — | **Да** | Экземпляр `LauncherBackend`. Через него запрашиваются и обновляются списки версий лоадера. | +| `loaderKey` | `string` | — | **Да** | Ключ лоадера, которым он опознаётся в бэкенде и в сохранённой сборке: `forge`, `fabric`, `neoforge`, `quilt`. | +| `title` | `string` | — | **Да** | Человекочитаемое название лоадера рядом с чекбоксом; оно же подставляется в сообщения о загрузке и об отсутствии поддержки. | +| `gameVersion` | `string` | `""` | Нет | Версия Minecraft, выбранная в карточке. Пустая строка выключает чекбокс. Смена значения сбрасывает выбранную версию лоадера и перезапрашивает список. | +| `checked` | `bool` (алиас на чекбокс) | `false` | Нет | Отмечен ли лоадер. Чтение даёт текущее состояние, запись переключает чекбокс программно — без сигнала `userChecked()`. | +| `selectedVersion` | `string` | `""` | Нет | Выбранная версия лоадера. Устанавливается только через `applyEntries()`, поэтому всегда либо пуста, либо присутствует в текущем списке. | +| `entries` | `var` | `[]` | Нет | Текущий список версий лоадера — массив записей, у каждой есть поля `version` (значение) и `label` (подпись для списка). Заполняется из бэкенда. | +| `applying` | `bool` | `false` | Нет | Признак того, что строку сейчас заполняет карточка данными сохранённой сборки. Пока он выставлен, изменения не считаются пользовательскими и сигнал `changed()` не испускается. | + +## Сигналы + +#### userChecked() + +Пользователь сам отметил чекбокс (не программная установка `checked`). Карточка сборки в ответ +снимает отметки с остальных строк лоадеров: одновременно в `.minecraft` может жить только один +лоадер. + +#### changed() + +Отметка или версия лоадера изменились и это изменение пользовательское. Обработчик — карточка +сборки — сохраняет сборку с новыми значениями. + +Сигнал сознательно не испускается, пока выставлен `applying`, то есть при заполнении строки из +уже сохранённой сборки: иначе загрузка карточки сразу же приводила бы к её перезаписи. + +## Методы + +#### versionIndex(string version) : int + +Возвращает позицию версии в текущем массиве `entries` или `-1`, если такой версии в списке нет. +Вспомогательная функция для синхронизации выбранного значения с выпадающим списком. + +#### applyEntries(var list) : void + +Единственное место, где меняются `entries` и `selectedVersion`. Через него проходят все три пути +получения списка — кэш, ответ сети и заполнение из сохранённой сборки, — потому что выбранная +версия обязана существовать в списке под текущую версию игры. + +Записывает новый список, а затем проверяет выбранную версию: если её в списке нет, подставляет +первую строку (список отсортирован новыми вперёд, поэтому первая — максимально доступная под эту +версию игры) или пустую строку для пустого списка. Если подстановка изменила значение и строка не +находится в режиме `applying`, испускает `changed()`. В конце синхронизирует `currentIndex` +выпадающего списка. + +#### reload() : void + +Перезапрашивает список версий. Если лоадер не отмечен или версия игры не выбрана, очищает список +через `applyEntries([])`. Иначе сначала берёт список из кэша бэкенда (`loaderVersions()`) — он +появляется мгновенно, — а затем просит обновление по сети (`refreshLoaderVersions()`), результат +которого придёт позже сигналом. + +#### applyBuild(string loader, string loaderVersion) : void + +Заполняет строку данными сохранённой сборки, не испуская `changed()`: на время работы выставляет +`applying`. Отмечает чекбокс, если ключ лоадера сборки совпадает с `loaderKey`, подставляет версию +из сборки как пожелание и вызывает `reload()`. Если под выбранную версию игры такой версии +лоадера нет, `applyEntries()` заменит её на максимально доступную. + +## Взаимодействие с другими компонентами + +**Что приходит извне.** Карточка сборки в [BuildsDialog](BuildsDialog.md) задаёт `backend`, +`loaderKey`, `title` и привязывает `gameVersion` к версии Minecraft, выбранной в карточке. +Заполнение сохранённой сборкой идёт вызовом `applyBuild()` снаружи. + +**Что уходит наружу.** По `userChecked()` карточка снимает отметки с остальных строк — набор +строк она держит в собственном списке. По `changed()` карточка сохраняет сборку, читая `checked` и +`selectedVersion`. + +**Бэкенд.** Компонент сам подписан на сигнал `loaderVersionsChanged(key, game)` через +`Connections`: пришедшее обновление принимается, только если ключ и версия игры совпадают с +текущими и чекбокс отмечен, — иначе ответ относится к другой строке или устарел. Текст в пустом +состоянии опрашивает `loaderVersionsLoading()`, чтобы отличать «ещё грузим» от «не поддерживается». + +**Реакция на смену версии игры.** Обработчик `onGameVersionChanged` сбрасывает `selectedVersion` и +вызывает `reload()`: сборка лоадера привязана к версии игры, поэтому под новой версией прежний +выбор недействителен. + +## Пример использования + +```qml +LoaderRow { + id: forgeRow + width: parent.width + + backend: launcherBackend + loaderKey: "forge" + title: "Forge" + gameVersion: buildCard.gameVersion + + onUserChecked: buildCard.keepOnly(forgeRow) + onChanged: buildCard.commitSelection() +} +``` + +--- + +При создании этого документа использовался ИИ. diff --git a/doc/qml/Main.md b/doc/qml/Main.md new file mode 100644 index 0000000..82e3e31 --- /dev/null +++ b/doc/qml/Main.md @@ -0,0 +1,212 @@ +# Main + +## Обзор компонента + +`Minecraft_launcher` — десктопный лаунчер Minecraft на Qt Quick. `Main.qml` — его главное и +единственное настоящее окно: точка входа приложения, которую загружает `main.cpp` вызовом +`engine.loadFromModule("Minecraft_launcher", "Main")`. + +Окно совмещает четыре роли. Оно держит единственный экземпляр `LauncherBackend` — весь остальной +интерфейс получает его от главного окна. Оно рисует сам экран запуска: фоновая картинка, большая +кнопка игры по центру, выпадающий список профилей, кнопка активной сборки, кнопки папки модов, +настроек и сезонных сборок. Оно показывает обратную связь — всплывающую плашку сообщений и две +панели хода долгих операций. И наконец, оно объявляет диалоги, которые не вынесены в отдельные +файлы: создание и редактирование профиля, ввод кода двухфакторной аутентификации и настройки +запуска. + +## Место в проекте и зависимости + +Импортирует `QtQuick`, `QtQuick.Layouts 2.15`, `QtQuick.Controls 2.15` и сам QML-модуль проекта +`Minecraft_launcher`, из которого приходит тип `LauncherBackend`. + +Инстанцирует четыре компонента модуля: [ProgressPanel](ProgressPanel.md) (дважды), +[SeasonalBuildsDialog](SeasonalBuildsDialog.md), [BuildsDialog](BuildsDialog.md), +[JavaPickerDialog](JavaPickerDialog.md), а также [DarkCombo](DarkCombo.md) и +[LabelledField](LabelledField.md) внутри своих диалогов. +[MicrosoftLoginDialog](MicrosoftLoginDialog.md) создаётся динамически — см. ниже. + +Стиль Qt Quick Controls принудительно выставлен в `Basic` в `main.cpp`, потому что нативные стили +игнорируют пользовательские `contentItem` и `background`; поэтому в этом файле почти каждый +элемент управления переопределяет своё оформление вручную. + +Использует ресурсы из `RESOURCES` QML-модуля: фоновую картинку, три состояния кнопки запуска, по +три состояния кнопок папки и настроек, стрелки выпадающих списков, `images/Trash.svg` и +`images/Pencil.svg`. + +## Иерархия и роль + +Корневой тип — `Window` размером 1280×720 px, видимое при старте. Это не переиспользуемый +компонент, а точка входа приложения, поэтому раздел с примером использования здесь неприменим. + +Раскладка держится на якорях относительно центральной кнопки запуска: список профилей — слева +сверху от неё, кнопка активной сборки — справа сверху, кнопки папки и настроек — под списком +профилей. Кнопка сезонных сборок стоит в правом нижнем углу: это единственная свободная часть +окна, потому что панели хода работ висят слева, а всё остальное собрано вокруг кнопки запуска. + +Панели загрузки и смены сборки имеют одни и те же якоря — они взаимоисключающи по построению: +признак занятости бэкенда не даёт начать переключение во время установки и наоборот. Панель смены +сборки объявлена неотменяемой: отступать после очистки `.minecraft` некуда, операцию нужно довести +до конца. + +Заголовки и подвалы всех диалогов сделаны на `Item` с явным `implicitHeight`, а не на +`Rectangle`: у прямоугольника `implicitHeight` равен нулю независимо от заданной высоты, и +`Dialog` не смог бы вычислить свою полную высоту. + +## Свойства + +| Свойство | Тип | По умолчанию | Обязательное | Описание | +|----------|-----|--------------|--------------|----------| +| `microsoftLoginDialog` | `var` | `null` | Нет | Созданный по требованию экземпляр окна входа Microsoft либо `null`, пока вход ни разу не запускался. Хранится в свойстве, чтобы окно создавалось один раз за сеанс. | + +### Внутренние диалоги и их состояние + +`Main.qml` объявляет четыре диалога прямо в файле. Их свойства — часть состояния главного окна. + +**Диалог редактирования профиля** (`editProfileDialog`): + +| Свойство | Тип | По умолчанию | Описание | +|----------|-----|--------------|----------| +| `editIndex` | `int` | `-1` | Индекс редактируемого профиля; `-1` — диалог не открыт ни для кого. | +| `msProfile` | `bool` | `false` | Открытый профиль имеет тип «Microsoft». | +| `msLinked` | `bool` | `false` | У профиля есть действующая сессия Microsoft — от этого зависит строка статуса и подпись кнопки входа. | +| `msName` | `string` | `""` | Ник, полученный при официальной авторизации. Показывается отдельным полем только для чтения, а не подменой поля логина: привязка сломалась бы первым же вводом в поле логина обычного профиля. | + +**Диалог двухфакторной аутентификации** (`twoFactorDialog`): + +| Свойство | Тип | По умолчанию | Описание | +|----------|-----|--------------|----------| +| `profileName` | `string` | `""` | Имя профиля, для которого запрошен код; подставляется в текст просьбы. | + +**Диалог настроек** (`settingsDialog`): + +| Свойство | Тип | По умолчанию | Описание | +|----------|-----|--------------|----------| +| `javaRuntimeId` | `string` | `""` | Выбранная сборка Java из папки лаунчера. Живёт в свойстве, а не в поле ввода: её выбирают в отдельном окне, а записывается она только по «Сохранить». Пустая строка означает «искать Java в системе». | +| `javaRuntimeInfo` | `var` | `null` | Подробности выбранной сборки для строки поля. Не привязка: `javaRuntimeInfo()` — обычный вызов, и сам он не пересчитается, когда сборка докачается, поэтому значение обновляется по событиям. | + +Типы профилей во всех выпадающих списках кодируются одинаково: позиция 0 — `offline` +(офлайн, без пароля), 1 — `elyby` (Ely.by, с логином и паролем), 2 — `microsoft` (лицензия). +Третья позиция показывается, только когда лаунчер собран с Qt WebEngine; в диалоге редактирования +она показывается ещё и тогда, когда профиль уже сохранён как лицензионный — иначе в сборке без +WebEngine он молча стал бы офлайновым. + +## Сигналы + +Собственных сигналов главное окно не объявляет. + +## Методы + +#### showToast(string text, color color, int timeout) : void + +Показывает единую всплывающую плашку сообщений: через неё проходят сообщения о ходе запуска, +ошибки и статус игры. Задаёт текст и цвет фона и перезапускает таймер скрытия. + +Параметр `timeout` — время показа в миллисекундах. Пропущенное значение означает четыре секунды; +`0` означает «держать до следующего сообщения» — так показываются промежуточные шаги запуска, чтобы +сообщение не исчезало посреди долгой операции. + +#### openMicrosoftLogin(url) : void + +Открывает окно входа в аккаунт Microsoft на переданном адресе, создавая его при первом вызове. + +Окно создаётся по требованию, а не вместе с главным: `MicrosoftLoginDialog.qml` попадает в модуль +только в сборках с Qt WebEngine, и обычная декларация сломала бы всё главное окно в остальных. +Поэтому компонент загружается через `Qt.createComponent()`, и если он не готов — сборка собрана без +WebEngine, — вход отменяется у бэкенда, а пользователю показывается сообщение о том, что окно +недоступно. При успешном создании окну сразу передаётся бэкенд, а его сигнал `failed` +подключается к плашке сообщений. + +#### formatMb(bytes) : string + +Переводит байты в мегабайты с одним знаком после запятой. Используется в строке подробностей +панели загрузки. + +## Взаимодействие с другими компонентами + +### Бэкенд + +Единственный экземпляр `LauncherBackend` объявлен прямо в окне и передаётся всем вложенным +диалогам через их свойство `backend`. Главное окно — единственное место, где обрабатываются его +сигналы: + +| Сигнал бэкенда | Что делает главное окно | +|----------------|-------------------------| +| `launched(profileName, buildName, serverUrl)` | показывает зелёное сообщение о запуске | +| `launchProgress(message)` | показывает сообщение без таймаута — до следующего шага | +| `launchError(message)` | показывает ошибку на восемь секунд | +| `twoFactorRequired(profileName)` | открывает диалог ввода кода: Ely.by отклонил пароль с пометкой two factor, и код добирается здесь, чтобы продолжить прерванный запуск | +| `gameFinished(exitCode, crashed)` | сообщает о закрытии игры; аварийное завершение показывается красным вместе с кодом выхода | +| `microsoftLoginUrlReady(url)` | вызывает `openMicrosoftLogin()` | +| `microsoftLoginSucceeded(playerName)` | сообщает об успешном входе. Выбор в списке профилей при этом не трогается: новый профиль уже выбран тем, кто его создал, а повторный вход мог быть и не в последний профиль | +| `microsoftLoginFailed(message)` | показывает ошибку на восемь секунд | +| `microsoftReloginRequired(profileIndex)` | сразу начинает вход заново для этого профиля | +| `gameOutput(line)` | пишет строку в консоль | +| `seasonalInstallFinished(seasonalId, buildName)` | сообщает, что сезонная сборка установлена и её можно запускать | +| `javaRuntimeInstalled(runtimeId)` | обновляет подробности выбранной сборки Java в настройках | + +Привязки к свойствам бэкенда управляют доступностью интерфейса: кнопка запуска выключена, пока +лаунчер занят или игра уже идёт; кнопка активной сборки — пока идёт игра или переключение сборок; +подпись на ней берётся из `activeBuildName`, а список профилей — из `profileNames`. + +### Профили + +Выпадающий список профилей переопределён целиком: кнопка «+ Добавить профиль» закреплена сверху +всплывающей панели, под ней список, где у строки при наведении появляются карандаш и корзина. +Карандаш открывает диалог редактирования (`openFor()` заполняет его через `profileAt()`), корзина +вызывает `removeProfile()`. + +Создание профиля вызывает `addProfile()`, выбирает новый профиль в списке и, если тип — +«Microsoft», сразу начинает вход: такой профиль без входа бесполезен. Скрытые поля при сохранении +не читаются — в них мог остаться текст, набранный до переключения типа профиля. + +В диалоге редактирования кнопка входа перед вызовом `startMicrosoftLogin()` сначала сохраняет +профиль вызовом `updateProfile()` с типом `microsoft`: тип мог быть только что переключён, и без +этого бэкенд приписал бы токены профилю другого типа. + +### Запуск игры + +Кнопка запуска вызывает `launchGame()` с индексом выбранного профиля и индексом активной сборки. +Дальше всё идёт через сигналы бэкенда: промежуточные шаги — в плашку сообщений, запрос кода +двухфакторной аутентификации — в отдельный диалог, где подтверждение вызывает +`submitTwoFactorCode()`, а отмена — `cancelPendingLaunch()`. + +### Настройки + +Диалог настроек открывается методом `load()`, который читает `settings()` бэкенда и раскладывает +значения по полям, а также подставляет разрешённый путь папки игры и список найденных в системе +сборок Java (`detectedJava()`). Сохранение собирает все поля в один `QVariantMap` и передаёт его +в `updateSettings()`. + +Первым пунктом диалога идёт выбор языка интерфейса — настройка уровня приложения, поэтому она +стоит над параметрами запуска. Подписи в модели переводятся, а коды (`system`, `ru`, `en`) лежат +рядом отдельным списком `codes` и не переводятся. Применяется язык по кнопке «Сохранить», как и +всё остальное в этом диалоге, и сразу же, без перезапуска: см. [Localization](../cpp/Localization.md). + +Разрешённый путь папки игры хранится свойством `resolvedGameDir` диалога, а не присваивается +тексту напрямую — иначе подпись не пережила бы смену языка. + +Поле выбора сборки Java открывает [JavaPickerDialog](JavaPickerDialog.md), передавая текущий выбор +и требование активной сборки (`requiredJavaMajor()`); крестик справа сбрасывает выбор обратно на +поиск Java в системе. Само окно выбора объявлено рядом с настройками, а не внутри них: оно шире и +центрируется по окну лаунчера. + +Выбранная сборка Java — общая настройка лаунчера: когда она задана, запуск идёт ею, а путь к Java +из соседнего поля остаётся запасным вариантом. + +### Тексты + +Все подписи, сообщения и подсказки окна берутся из синглтона `Loc`: `Loc.t.домен.вид.имя`. +Ни одного текстового литерала в разметке не осталось, `qsTr` не используется. Модель типов входа +в диалогах профиля — тоже ключ каталога (`Loc.t.profile.authTypes`), причём порядок значений +в нём значим: код сравнивает `currentIndex` с 1 и 2, а вариант без Microsoft получается из той же +модели через `.slice(0, 2)`. Подробности — в [Localization](../cpp/Localization.md). + +### Прочие кнопки + +Кнопка папки вызывает `openMinecraftFolder()`, кнопка настроек открывает диалог настроек, кнопка +сезонных сборок — [SeasonalBuildsDialog](SeasonalBuildsDialog.md) методом `openCatalog()`, кнопка +активной сборки — [BuildsDialog](BuildsDialog.md). + +--- + +При создании этого документа использовался ИИ. diff --git a/doc/qml/MicrosoftLoginDialog.md b/doc/qml/MicrosoftLoginDialog.md new file mode 100644 index 0000000..1b5085f --- /dev/null +++ b/doc/qml/MicrosoftLoginDialog.md @@ -0,0 +1,121 @@ +# MicrosoftLoginDialog + +## Обзор компонента + +`Minecraft_launcher` — десктопный лаунчер Minecraft на Qt Quick. Он поддерживает несколько +способов входа: офлайн-профиль, сервер Ely.by и учётную запись Microsoft. Последний путь требует +показать пользователю настоящую страницу входа Microsoft и дождаться, пока браузер уйдёт на +`redirect_uri` с кодом авторизации в адресе — ровно так же поступает официальный лаунчер. + +`MicrosoftLoginDialog` — окно с этой страницей. Внутри него живёт `WebEngineView`; диалог следит +за сменой адреса, отдаёт перехваченный код бэкенду и закрывается. Собственной логики разбора +адреса у него нет — она в C++, чтобы правила совпадения совпадали с теми, по которым сервис сам +строит `redirect_uri`. + +## Место в проекте и зависимости + +Импортирует `QtQuick`, `QtQuick.Controls 2.15` и `QtWebEngine`. В начале файла объявлена +`pragma ComponentBehavior: Bound`. + +Работает с C++-типом `LauncherBackend` (`launcherbackend.h`, `QML_ELEMENT`), который передаётся +снаружи в свойство `backend`. Вызывает у него `inspectMicrosoftRedirect()`, +`finishMicrosoftLogin()` и `cancelMicrosoftLogin()`. + +**Особенность сборки.** Это единственный QML-файл проекта, который попадает в модуль условно. +`CMakeLists.txt` ищет `Qt6WebEngineQuick` через `find_package(... QUIET)`; модуль объявлен +необязательным сознательно — он ставится отдельной галочкой в установщике Qt и тянет за собой +WebChannel с Positioning, которых в типовой установке нет. Если модуль найден, файл добавляется в +`QML_FILES` и определяется макрос `LAUNCHER_HAS_WEBENGINE`; если нет — лаунчер собирается и +работает как прежде, только без входа через Microsoft. В QML это различие видно через свойство +`backend.microsoftAvailable`, и интерфейс не должен предлагать этот путь, когда оно ложно. + +Инстанцируется динамически из [Main](Main.md) — главное окно создаёт диалог по требованию, потому +что при сборке без WebEngine самого типа в модуле не существует. + +## Иерархия и роль + +Корневой тип — `Dialog` из Qt Quick Controls: модальный, 560×680 px, по центру родителя, с нулевым +внутренним отступом и `closePolicy: Popup.NoAutoClose` — окно нельзя закрыть щелчком мимо или +клавишей Escape, выход только через кнопку отмены или успешный вход. + +Оформление задано вручную: тёмный фон со скруглением и акцентной рамкой, заголовок с +разделительной линией, подвал с кнопкой «Отмена». + +Содержимое — `WebEngineView` во всю площадь с отступом 12 px и индикатор занятости по центру, +видимый на время загрузки страницы. Рядом объявлен `WebEngineProfilePrototype` без `storageName`: +профиль без имени хранилища означает профиль без диска, поэтому куки живут только пока работает +лаунчер и в общий браузер не попадают. За выбор аккаунта в пределах сессии отвечает параметр +`prompt=select_account` в адресе входа, который формирует бэкенд. + +## Свойства + +| Свойство | Тип | По умолчанию | Обязательное | Описание | +|----------|-----|--------------|--------------|----------| +| `backend` | `var` | — | **Да** | Экземпляр `LauncherBackend`. Разбирает перехваченный адрес и завершает или отменяет вход. | +| `codeTaken` | `bool` | `false` | Нет | Код авторизации уже отдан бэкенду. Защита от повторной обработки: `WebEngineView` успевает сообщить об изменении адреса несколько раз, и без этого признака код ушёл бы дважды. Сбрасывается в `openAt()` и принудительно выставляется при отмене. | + +## Сигналы + +#### failed(string message) + +Вход не завершён: адрес совпал с `redirect_uri`, но кода в нём нет — например, пользователь +отказался выдать разрешение, или Microsoft вернула ошибку. В параметре приходит текст ошибки от +бэкенда, а если его нет — сообщение по умолчанию о незавершённом входе. + +Окно ничего не знает про тосты главного окна, поэтому о неудаче сообщает сигналом. Обработчик в +[Main](Main.md) показывает это сообщение пользователю. К моменту испускания сигнала диалог уже +закрыт, а вход у бэкенда отменён — обработчику остаётся только уведомить. + +Отмена по кнопке сигнала не испускает: пользователь и так знает, что закрыл окно. + +## Методы + +#### openAt(string url) : void + +Открывает диалог на переданном адресе страницы входа. Сбрасывает `codeTaken`, загружает адрес в +`WebEngineView` и показывает окно. Адрес формирует бэкенд — в нём уже присутствуют `redirect_uri`, +идентификатор клиента и `prompt=select_account`. + +#### handleUrl(url) : void + +Обработчик смены адреса в `WebEngineView`; вызывать снаружи не нужно. Ничего не делает, если код +уже перехвачен. Иначе передаёт адрес в `backend.inspectMicrosoftRedirect()` и смотрит на поле +`matched` ответа: если адрес не является `redirect_uri`, обработка на этом заканчивается — это +обычная навигация по страницам входа. + +При совпадении выставляет `codeTaken`, закрывает окно и дальше расходится по двум путям: непустое +поле `code` уходит в `backend.finishMicrosoftLogin()`, иначе вход отменяется через +`backend.cancelMicrosoftLogin()` и испускается сигнал `failed()` с текстом из поля `error`. + +## Взаимодействие с другими компонентами + +**Со стороны главного окна.** [Main](Main.md) создаёт диалог динамически (функция +`openMicrosoftLogin()`), задаёт `backend`, вызывает `openAt()` с адресом от бэкенда и +подписывается на `failed()`, чтобы показать тост с ошибкой. Показывать ли кнопку входа через +Microsoft вообще, главное окно решает по `backend.microsoftAvailable`. + +**Со стороны бэкенда.** Диалог только доставляет код: `inspectMicrosoftRedirect()` — разбор +адреса, `finishMicrosoftLogin()` — продолжение обмена кода на токены, `cancelMicrosoftLogin()` — +сброс начатой сессии входа. Результат входа диалогу не возвращается: об успехе главное окно +узнаёт от бэкенда по его собственным сигналам, а диалог к этому моменту уже закрыт. + +**Кнопка отмены.** Выставляет `codeTaken`, закрывает окно и отменяет вход у бэкенда. Признак +ставится до закрытия, чтобы последний сигнал об изменении адреса при закрытии не был обработан. + +## Пример использования + +```qml +MicrosoftLoginDialog { + id: msLogin + parent: Overlay.overlay + backend: launcherBackend + onFailed: (message) => showToast(message, "#cc6666", 4000) +} + +// открывать только в сборке с Qt WebEngine +Component.onCompleted: if (launcherBackend.microsoftAvailable) msLogin.openAt(loginUrl) +``` + +--- + +При создании этого документа использовался ИИ. diff --git a/doc/qml/ProgressPanel.md b/doc/qml/ProgressPanel.md new file mode 100644 index 0000000..0d31567 --- /dev/null +++ b/doc/qml/ProgressPanel.md @@ -0,0 +1,90 @@ +# ProgressPanel + +## Обзор компонента + +`Minecraft_launcher` — десктопный лаунчер Minecraft на Qt Quick. Почти каждое действие в нём +долгое: скачивание версии игры, установка Java, распаковка и архивация папки `.minecraft` при +смене сборки, загрузка сезонной сборки. Лаунчер не блокирует окно на это время, поэтому ход +операции нужно показывать неотрывно от остального интерфейса. + +`ProgressPanel` — та самая плашка прогресса. Она размещается в левом нижнем углу главного окна, +где не перекрывает кнопку запуска, сообщение по центру и кнопки папки и настроек. Компонент +только отображает переданное состояние; сам он ничего не считает и ни за чем не следит. + +## Место в проекте и зависимости + +Импортирует `QtQuick` и `QtQuick.Controls 2.15`; C++-типы напрямую не использует. + +Объявлен в `QML_FILES` модуля `Minecraft_launcher` (`CMakeLists.txt`), поэтому доступен по имени +внутри модуля без импорта. Инстанцируется в [Main](Main.md) — по одной плашке на вид долгой +операции. + +## Иерархия и роль + +Корневой тип — `Rectangle` фиксированного размера 320×72 px со скруглением 8 px, тёмной заливкой, +акцентной рамкой и лёгкой полупрозрачностью, чтобы плашка читалась поверх фонового изображения +окна. + +Внутри — пять элементов без внешних зависимостей: заголовок слева сверху, проценты справа сверху, +полоса прогресса (дорожка и заполнение с плавной анимацией ширины на 120 мс), строка подробностей +снизу и крестик отмены в правом нижнем углу с увеличенной областью нажатия. + +## Свойства + +| Свойство | Тип | По умолчанию | Обязательное | Описание | +|----------|-----|--------------|--------------|----------| +| `title` | `string` | `""` | Нет | Заголовок операции в левом верхнем углу, полужирным. Длинный текст обрезается справа многоточием. | +| `status` | `string` | `""` | Нет | Строка состояния внизу плашки. Показывается только когда `detail` пуст. | +| `fraction` | `double` | `-1` | Нет | Доля выполнения от `0` до `1`. Значение `-1` означает «итог ещё неизвестен»: вместо процентов выводится многоточие, а полоса остаётся пустой. | +| `detail` | `string` | `""` | Нет | Необязательная вторая строка подробностей — мегабайты у загрузки, путь у архивации. Если задана, вытесняет `status`. Длинный текст обрезается посередине, чтобы у пути были видны и начало, и конец. | +| `cancellable` | `bool` | `true` | Нет | Показывать ли крестик отмены. Ставится в `false` для операций, которые прерывать нельзя. | + +## Сигналы + +#### cancelRequested() + +Пользователь нажал крестик в правом нижнем углу. Сигнал сообщает только о намерении: плашка не +скрывает себя и не меняет своё состояние. + +Обработчик должен сам остановить операцию в бэкенде и убрать плашку с экрана — как правило, вызвав +соответствующий метод отмены у `LauncherBackend`; видимость плашки при этом снимется сама, потому +что она привязана к свойству занятости бэкенда. + +Сигнал не испускается при `cancellable: false` — в этом случае крестик скрыт. + +## Методы + +Собственных функций нет. + +## Взаимодействие с другими компонентами + +Все пять свойств плашки — точки внешней привязки. В [Main](Main.md) они связаны со свойствами +`LauncherBackend`: у загрузки версии это группа `downloading` / `downloadProgress` / +`downloadVersion` / `downloadStatus` / `downloadBytesDone` / `downloadBytesTotal`, у смены сборки — +`switching` / `switchProgress` / `switchStage` / `switchStatus`. Байты в мегабайты переводит +функция `formatMb()` главного окна, а не сама плашка. + +Видимостью плашки управляет родитель, обычно привязывая её к тому же признаку занятости, который +питает `fraction`. Сигнал `cancelRequested` родитель замыкает на метод отмены бэкенда. + +## Пример использования + +```qml +ProgressPanel { + anchors.left: parent.left + anchors.bottom: parent.bottom + anchors.margins: 16 + visible: backend.downloading + + title: qsTr("Загрузка Minecraft %1").arg(backend.downloadVersion) + status: backend.downloadStatus + fraction: backend.downloadProgress + detail: formatMb(backend.downloadBytesDone) + " / " + formatMb(backend.downloadBytesTotal) + + onCancelRequested: backend.cancelDownload() +} +``` + +--- + +При создании этого документа использовался ИИ. diff --git a/doc/qml/SeasonalBuildsDialog.md b/doc/qml/SeasonalBuildsDialog.md new file mode 100644 index 0000000..970f411 --- /dev/null +++ b/doc/qml/SeasonalBuildsDialog.md @@ -0,0 +1,135 @@ +# SeasonalBuildsDialog + +## Обзор компонента + +`Minecraft_launcher` — десктопный лаунчер Minecraft на Qt Quick. Кроме сборок, которые +пользователь собирает сам, он умеет ставить готовые сезонные сборки с сервера лаунчера: набор +модов под конкретную версию игры и модлоадер, подготовленный заранее и выдаваемый целиком. + +`SeasonalBuildsDialog` — окно этого каталога: таблица со всем, что нужно знать, чтобы решить, +ставить сборку или нет, и одна кнопка, которая делает всё остальное — заводит сборку, ставит +версию игры, модлоадер, Java и раскладывает файлы. Окно также показывает, что установленная +сборка устарела, и предлагает обновить её до свежей ревизии. + +Таблица собрана из строк `Row` с фиксированными колонками, а не из `TableView`: в проекте нет ни +одной модели `QAbstractItemModel`, а строки приходят готовыми `QVariantMap` — заводить ради семи +колонок отдельную модель незачем. + +## Место в проекте и зависимости + +В начале файла объявлена `pragma ComponentBehavior: Bound`. Импортирует `QtQuick` и +`QtQuick.Controls 2.15` — слои `QtQuick.Layouts` здесь не нужны, вся раскладка на якорях и +фиксированных ширинах колонок. + +Работает с C++-типом `LauncherBackend` (`launcherbackend.h`, `QML_ELEMENT` в модуле +`Minecraft_launcher`), передаваемым снаружи в свойство `backend`. Читает свойства +`seasonalCatalog`, `seasonalCatalogLoading`, `seasonalCatalogError`, `seasonalInstalling` и `busy`, +вызывает `refreshSeasonalCatalog()` и `installSeasonalBuild()`. + +Объявлен в `QML_FILES` модуля (`CMakeLists.txt`). Инстанцируется в [Main](Main.md). + +Строки каталога приходят из C++ уже отсортированными и сведёнными с локальными записями — окно +показывает статус, не считая ничего само. Запись содержит поля `id`, `name`, `description`, +`minecraftVersion`, `loaderTitle`, `loaderVersion`, `modCount`, `seasonStart`, `seasonEnd`, +`status`, `revision`, `installedRevision`, `updateAvailable`, `sizeBytes` и `serverUrl`. + +## Иерархия и роль + +Корневой тип — `Dialog`: модальный, 960×560 px, нулевой внутренний отступ, тёмный фон со +скруглением и акцентной рамкой. + +Политика закрытия зависит от состояния: пока идёт установка, окно закрывается только кнопкой +(`Popup.NoAutoClose`), потому что случайный щелчок мимо не должен спрятать единственную видимую +отмену; в остальное время работают Escape и щелчок мимо. + +Содержимое — шапка таблицы (`Row` с `Repeater` по `columns`), разделительная линия, список строк и +сообщение по центру для пустого состояния. Строка списка — `Rectangle` высотой 36 px с вложенным +`Row`, который повторяет тот же набор колонок; ширины берутся из общего описания `columns`, +поэтому шапка и строки не могут разъехаться. + +Подвал высотой 76 px несёт описание и размер выбранной сборки (они длинные и в таблицу не +помещаются, а решение принимается именно по ним) и две кнопки — обновления списка и установки. + +## Свойства + +| Свойство | Тип | По умолчанию | Обязательное | Описание | +|----------|-----|--------------|--------------|----------| +| `backend` | `var` | — | **Да** | Экземпляр `LauncherBackend` — источник каталога сезонных сборок и исполнитель установки. | +| `selectedId` | `string` | `""` | Нет | Идентификатор выбранной строки. Хранится по `id`, а не по индексу: список обновляется под руками, и индекс после обновления указывал бы на другую сборку. | +| `columns` | `var` (только чтение) | список из семи колонок | Нет | Описание таблицы: массив объектов с полями `key` (поле записи), `title` (заголовок), `width` (ширина в пикселях) и `align` (выравнивание). Колонки: название, версия, загрузчик, число модов, начало и конец сезона, статус. Ширины собраны в одном месте, потому что их повторяют и шапка, и делегат строки. | +| `entries` | `var` (только чтение) | `backend.seasonalCatalog` | Нет | Строки каталога напрямую из бэкенда. Отбора и сортировки в окне нет. | +| `selectedEntry` | `var` (только чтение) | вычисляется | Нет | Полная запись выбранной сборки или `null`, если ничего не выбрано. От неё зависят подвал и доступность кнопки установки. | + +## Сигналы + +Собственных сигналов компонент не объявляет. Результат работы окна виден через состояние бэкенда: +установка меняет список сборок и запускает загрузку, за ходом которой следит главное окно. + +## Методы + +#### openCatalog() : void + +Открывает окно. Обновление каталога происходит само в обработчике `onAboutToShow`, который просит +у бэкенда `refreshSeasonalCatalog(false)` — без принудительного обхода кэша: свежий кэш отвечает +без сети, поэтому вызов при каждом открытии ничего не стоит. + +#### formatMb(bytes) : string + +Переводит размер в байтах в строку с мегабайтами и одним знаком после запятой. Для нулевого или +отсутствующего значения возвращает пустую строку, чтобы размер просто не попал в строку подвала. + +#### cellText(entry, string key) : string + +Возвращает текст ячейки для записи и ключа колонки. Для всех колонок это значение одноимённого +поля записи, приведённое к строке; исключение — колонка загрузчика, где название и версия +склеиваются в одну подпись, а при пустой версии остаётся только название. + +#### installSelected() : void + +Ставит выбранную сборку. Ничего не делает, если строка не выбрана или лаунчер занят другой +операцией: установка занимает и панель загрузки, и `.minecraft` целиком, поэтому вторую начинать +нельзя. Иначе вызывает `installSeasonalBuild()` у бэкенда. + +Вызывается кнопкой установки и двойным щелчком по строке. + +## Взаимодействие с другими компонентами + +**Со стороны родителя.** [Main](Main.md) задаёт `backend` и открывает окно вызовом +`openCatalog()`. Обратной связи наружу через сигналы нет. + +**Со стороны бэкенда.** Всё содержимое таблицы — привязка к `seasonalCatalog`, поэтому обновление +каталога и изменение статуса установленной сборки перерисовывают окно сами. Пустое состояние +различает три случая по `seasonalCatalogLoading` и `seasonalCatalogError`: идёт загрузка, сборок +пока нет, произошла ошибка — её текст показывается прямо на месте строк, потому что пустой список +и ошибка выглядят одинаково пустыми. + +**Занятость.** Кнопка установки выключается по общему признаку `busy`, кнопка обновления списка — +по `seasonalCatalogLoading` (её подпись при этом меняется на «Обновление…»). Политика закрытия +окна завязана на `seasonalInstalling`. + +**Обновление ревизии.** Если у записи выставлен `updateAvailable`, статус в таблице подсвечивается +акцентным цветом, в подвале дописывается установленная ревизия, а кнопка установки называется +«Обновить». Отдельного пути обновления нет — это тот же вызов `installSeasonalBuild()`. + +**Ход установки.** Окно не показывает прогресс: за это отвечает плашка +[ProgressPanel](ProgressPanel.md) в главном окне, а отмена — метод `cancelSeasonalInstall()` +бэкенда. + +## Пример использования + +```qml +SeasonalBuildsDialog { + id: seasonalDialog + parent: Overlay.overlay + backend: launcherBackend +} + +Button { + text: qsTr("Сезонные сборки") + onClicked: seasonalDialog.openCatalog() +} +``` + +--- + +При создании этого документа использовался ИИ. diff --git a/doc/qml/VersionPickerDialog.md b/doc/qml/VersionPickerDialog.md new file mode 100644 index 0000000..69da248 --- /dev/null +++ b/doc/qml/VersionPickerDialog.md @@ -0,0 +1,158 @@ +# VersionPickerDialog + +## Обзор компонента + +`Minecraft_launcher` — десктопный лаунчер Minecraft на Qt Quick. Каталог версий Mojang — это около +тысячи записей: релизы, снапшоты, старые беты и альфы. Раньше выбор версии был обычным выпадающим +списком на всю эту тысячу, и найти в нём, скажем, бету 1.7 можно было только поиском по точному +номеру. + +`VersionPickerDialog` заменил тот список отдельным окном: слева категории, справа сами версии с +поиском сверху. Категории делят каталог на обозримые части, а поиск работает внутри выбранной. +Помимо выбора окно умеет удалять уже скачанные версии — с предупреждением о последствиях, потому +что версия весит десятки мегабайт, а поверх неё могут стоять профили модлоадеров. + +Окно открывается из карточки сборки, когда пользователь выбирает, на какой версии Minecraft +собирается играть. + +## Место в проекте и зависимости + +В начале файла объявлена `pragma ComponentBehavior: Bound`. Импортирует `QtQuick`, +`QtQuick.Controls 2.15` и `QtQuick.Layouts 2.15`. + +Работает с C++-типом `LauncherBackend` (`launcherbackend.h`, `QML_ELEMENT` в модуле +`Minecraft_launcher`), передаваемым снаружи в свойство `backend`. Читает у него свойства +`versionCatalog` и `catalogLoading`, вызывает `refreshVersionCatalog()`, `versionRemovalInfo()` и +`removeVersion()`. + +Объявлен в `QML_FILES` модуля (`CMakeLists.txt`); использует ресурс `images/Trash.svg` из +`RESOURCES` того же модуля. Инстанцируется в [BuildsDialog](BuildsDialog.md). + +Каталог приходит из C++ уже отсортированным (новые сверху), поэтому окно только отбирает записи и +порядок не трогает. Каждая запись каталога — объект с полями `id` (идентификатор версии), `label` +(подпись строки), `category` (ключ категории), `installed` (скачана ли) и `search` +(предвычисленная строка для поиска в нижнем регистре). + +## Иерархия и роль + +Корневой тип — `Dialog`: модальный, 720×480 px, нулевой внутренний отступ, закрывается по Escape и +щелчку мимо. Оформление задано вручную — тёмный фон со скруглением и акцентной рамкой. + +Содержимое — `RowLayout` из двух частей, разделённых вертикальной линией: колонка категорий +шириной 150 px (`Repeater` по `categories`) и область версий. В области версий сверху поле поиска, +внизу флажок «Только установленные», между ними `ListView` со строками версий. Строка показывает +подпись, галочку для установленной версии и корзину удаления; галочка рядом со строкой — +единственное место, где видно, что именно скачано. + +Отдельно объявлен вложенный `Dialog` подтверждения удаления шириной 420 px с красной рамкой. Он +привязан к тому же родителю, что и само окно, и центрируется в нём вручную, чтобы не оказаться +внутри области выбора. + +Пустое состояние списка объясняется текстом по центру, который различает три случая: каталог ещё +грузится, каталог пуст (нет соединения) и по фильтру ничего не найдено. + +## Свойства + +| Свойство | Тип | По умолчанию | Обязательное | Описание | +|----------|-----|--------------|--------------|----------| +| `backend` | `var` | — | **Да** | Экземпляр `LauncherBackend` — источник каталога версий и исполнитель удаления. | +| `selectedId` | `string` | `""` | Нет | Идентификатор выбранной версии. Окно открывается с текущей версией, и по «Отмене» выбор возвращается к ней, потому что результат уходит наружу только через сигнал. Пустая строка — версия не выбрана, кнопка подтверждения выключена. | +| `category` | `string` | `"release"` | Нет | Ключ активной категории. Допустимые значения: `release` (релизы), `snapshot` (снапшоты), `old_beta` (беты), `old_alpha` (альфы), `other` (прочие — сюда попадают в том числе установленные профили модлоадеров). | +| `filterText` | `string` | `""` | Нет | Текст поиска. Сравнивается в нижнем регистре без учёта регистра с полем `search` записи каталога; поиск идёт внутри активной категории. | +| `installedOnly` | `bool` | `false` | Нет | Показывать только скачанные версии. | +| `categories` | `var` (только чтение) | список из пяти записей | Нет | Описание колонки категорий: массив объектов с полями `key` и переведённым `title`. Задаёт и порядок пунктов, и набор допустимых значений `category`. | +| `visibleEntries` | `var` (только чтение) | вычисляется | Нет | Отобранные строки каталога: записи активной категории, прошедшие флажок «только установленные» и текст поиска. Порядок наследуется от каталога. Служит моделью списка. | + +## Сигналы + +#### versionChosen(string versionId) + +Версия подтверждена — кнопкой «Выбрать», двойным щелчком по строке или клавишей Enter в поле +поиска. В параметре приходит идентификатор версии. + +Имя не `accepted()` сознательно: такой сигнал у `Dialog` уже есть и переопределить его нельзя. + +Обработчик — карточка сборки — записывает выбранную версию в сборку. К моменту вызова обработчика +окно уже закрыто. При отмене сигнал не испускается, поэтому снаружи ничего откатывать не нужно. + +## Методы + +#### openFor(string versionId) : void + +Открывает окно на переданной версии. Запоминает её в `selectedId`, очищает поиск, переключается на +категорию именно этой версии (а не всегда на релизы), просит бэкенд обновить каталог и +прокручивает список к выбранной строке. + +#### categoryOf(string versionId) : string + +Возвращает ключ категории версии по каталогу; для неизвестной версии — `release`. + +#### indexOfSelected() : int + +Позиция выбранной версии в `visibleEntries` или `-1`, если под текущим фильтром её не видно. + +#### revealSelected() : void + +Выставляет текущий индекс списка на выбранную версию и прокручивает список так, чтобы строка +оказалась по центру. Вызывается после каждой смены фильтра, категории или удаления. + +#### acceptSelection() : void + +Подтверждает выбор: испускает `versionChosen()` и закрывает окно. При пустом `selectedId` не +делает ничего. + +#### catalogHas(string versionId) : bool + +Есть ли версия в каталоге. Нужен после удаления: профиль модлоадера присутствовал в каталоге +только потому, что был установлен, и после удаления строка исчезает совсем. + +#### askRemove(string versionId) : void + +Спрашивает подтверждение перед удалением. Запрашивает у бэкенда `versionRemovalInfo()` и, если +версия действительно установлена, наполняет окно подтверждения: занимаемый объём, список +зависящих профилей модлоадеров и список сборок, которые эту версию используют. Идентификатор +запоминается в самом окне подтверждения, потому что к моменту ответа строка под курсором может +быть уже другой. + +Окно подтверждения объясняет три вещи: файлы удалятся из `versions/` и освободится столько-то +мегабайт; профили модлоадеров поверх этой версии без неё не запустятся; библиотеки и ресурсы в +`libraries/` и `assets/` общие для всех версий и остаются на месте. + +#### performRemove(string versionId) : void + +Удаляет версию через бэкенд. Если удалена была именно выбранная версия и её больше нет в каталоге, +снимает выбор. В конце обновляет позицию списка. + +## Взаимодействие с другими компонентами + +**Со стороны родителя.** [BuildsDialog](BuildsDialog.md) задаёт `backend`, открывает окно вызовом +`openFor()` с текущей версией сборки и подписывается на `versionChosen()`, чтобы записать выбор. +Никаких других точек входа у окна нет — прямая запись `selectedId` снаружи не предполагается. + +**Со стороны бэкенда.** `versionCatalog` и `catalogLoading` — привязки, от которых зависят и +модель списка, и текст пустого состояния: пришедшее обновление каталога пересчитывает +`visibleEntries` само. `refreshVersionCatalog()` вызывается при открытии окна, `removeVersion()` — +после подтверждения удаления. + +**Клавиатура.** Фокус при открытии уходит в поле поиска. Стрелки вверх и вниз двигают выбор по +списку функцией `step()`, Enter подтверждает выбор, Escape закрывает окно. + +## Пример использования + +```qml +VersionPickerDialog { + id: versionPicker + parent: Overlay.overlay + backend: launcherBackend + onVersionChosen: (versionId) => buildCard.gameVersion = versionId +} + +Button { + text: buildCard.gameVersion || qsTr("Выбрать версию") + onClicked: versionPicker.openFor(buildCard.gameVersion) +} +``` + +--- + +При создании этого документа использовался ИИ.