# 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) } } ``` --- При создании этого документа использовался ИИ.