#pragma once #include "minecraftversion.h" #include "seasonalbuildservice.h" #include #include #include #include #include #include #include struct AuthResult; class AuthService; class MsaAuthService; class GameLauncher; class VersionManifestService; class VersionInstaller; class ModLoaderVersionService; class ModLoaderInstaller; class BuildSwitcher; class JavaRuntimeService; class JavaInstaller; class SeasonalBuildService; class SeasonalPackDownloader; class LauncherBackend : public QObject { Q_OBJECT QML_ELEMENT Q_PROPERTY(QStringList profileNames READ profileNames NOTIFY profilesChanged) Q_PROPERTY(QStringList customBuildNames READ customBuildNames NOTIFY customBuildsChanged) // Сборка, которую запускает кнопка игры. Хранится по id сборки, а не по // индексу: удаление соседней записи не должно переназначать активную. Q_PROPERTY(int activeBuildIndex READ activeBuildIndex WRITE setActiveBuildIndex NOTIFY activeBuildChanged) Q_PROPERTY(QString activeBuildName READ activeBuildName NOTIFY activeBuildChanged) Q_PROPERTY(QStringList installedVersions READ installedVersions NOTIFY installedVersionsChanged) Q_PROPERTY(bool gameRunning READ gameRunning NOTIFY gameRunningChanged) // Идёт архивация/распаковка .minecraft при смене сборки. Отдельно от busy, // потому что на это время блокируется ещё и список сборок. Q_PROPERTY(bool switching READ switching NOTIFY switchChanged) Q_PROPERTY(double switchProgress READ switchProgress NOTIFY switchChanged) Q_PROPERTY(QString switchStage READ switchStage NOTIFY switchChanged) Q_PROPERTY(QString switchStatus READ switchStatus NOTIFY switchChanged) Q_PROPERTY(bool busy READ busy NOTIFY busyChanged) // Собран ли лаунчер с Qt WebEngine. Без него окно входа Microsoft // показать нечем, и интерфейс не должен предлагать этот путь. Q_PROPERTY(bool microsoftAvailable READ microsoftAvailable CONSTANT) // Объединённый список для выпадающего меню версий: установленные, разделитель, // затем весь каталог Mojang. Q_PROPERTY(QVariantList versionCatalog READ versionCatalog NOTIFY versionCatalogChanged) Q_PROPERTY(bool catalogLoading READ catalogLoading NOTIFY catalogLoadingChanged) // Сборки Java для окна выбора: скачиваемые из сети плюс те, что уже лежат // в папке лаунчера. Q_PROPERTY(QVariantList javaCatalog READ javaCatalog NOTIFY javaCatalogChanged) Q_PROPERTY(bool javaCatalogLoading READ javaCatalogLoading NOTIFY javaCatalogLoadingChanged) // Готовые сборки с сервера. Строки таблицы уже сведены с локальными // записями: окно показывает статус, не считая ничего само. Q_PROPERTY(QVariantList seasonalCatalog READ seasonalCatalog NOTIFY seasonalCatalogChanged) Q_PROPERTY(bool seasonalCatalogLoading READ seasonalCatalogLoading NOTIFY seasonalCatalogLoadingChanged) Q_PROPERTY(QString seasonalCatalogError READ seasonalCatalogError NOTIFY seasonalCatalogChanged) // Идёт установка сезонной сборки: окно не даёт начать вторую. Q_PROPERTY(bool seasonalInstalling READ seasonalInstalling NOTIFY seasonalInstallingChanged) // Одного сигнала на все свойства загрузки достаточно: установщик уже // ограничивает частоту, а QML всё равно перечитывает их разом. Q_PROPERTY(bool downloading READ downloading NOTIFY downloadChanged) Q_PROPERTY(double downloadProgress READ downloadProgress NOTIFY downloadChanged) Q_PROPERTY(QString downloadVersion READ downloadVersion NOTIFY downloadChanged) Q_PROPERTY(QString downloadStatus READ downloadStatus NOTIFY downloadChanged) Q_PROPERTY(qint64 downloadBytesDone READ downloadBytesDone NOTIFY downloadChanged) Q_PROPERTY(qint64 downloadBytesTotal READ downloadBytesTotal NOTIFY downloadChanged) public: explicit LauncherBackend(QObject *parent = nullptr); ~LauncherBackend() override; QStringList profileNames() const; QStringList customBuildNames() const; int activeBuildIndex() const; void setActiveBuildIndex(int index); QString activeBuildName() const; QStringList installedVersions() const; bool gameRunning() const; bool switching() const; double switchProgress() const; QString switchStage() const; QString switchStatus() const; // Пока идёт загрузка версии, лаунчер тоже занят: кнопка запуска гаснет. bool busy() const; bool microsoftAvailable() const; QVariantList versionCatalog() const; bool catalogLoading() const; QVariantList javaCatalog() const; bool javaCatalogLoading() const; QVariantList seasonalCatalog() const; bool seasonalCatalogLoading() const; QString seasonalCatalogError() const; bool seasonalInstalling() const; bool downloading() const; double downloadProgress() const; QString downloadVersion() const; QString downloadStatus() const; qint64 downloadBytesDone() const; qint64 downloadBytesTotal() const; Q_INVOKABLE void addProfile(const QString &name, const QString &login, const QString &password, const QString &authType = QStringLiteral("offline")); Q_INVOKABLE void addCustomBuild(const QString &name, const QString &serverUrl, const QString &minecraftVersion = QString()); Q_INVOKABLE void updateProfile(int index, const QString &name, const QString &login, const QString &password, const QString &authType = QStringLiteral("offline")); // Мержит только присланные ключи: name, serverUrl, minecraftVersion, // loader, loaderVersion, resolvedVersionId. Q_INVOKABLE void updateCustomBuild(int index, const QVariantMap &fields); Q_INVOKABLE QVariantMap profileAt(int index) const; // Вход в аккаунт Microsoft. profileIndex = -1 — вход ещё не привязан к // профилю: он создастся по нику, который вернут Minecraft Services. Q_INVOKABLE void startMicrosoftLogin(int profileIndex); Q_INVOKABLE void finishMicrosoftLogin(const QString &code); Q_INVOKABLE void cancelMicrosoftLogin(); // Разбор адреса, на который встроенное окно возвращается после входа. // Живёт в C++, чтобы правила совпадения не разъезжались с сервисом. Q_INVOKABLE QVariantMap inspectMicrosoftRedirect(const QString &url) const; Q_INVOKABLE QVariantMap customBuildAt(int index) const; Q_INVOKABLE void removeProfile(int index); // Что именно потеряется при удалении — для текста предупреждения. Q_INVOKABLE QVariantMap customBuildRemovalInfo(int index) const; Q_INVOKABLE void removeCustomBuild(int index); // Докачать то, чего не хватает выбранной сборке. Отдельной кнопкой, потому // что карточка сборки сохраняет правки по ходу редактирования. Q_INVOKABLE void installCustomBuild(int index); // Главная кнопка. Проверяет выбор профиля и версии, комплектность .minecraft, // при необходимости авторизуется и стартует игру. Q_INVOKABLE void launchGame(int profileIndex, int buildIndex); Q_INVOKABLE void submitTwoFactorCode(const QString &code); Q_INVOKABLE void cancelPendingLaunch(); Q_INVOKABLE void stopGame(); // Проверка без запуска — для подсказки в интерфейсе. Q_INVOKABLE QStringList checkInstallation(int buildIndex) const; // Вызывается при открытии выпадающего списка версий: свежий кэш — без сети. Q_INVOKABLE void refreshVersionCatalog(bool force = false); Q_INVOKABLE bool isVersionInstalled(const QString &versionId) const; Q_INVOKABLE void installVersion(const QString &versionId); // Что потеряется при удалении версии — для текста предупреждения. Q_INVOKABLE QVariantMap versionRemovalInfo(const QString &versionId) const; Q_INVOKABLE void removeVersion(const QString &versionId); Q_INVOKABLE void cancelDownload(); // Версии модлоадеров для выбранной версии игры. Возвращает список карт // {version, label, recommended, stable}; несовместимых строк в нём нет — // отбор заложен в сам источник данных. Q_INVOKABLE QVariantList loaderVersions(const QString &loaderKey, const QString &gameVersion) const; Q_INVOKABLE void refreshLoaderVersions(const QString &loaderKey, const QString &gameVersion, bool force = false); Q_INVOKABLE bool loaderVersionsLoading(const QString &loaderKey, const QString &gameVersion) const; // Ставит модлоадер, выбранный в сборке, и записывает получившийся профиль // в resolvedVersionId. Q_INVOKABLE void installLoaderForBuild(int index); Q_INVOKABLE QVariantMap settings() const; Q_INVOKABLE void updateSettings(const QVariantMap &values); Q_INVOKABLE QStringList detectedJava() const; // ── Сборки Java ───────────────────────────────────────────────────── // Вызывается при открытии окна выбора: свежий кэш — без сети. Q_INVOKABLE void refreshJavaCatalog(bool force = false); Q_INVOKABLE void installJavaRuntime(const QString &runtimeId); Q_INVOKABLE void removeJavaRuntime(const QString &runtimeId); // Пусто — сборки с таким id в папке лаунчера нет. Q_INVOKABLE QVariantMap javaRuntimeInfo(const QString &runtimeId) const; // Минимальная Java для версии игры выбранной сборки; 0 — версия не выбрана. Q_INVOKABLE int requiredJavaMajor(int buildIndex) const; // ── Сезонные сборки ───────────────────────────────────────────────── Q_INVOKABLE void refreshSeasonalCatalog(bool force = false); // Ставит или обновляет сборку целиком: запись, версия, модлоадер, Java и // файлы — одной цепочкой. Q_INVOKABLE void installSeasonalBuild(const QString &seasonalId); Q_INVOKABLE void cancelSeasonalInstall(); Q_INVOKABLE void openMinecraftFolder(); Q_INVOKABLE void openGameFolder(); signals: void profilesChanged(); void customBuildsChanged(); void activeBuildChanged(); void switchChanged(); void loaderVersionsChanged(const QString &loaderKey, const QString &gameVersion); void installedVersionsChanged(); void settingsChanged(); void gameRunningChanged(); void busyChanged(); void versionCatalogChanged(); void catalogLoadingChanged(); void javaCatalogChanged(); void javaCatalogLoadingChanged(); void seasonalCatalogChanged(); void seasonalCatalogLoadingChanged(); void seasonalInstallingChanged(); void downloadChanged(); // Сезонная сборка установлена и активна — можно запускать игру. void seasonalInstallFinished(const QString &seasonalId, const QString &buildName); // Сборка Java установлена: окно настроек обновляет подпись, не переоткрываясь. void javaRuntimeInstalled(const QString &runtimeId); void launchProgress(const QString &message); void launched(const QString &profileName, const QString &buildName, const QString &serverUrl); void launchError(const QString &message); void twoFactorRequired(const QString &profileName); // Окну входа Microsoft: открыться на этом адресе. void microsoftLoginUrlReady(const QString &url); void microsoftLoginSucceeded(const QString &playerName); void microsoftLoginFailed(const QString &message); // Сессия профиля протухла настолько, что нужен повторный вход руками. void microsoftReloginRequired(int profileIndex); void gameOutput(const QString &line); void gameFinished(int exitCode, bool crashed); private: struct Profile { int id = 0; QString name, login, password; QString authType = QStringLiteral("offline"); // offline | elyby | microsoft QString clientToken, accessToken, uuid, resolvedName, userType; // Только для microsoft: продление сессии, ${auth_xuid} и срок жизни // accessToken в ISO-8601 (чтобы класть в JSON без конвертеров). QString refreshToken, xuid, tokenExpiry; }; // Пользовательская сборка: имя, сервер, версия Minecraft и, начиная со // второго этапа, выбранный модлоадер. struct CustomBuild { int id = 0; QString name, serverUrl; QString minecraftVersion; // 1.20.1 QString loader; // "" | forge | fabric | neoforge | quilt QString loaderVersion; // 47.4.0 | 0.19.3 QString resolvedVersionId; // versions/, который реально запускается // Сборка приехала с сервера сезонных сборок: id её строки в каталоге и // установленная ревизия. Пусто — обычная пользовательская сборка. QString seasonalId; int seasonalRevision = 0; }; // Шаг установки сезонной сборки. Порядок жёсткий: Java нужна установщику // Forge, а раскатывать файлы поверх имеет смысл только когда всё остальное // на месте. enum class SeasonStage { Idle, Switching, Java, Loader, Downloading, Applying }; struct SeasonalInstall { SeasonStage stage = SeasonStage::Idle; int buildId = -1; QString packPath; // Копия строки каталога на момент старта: обновление списка посреди // установки не должно менять то, что ставится. SeasonalBuildEntry entry; }; void loadData(); void loadSettings(); void ensureDataFiles(); void saveProfiles(); void saveCustomBuilds(); void saveSettings(); // Чтение файлов лаунчера. Отсутствие файла — норма (первый запуск), а вот // повреждённое содержимое отводится в .bak, чтобы файл создался заново. QJsonArray readArrayFile(const QString &path); QJsonObject readObjectFile(const QString &path); bool moveAsideBroken(const QString &path, const QString &reason); // Проверяет папку лаунчера и пишет документ. Об ошибке сообщает через launchError. bool writeJsonFile(const QString &path, const QJsonDocument &document); QString gameDir() const; // id профиля в /versions, который запускает эта сборка. QString launchVersionIdOf(const CustomBuild &build) const; int indexOfBuildId(int buildId) const; // Перекладывает /versions.json в customBuilds.json при первом запуске // после переименования. true — миграция прошла, читать нужно новый файл. bool migrateLegacyBuildsFile(); void invalidateCatalog(); void rebuildCatalog() const; void invalidateJavaCatalog(); void rebuildJavaCatalog() const; void invalidateSeasonalCatalog(); void rebuildSeasonalCatalog() const; // Панель загрузки занята скачиванием пака сезонной сборки. bool packDownloadShown() const; // ── Установка сезонной сборки ─────────────────────────────────────── // Каждый шаг либо запускает асинхронную операцию и возвращает true, либо // решает, что делать нечего, и уступает следующему. void seasonAdvance(); bool seasonBeginSwitch(); bool seasonBeginJava(); bool seasonBeginLoader(); bool seasonBeginDownload(); bool seasonBeginApply(); // message пусто — о причине уже сообщил тот, кто её обнаружил. void seasonFail(const QString &message); void seasonFinish(); int indexOfSeasonalBuild(const QString &seasonalId) const; // Что принесла установленная ревизия сборки; пусто — сезонных файлов нет. QStringList seasonalPackEntries(int buildId) const; // Запоминает раскатанную ревизию: и в builds//season.json, и в записи // сборки. Вызывается и после доигрывания прерванной установки. void recordSeasonalPack(int buildId, const QJsonObject ¬e, const QStringList &entries); QUrl seasonalBaseUrl() const; // Панель загрузки занята установкой Java, а не версии или модлоадера. bool javaDownloadShown() const; // Путь к java выбранной сборки из папки лаунчера. Пусто — сборка не выбрана // или ещё не скачана; тогда в дело идёт javaPath и автопоиск. QString managedJavaPath() const; // Версия выбрана, но её нет в .minecraft — ставим в фоне. void maybeInstall(const QString &versionId); void setBusy(bool busy); void failLaunch(const QString &message); void beginAuthentication(); void beginMicrosoftAuthentication(); void applyMicrosoftResult(Profile &profile, const AuthResult &auth); void continueLaunch(const AuthResult &auth); QList m_profiles; QList m_builds; QVariantMap m_settings; // Проблемы хранилища, замеченные на старте: показываются одним сообщением, // когда интерфейс уже подключился к сигналам. QStringList m_storageIssues; int m_nextProfileId = 1; int m_nextBuildId = 1; AuthService *m_auth; MsaAuthService *m_msa; GameLauncher *m_launcher; VersionManifestService *m_manifest; VersionInstaller *m_installer; ModLoaderVersionService *m_loaderMeta; ModLoaderInstaller *m_loaderInstaller; BuildSwitcher *m_switcher; JavaRuntimeService *m_javaMeta; JavaInstaller *m_javaInstaller; SeasonalBuildService *m_seasonalMeta; SeasonalPackDownloader *m_packDownloader; // Сборка, для которой сейчас ставится модлоадер: по её id находим запись, // когда установщик отчитается. int m_loaderInstallBuildId = -1; // Профиль, для которого открыто окно входа Microsoft. -1 — окно закрыто, // -2 — вход для профиля, которого ещё нет. int m_msaLoginProfile = -1; // Каталог версий пересобирается только по изменениям: QML читает свойство // помногу раз за кадр, пока открыт список. mutable QVariantList m_catalog; mutable bool m_catalogDirty = true; mutable QVariantList m_javaCatalog; mutable bool m_javaCatalogDirty = true; mutable QVariantList m_seasonalCatalog; mutable bool m_seasonalCatalogDirty = true; SeasonalInstall m_season; bool m_busy = false; // Состояние запуска, начатого до асинхронной авторизации. int m_pendingProfile = -1; int m_pendingBuild = -1; MinecraftVersion m_pendingVersionData; QString m_pendingInjectorPath; };