#include "launcherbackend.h" #include "authservice.h" #include "gamelauncher.h" #include "javalocator.h" #include "launcherpaths.h" #include "versioninstaller.h" #include "versionmanifestservice.h" #include #include #include #include #include #include #include #include #include #include #include #include namespace { const QString kOffline = QStringLiteral("offline"); const QString kElyBy = QStringLiteral("elyby"); int settingInt(const QVariantMap &settings, const QString &key, int fallback) { const QVariant value = settings.value(key); bool ok = false; const int parsed = value.toInt(&ok); return ok ? parsed : fallback; } } // namespace LauncherBackend::LauncherBackend(QObject *parent) : QObject(parent) , m_auth(new AuthService(this)) , m_launcher(new GameLauncher(this)) , m_manifest(new VersionManifestService(this)) , m_installer(new VersionInstaller(m_manifest, this)) { // Папка лаунчера создаётся при первом запуске и проверяется при каждом // следующем — если её удалили, она появится снова. QString storageError; if (!LauncherPaths::ensureRootExists(&storageError)) m_storageIssues << tr("Не удалось создать папку лаунчера: %1").arg(storageError); loadSettings(); loadData(); ensureDataFiles(); connect(m_auth, &AuthService::progress, this, &LauncherBackend::launchProgress); connect(m_launcher, &GameLauncher::progress, this, &LauncherBackend::launchProgress); connect(m_launcher, &GameLauncher::output, this, &LauncherBackend::gameOutput); connect(m_launcher, &GameLauncher::gameStarted, this, [this](const QString &commandLine) { emit gameOutput(commandLine); emit gameRunningChanged(); }); connect(m_launcher, &GameLauncher::gameFinished, this, [this](int exitCode, bool crashed) { emit gameRunningChanged(); emit gameFinished(exitCode, crashed); }); connect(m_manifest, &VersionManifestService::versionsChanged, this, &LauncherBackend::invalidateCatalog); connect(m_manifest, &VersionManifestService::refreshingChanged, this, &LauncherBackend::catalogLoadingChanged); connect(m_installer, &VersionInstaller::started, this, [this](const QString &id) { emit downloadChanged(); emit busyChanged(); emit launchProgress(tr("Загрузка версии «%1»…").arg(id)); }); connect(m_installer, &VersionInstaller::progressChanged, this, &LauncherBackend::downloadChanged); // Порядок важен: на installedVersionsChanged интерфейс перечитывает каталог, // и устаревший кэш вернулся бы наружу. connect(m_installer, &VersionInstaller::finished, this, [this](const QString &id) { invalidateCatalog(); emit installedVersionsChanged(); emit downloadChanged(); emit busyChanged(); emit launchProgress(tr("Версия «%1» загружена").arg(id)); }); connect(m_installer, &VersionInstaller::failed, this, [this](const QString &id, const QString &message) { // Часть файлов всё же появилась — список установленного мог измениться. invalidateCatalog(); emit installedVersionsChanged(); emit downloadChanged(); emit busyChanged(); emit launchError(tr("Не удалось загрузить версию «%1»: %2").arg(id, message)); }); connect(m_installer, &VersionInstaller::canceled, this, [this](const QString &id) { invalidateCatalog(); emit installedVersionsChanged(); emit downloadChanged(); emit busyChanged(); emit launchProgress(tr("Загрузка версии «%1» отменена").arg(id)); }); // Интерфейс подключается к сигналам уже после конструктора — сообщаем отложенно. if (!m_storageIssues.isEmpty()) { QTimer::singleShot(0, this, [this] { emit launchError(m_storageIssues.join(u'\n')); }); } } LauncherBackend::~LauncherBackend() = default; QStringList LauncherBackend::profileNames() const { QStringList names; for (const auto &p : m_profiles) names << p.name; return names; } QStringList LauncherBackend::versionNames() const { QStringList names; for (const auto &v : m_versions) names << v.name; return names; } QStringList LauncherBackend::installedVersions() const { return VersionLoader::installedVersions(gameDir()); } bool LauncherBackend::gameRunning() const { return m_launcher->isRunning(); } void LauncherBackend::addProfile(const QString &name, const QString &login, const QString &password, const QString &authType) { Profile profile; profile.id = m_nextProfileId++; profile.name = name; profile.login = login; profile.password = password; profile.authType = (authType == kElyBy) ? kElyBy : kOffline; profile.clientToken = AuthService::generateClientToken(); m_profiles.append(profile); saveProfiles(); emit profilesChanged(); } void LauncherBackend::addVersion(const QString &name, const QString &serverUrl, const QString &versionId) { m_versions.append({m_nextVersionId++, name, serverUrl, versionId}); saveVersions(); emit versionsChanged(); maybeInstall(versionIdOf(m_versions.last())); } void LauncherBackend::updateProfile(int index, const QString &name, const QString &login, const QString &password, const QString &authType) { if (index < 0 || index >= m_profiles.size()) return; Profile &profile = m_profiles[index]; // Смена логина или пароля обесценивает сохранённый токен Ely.by. if (profile.login != login || profile.password != password) { profile.accessToken.clear(); profile.uuid.clear(); profile.resolvedName.clear(); } profile.name = name; profile.login = login; profile.password = password; profile.authType = (authType == kElyBy) ? kElyBy : kOffline; if (profile.clientToken.isEmpty()) profile.clientToken = AuthService::generateClientToken(); saveProfiles(); emit profilesChanged(); } void LauncherBackend::updateVersion(int index, const QString &name, const QString &serverUrl, const QString &versionId) { if (index < 0 || index >= m_versions.size()) return; m_versions[index].name = name; m_versions[index].serverUrl = serverUrl; m_versions[index].versionId = versionId; saveVersions(); emit versionsChanged(); maybeInstall(versionIdOf(m_versions[index])); } QVariantMap LauncherBackend::profileAt(int index) const { if (index < 0 || index >= m_profiles.size()) return {}; const auto &p = m_profiles[index]; return {{"name", p.name}, {"login", p.login}, {"password", p.password}, {"authType", p.authType}}; } QVariantMap LauncherBackend::versionAt(int index) const { if (index < 0 || index >= m_versions.size()) return {}; const auto &v = m_versions[index]; return {{"name", v.name}, {"serverUrl", v.serverUrl}, {"versionId", versionIdOf(v)}}; } void LauncherBackend::removeProfile(int index) { if (index < 0 || index >= m_profiles.size()) return; m_profiles.removeAt(index); saveProfiles(); emit profilesChanged(); } void LauncherBackend::removeVersion(int index) { if (index < 0 || index >= m_versions.size()) return; m_versions.removeAt(index); saveVersions(); emit versionsChanged(); } QString LauncherBackend::versionIdOf(const Version &version) const { // Пользователь мог назвать запись как угодно — тогда версией считаем само имя. return version.versionId.isEmpty() ? version.name : version.versionId; } QStringList LauncherBackend::checkInstallation(int versionIndex) const { if (versionIndex < 0 || versionIndex >= m_versions.size()) return {tr("Версия не выбрана")}; const Version &entry = m_versions[versionIndex]; const QString id = versionIdOf(entry); if (id.trimmed().isEmpty()) return {tr("У записи лаунчера не указан ID версии. Проверьте %1") .arg(QDir::toNativeSeparators(LauncherPaths::versionsFile()))}; QString error; const MinecraftVersion version = VersionLoader::load(gameDir(), id, {}, &error); if (!version.isValid()) { return {error.isEmpty() ? tr("Версия «%1» из записи «%2» не установлена в %3") .arg(id, entry.name, QDir::toNativeSeparators(gameDir() + QStringLiteral("/versions"))) : error}; } LaunchOptions options; options.gameDir = gameDir(); options.versionId = id; return GameLauncher::missingFiles(options, version); } // ── Каталог версий ───────────────────────────────────────────────────────── void LauncherBackend::invalidateCatalog() { m_catalogDirty = true; emit versionCatalogChanged(); } bool LauncherBackend::catalogLoading() const { return m_manifest->isRefreshing(); } void LauncherBackend::refreshVersionCatalog(bool force) { // Список установленного мог поменяться и без нас (папку добавили руками). invalidateCatalog(); m_manifest->ensureLoaded([this](bool ok, const QString &warning) { if (!ok && !warning.isEmpty()) emit launchError(warning); }, force); } bool LauncherBackend::isVersionInstalled(const QString &versionId) const { if (versionId.trimmed().isEmpty()) return false; return VersionLoader::installedVersions(gameDir()).contains(versionId); } QVariantList LauncherBackend::versionCatalog() const { if (m_catalogDirty) { rebuildCatalog(); m_catalogDirty = false; } return m_catalog; } void LauncherBackend::rebuildCatalog() const { m_catalog.clear(); const QList remote = m_manifest->versions(); QHash remoteIndex; remoteIndex.reserve(remote.size()); for (int i = 0; i < remote.size(); ++i) remoteIndex.insert(remote.at(i).id, i); const auto makeVersionRow = [](const QString &id, const QString &type, bool installed) { return QVariantMap{{QStringLiteral("kind"), installed ? QStringLiteral("installed") : QStringLiteral("remote")}, {QStringLiteral("id"), id}, {QStringLiteral("type"), type}, {QStringLiteral("label"), id}, {QStringLiteral("installed"), installed}, {QStringLiteral("selectable"), true}, {QStringLiteral("search"), (id + u' ' + type).toLower()}}; }; // Установленные: сначала знакомые манифесту, свежие сверху, затем сборки // вроде fabric-loader-…, о которых манифест ничего не знает. QStringList installed = VersionLoader::installedVersions(gameDir()); std::stable_sort(installed.begin(), installed.end(), [&remoteIndex, &remote](const QString &a, const QString &b) { const auto ia = remoteIndex.constFind(a); const auto ib = remoteIndex.constFind(b); const bool knownA = ia != remoteIndex.constEnd(); const bool knownB = ib != remoteIndex.constEnd(); if (knownA != knownB) return knownA; if (knownA) return ia.value() < ib.value(); // манифест уже отсортирован return a < b; }); QSet installedSet; for (const QString &id : std::as_const(installed)) { const auto it = remoteIndex.constFind(id); const QString type = it != remoteIndex.constEnd() ? remote.at(it.value()).type : QString(); installedSet.insert(id); m_catalog.append(makeVersionRow(id, type, true)); } m_catalog.append(QVariantMap{{QStringLiteral("kind"), QStringLiteral("separator")}, {QStringLiteral("id"), QString()}, {QStringLiteral("type"), QString()}, {QStringLiteral("label"), tr("Все версии Minecraft")}, {QStringLiteral("installed"), false}, {QStringLiteral("selectable"), false}, {QStringLiteral("search"), QString()}}); if (remote.isEmpty()) { m_catalog.append( QVariantMap{{QStringLiteral("kind"), QStringLiteral("status")}, {QStringLiteral("id"), QString()}, {QStringLiteral("type"), QString()}, {QStringLiteral("label"), m_manifest->isRefreshing() ? tr("Загрузка списка версий…") : tr("Список версий недоступен — проверьте соединение")}, {QStringLiteral("installed"), false}, {QStringLiteral("selectable"), false}, {QStringLiteral("search"), QString()}}); return; } for (const RemoteVersionEntry &entry : remote) { if (installedSet.contains(entry.id)) continue; m_catalog.append(makeVersionRow(entry.id, entry.type, false)); } } // ── Загрузка версий ──────────────────────────────────────────────────────── void LauncherBackend::maybeInstall(const QString &versionId) { const QString id = versionId.trimmed(); if (id.isEmpty() || isVersionInstalled(id)) return; if (m_installer->versionId() == id || m_installer->isQueued(id)) return; // Манифест здесь не спрашиваем: установщик сам скажет, что версии в нём нет, // и это правильный ответ на вручную вписанный id сборки. m_installer->install(gameDir(), id); } void LauncherBackend::installVersion(const QString &versionId) { const QString id = versionId.trimmed(); if (id.isEmpty()) return; if (m_installer->versionId() == id || m_installer->isQueued(id)) return; m_installer->install(gameDir(), id); } void LauncherBackend::cancelDownload() { m_installer->cancel(); } bool LauncherBackend::downloading() const { return m_installer->isRunning(); } double LauncherBackend::downloadProgress() const { return m_installer->fraction(); } QString LauncherBackend::downloadVersion() const { return m_installer->versionId(); } QString LauncherBackend::downloadStatus() const { const QString stage = m_installer->stage(); const QString file = m_installer->currentFile(); if (stage.isEmpty()) return file; if (file.isEmpty()) return stage; return QStringLiteral("%1 — %2").arg(stage, file); } qint64 LauncherBackend::downloadBytesDone() const { return m_installer->bytesDone(); } qint64 LauncherBackend::downloadBytesTotal() const { return m_installer->bytesTotal(); } void LauncherBackend::launchGame(int profileIndex, int versionIndex) { if (m_installer->isRunning()) { emit launchError(tr("Идёт загрузка версии «%1» — дождитесь окончания") .arg(m_installer->versionId())); return; } if (m_busy) { emit launchError(tr("Запуск уже выполняется")); return; } if (m_launcher->isRunning()) { emit launchError(tr("Игра уже запущена")); return; } if (profileIndex < 0 || profileIndex >= m_profiles.size()) { emit launchError(tr("Выберите профиль перед запуском")); return; } if (versionIndex < 0 || versionIndex >= m_versions.size()) { emit launchError(tr("Выберите версию перед запуском")); return; } // Дальше проверяется только папка игры: свои файлы лаунчер уже разложил // в galeonLauncher при запуске. const QString directory = gameDir(); if (!QDir(directory).exists()) { emit launchError(tr("Папка .minecraft не найдена: %1. Укажите путь к ней в настройках.") .arg(QDir::toNativeSeparators(directory))); return; } const Version &entry = m_versions[versionIndex]; const QString id = versionIdOf(entry); if (id.trimmed().isEmpty()) { emit launchError(tr("У записи лаунчера не указан ID версии. Проверьте %1") .arg(QDir::toNativeSeparators(LauncherPaths::versionsFile()))); return; } const QString versionsDir = QDir::toNativeSeparators(directory + QStringLiteral("/versions")); const QStringList installed = VersionLoader::installedVersions(directory); if (!installed.contains(id)) { emit launchError(installed.isEmpty() ? tr("В %1 нет ни одной установленной версии").arg(versionsDir) : tr("Версия «%1» из записи «%2» не установлена в %3. Доступны: %4") .arg(id, entry.name, versionsDir, installed.join(QStringLiteral(", ")))); return; } setBusy(true); emit launchProgress(tr("Чтение описания версии «%1»…").arg(id)); QSet features; if (settingInt(m_settings, QStringLiteral("windowWidth"), 0) > 0 && settingInt(m_settings, QStringLiteral("windowHeight"), 0) > 0) features << QStringLiteral("has_custom_resolution"); QString error; m_pendingVersionData = VersionLoader::load(directory, id, features, &error); if (!m_pendingVersionData.isValid()) { failLaunch(error.isEmpty() ? tr("Не удалось прочитать версию «%1»").arg(id) : error); return; } LaunchOptions probe; probe.gameDir = directory; probe.versionId = id; const QStringList missing = GameLauncher::missingFiles(probe, m_pendingVersionData); if (!missing.isEmpty()) { failLaunch(tr("В папке игры не хватает файлов (%1):\n%2") .arg(missing.size()) .arg(missing.join(u'\n'))); return; } m_pendingProfile = profileIndex; m_pendingVersion = versionIndex; m_pendingInjectorPath.clear(); beginAuthentication(); } void LauncherBackend::beginAuthentication() { const Profile &profile = m_profiles[m_pendingProfile]; if (profile.authType != kElyBy) { const QString nickname = profile.login.isEmpty() ? profile.name : profile.login; const AuthResult result = AuthService::offline(nickname); if (!result.ok) { failLaunch(result.error); return; } continueLaunch(result); return; } // Ely.by работает только вместе с authlib-injector — без него игра пойдёт // в сессионный сервер Mojang и токен окажется недействительным. m_auth->ensureAuthlibInjector(LauncherPaths::runtimeDir(), [this](const QString &path, const QString &error) { if (m_pendingProfile < 0) return; // запуск отменили, пока качался инжектор if (!error.isEmpty()) { failLaunch(error); return; } m_pendingInjectorPath = path; const Profile &pending = m_profiles[m_pendingProfile]; m_auth->loginElyBy(pending.login, pending.password, pending.clientToken, pending.accessToken, [this](const AuthResult &result) { if (m_pendingProfile < 0) return; if (result.twoFactorRequired) { setBusy(false); emit twoFactorRequired(m_profiles[m_pendingProfile].name); return; } if (!result.ok) { failLaunch(result.error); return; } continueLaunch(result); }); }); } void LauncherBackend::submitTwoFactorCode(const QString &code) { if (m_pendingProfile < 0 || m_pendingProfile >= m_profiles.size()) { emit launchError(tr("Запуск уже отменён")); return; } if (code.trimmed().isEmpty()) { emit launchError(tr("Введите код двухфакторной аутентификации")); return; } setBusy(true); const Profile &profile = m_profiles[m_pendingProfile]; m_auth->loginElyByWithTotp(profile.login, profile.password, code.trimmed(), profile.clientToken, [this](const AuthResult &result) { if (m_pendingProfile < 0) return; if (!result.ok) { failLaunch(result.error); return; } continueLaunch(result); }); } void LauncherBackend::cancelPendingLaunch() { m_pendingProfile = -1; m_pendingVersion = -1; m_pendingInjectorPath.clear(); setBusy(false); } void LauncherBackend::continueLaunch(const AuthResult &auth) { if (m_pendingProfile < 0 || m_pendingVersion < 0) { failLaunch(tr("Запуск был отменён")); return; } Profile &profile = m_profiles[m_pendingProfile]; const Version &version = m_versions[m_pendingVersion]; // Ely.by возвращает игровой ник и UUID аккаунта — их и запоминаем, // чтобы в следующий раз обойтись без пароля. if (!auth.accessToken.isEmpty() && profile.authType == kElyBy) { profile.accessToken = auth.accessToken; profile.clientToken = auth.clientToken; if (!auth.uuid.isEmpty()) profile.uuid = auth.uuid; if (!auth.playerName.isEmpty()) profile.resolvedName = auth.playerName; profile.userType = auth.userType; saveProfiles(); } LaunchOptions options; options.gameDir = gameDir(); options.versionId = m_pendingVersionData.id; options.playerName = auth.playerName.isEmpty() ? profile.resolvedName : auth.playerName; if (options.playerName.isEmpty()) options.playerName = profile.login.isEmpty() ? profile.name : profile.login; options.uuid = auth.uuid.isEmpty() ? profile.uuid : auth.uuid; options.accessToken = auth.accessToken; options.clientToken = auth.clientToken; options.userType = auth.userType; options.javaPath = m_settings.value(QStringLiteral("javaPath")).toString(); options.minMemoryMb = settingInt(m_settings, QStringLiteral("minMemoryMb"), 512); options.maxMemoryMb = settingInt(m_settings, QStringLiteral("maxMemoryMb"), 4096); options.windowWidth = settingInt(m_settings, QStringLiteral("windowWidth"), 0); options.windowHeight = settingInt(m_settings, QStringLiteral("windowHeight"), 0); options.fullscreen = m_settings.value(QStringLiteral("fullscreen")).toBool(); options.serverAddress = version.serverUrl.trimmed(); options.launcherName = m_settings.value(QStringLiteral("launcherName")).toString(); options.launcherVersion = m_settings.value(QStringLiteral("launcherVersion")).toString(); const QString extra = m_settings.value(QStringLiteral("jvmArgs")).toString().trimmed(); if (!extra.isEmpty()) options.extraJvmArgs = extra.split(QRegularExpression(QStringLiteral("\\s+")), Qt::SkipEmptyParts); if (profile.authType == kElyBy) options.authlibInjectorPath = m_pendingInjectorPath; QString error; const bool started = m_launcher->launch(options, m_pendingVersionData, &error); const QString profileName = profile.name; const QString versionName = version.name; const QString serverUrl = version.serverUrl; cancelPendingLaunch(); if (!started) { emit launchError(error); return; } emit gameRunningChanged(); emit launched(profileName, versionName, serverUrl); } void LauncherBackend::stopGame() { m_launcher->terminate(); } void LauncherBackend::failLaunch(const QString &message) { cancelPendingLaunch(); emit launchError(message); } bool LauncherBackend::busy() const { return m_busy || m_installer->isRunning(); } void LauncherBackend::setBusy(bool busy) { if (m_busy == busy) return; m_busy = busy; emit busyChanged(); } QString LauncherBackend::gameDir() const { const QString configured = m_settings.value(QStringLiteral("gameDir")).toString().trimmed(); return configured.isEmpty() ? LauncherPaths::defaultMinecraftDir() : QDir::fromNativeSeparators(configured); } QVariantMap LauncherBackend::settings() const { QVariantMap result = m_settings; result[QStringLiteral("resolvedGameDir")] = QDir::toNativeSeparators(gameDir()); return result; } void LauncherBackend::updateSettings(const QVariantMap &values) { const QString previousGameDir = gameDir(); for (auto it = values.cbegin(); it != values.cend(); ++it) m_settings.insert(it.key(), it.value()); saveSettings(); emit settingsChanged(); if (gameDir() != previousGameDir) { invalidateCatalog(); emit installedVersionsChanged(); } } QStringList LauncherBackend::detectedJava() const { QStringList result; const QStringList candidates = JavaLocator::findAll(gameDir()); for (const QString &candidate : candidates) { const int major = JavaLocator::majorVersion(candidate); result << (major > 0 ? QStringLiteral("Java %1 — %2") .arg(major) .arg(QDir::toNativeSeparators(candidate)) : QDir::toNativeSeparators(candidate)); } return result; } void LauncherBackend::openMinecraftFolder() { const QString modsDir = gameDir() + QStringLiteral("/mods"); QDir().mkpath(modsDir); if (!QDesktopServices::openUrl(QUrl::fromLocalFile(modsDir))) emit launchError(tr("Не удалось открыть папку с модами")); } void LauncherBackend::openGameFolder() { const QString directory = gameDir(); QDir().mkpath(directory); if (!QDesktopServices::openUrl(QUrl::fromLocalFile(directory))) emit launchError(tr("Не удалось открыть папку игры")); } // Повреждённый файл не удаляем: отводим в .bak, чтобы данные можно было спасти // руками, а ensureDataFiles() создал на его месте пустой. bool LauncherBackend::moveAsideBroken(const QString &path, const QString &reason) { const QString backup = path + QStringLiteral(".bak"); QFile::remove(backup); if (!QFile::rename(path, backup)) { m_storageIssues << tr("Файл %1 повреждён (%2), и его не удалось отложить в сторону") .arg(QDir::toNativeSeparators(path), reason); return false; } m_storageIssues << tr("Файл %1 повреждён (%2). Копия сохранена как %3, файл создан заново.") .arg(QDir::toNativeSeparators(path), reason, QDir::toNativeSeparators(backup)); return true; } QJsonArray LauncherBackend::readArrayFile(const QString &path) { QFile file(path); if (!file.exists()) return {}; // первый запуск — это не ошибка if (!file.open(QIODevice::ReadOnly)) { m_storageIssues << tr("Не удалось прочитать %1: %2") .arg(QDir::toNativeSeparators(path), file.errorString()); return {}; } QJsonParseError parseError; const QJsonDocument document = QJsonDocument::fromJson(file.readAll(), &parseError); if (parseError.error != QJsonParseError::NoError) { file.close(); moveAsideBroken(path, parseError.errorString()); return {}; } if (!document.isArray()) { file.close(); moveAsideBroken(path, tr("ожидался список записей")); return {}; } return document.array(); } QJsonObject LauncherBackend::readObjectFile(const QString &path) { QFile file(path); if (!file.exists()) return {}; if (!file.open(QIODevice::ReadOnly)) { m_storageIssues << tr("Не удалось прочитать %1: %2") .arg(QDir::toNativeSeparators(path), file.errorString()); return {}; } QJsonParseError parseError; const QJsonDocument document = QJsonDocument::fromJson(file.readAll(), &parseError); if (parseError.error != QJsonParseError::NoError) { file.close(); moveAsideBroken(path, parseError.errorString()); return {}; } if (!document.isObject()) { file.close(); moveAsideBroken(path, tr("ожидался набор настроек")); return {}; } return document.object(); } // Перед каждой записью убеждаемся, что папка лаунчера на месте: её могли удалить // уже после запуска, и тогда данные молча пропадали бы. bool LauncherBackend::writeJsonFile(const QString &path, const QJsonDocument &document) { QString storageError; if (!LauncherPaths::ensureRootExists(&storageError)) { emit launchError(tr("Не удалось создать папку лаунчера: %1").arg(storageError)); return false; } const QByteArray payload = document.toJson(); QFile file(path); if (!file.open(QIODevice::WriteOnly) || file.write(payload) != payload.size()) { emit launchError(tr("Не удалось сохранить %1: %2") .arg(QDir::toNativeSeparators(path), file.errorString())); return false; } return true; } void LauncherBackend::loadData() { auto loadFile = [this](const QString &path, auto handler) { for (const auto &val : readArrayFile(path)) handler(val.toObject()); }; loadFile(LauncherPaths::profilesFile(), [this](const QJsonObject &o) { Profile profile; profile.id = o["id"].toInt(); profile.name = o["name"].toString(); profile.login = o["login"].toString(); profile.password = o["password"].toString(); profile.authType = o.value("authType").toString(kOffline); profile.clientToken = o.value("clientToken").toString(); profile.accessToken = o.value("accessToken").toString(); profile.uuid = o.value("uuid").toString(); profile.resolvedName = o.value("resolvedName").toString(); profile.userType = o.value("userType").toString(); if (profile.clientToken.isEmpty()) profile.clientToken = AuthService::generateClientToken(); m_profiles.append(profile); if (profile.id >= m_nextProfileId) m_nextProfileId = profile.id + 1; }); std::sort(m_profiles.begin(), m_profiles.end(), [](const Profile &a, const Profile &b) { return a.id < b.id; }); int skippedVersions = 0; loadFile(LauncherPaths::versionsFile(), [this, &skippedVersions](const QJsonObject &o) { const int id = o["id"].toInt(); const QString name = o["name"].toString(); const QString versionId = o.value("versionId").toString(); // Без имени и без ID запись нерабочая: versionIdOf() вернёт пустую строку. if (name.trimmed().isEmpty() && versionId.trimmed().isEmpty()) { ++skippedVersions; return; } m_versions.append({id, name, o["serverUrl"].toString(), versionId}); if (id >= m_nextVersionId) m_nextVersionId = id + 1; }); if (skippedVersions > 0) { m_storageIssues << tr("В %1 пропущены записи без имени и ID версии: %2") .arg(QDir::toNativeSeparators(LauncherPaths::versionsFile())) .arg(skippedVersions); } std::sort(m_versions.begin(), m_versions.end(), [](const Version &a, const Version &b) { return a.id < b.id; }); } // Файлы могли не появиться при первом запуске, быть удалены или отложены в .bak — // создаём заново, чтобы папка лаунчера всегда была укомплектована. void LauncherBackend::ensureDataFiles() { if (!QFile::exists(LauncherPaths::settingsFile())) saveSettings(); if (!QFile::exists(LauncherPaths::profilesFile())) saveProfiles(); if (!QFile::exists(LauncherPaths::versionsFile())) saveVersions(); } void LauncherBackend::saveProfiles() { QJsonArray arr; for (const auto &p : m_profiles) arr.append(QJsonObject{{"id", p.id}, {"name", p.name}, {"login", p.login}, {"password", p.password}, {"authType", p.authType}, {"clientToken", p.clientToken}, {"accessToken", p.accessToken}, {"uuid", p.uuid}, {"resolvedName", p.resolvedName}, {"userType", p.userType}}); writeJsonFile(LauncherPaths::profilesFile(), QJsonDocument(arr)); } void LauncherBackend::saveVersions() { QJsonArray arr; for (const auto &v : m_versions) arr.append(QJsonObject{{"id", v.id}, {"name", v.name}, {"serverUrl", v.serverUrl}, {"versionId", v.versionId}}); writeJsonFile(LauncherPaths::versionsFile(), QJsonDocument(arr)); } void LauncherBackend::loadSettings() { m_settings = {{QStringLiteral("gameDir"), QString()}, {QStringLiteral("javaPath"), QString()}, {QStringLiteral("minMemoryMb"), 512}, {QStringLiteral("maxMemoryMb"), 4096}, {QStringLiteral("jvmArgs"), QString()}, {QStringLiteral("windowWidth"), 0}, {QStringLiteral("windowHeight"), 0}, {QStringLiteral("fullscreen"), false}, {QStringLiteral("launcherName"), QStringLiteral("KishkaLauncher")}, {QStringLiteral("launcherVersion"), QStringLiteral("1.0")}}; const QJsonObject stored = readObjectFile(LauncherPaths::settingsFile()); for (auto it = stored.constBegin(); it != stored.constEnd(); ++it) m_settings.insert(it.key(), it.value().toVariant()); } void LauncherBackend::saveSettings() { writeJsonFile(LauncherPaths::settingsFile(), QJsonDocument(QJsonObject::fromVariantMap(m_settings))); }