From af4230500eef8723b947d1fb91df959d1f34c862 Mon Sep 17 00:00:00 2001 From: galeon Date: Sun, 23 Aug 2026 01:20:18 +0300 Subject: [PATCH] add launcher folder and some checks to it --- CMakeLists.txt | 2 + authservice.cpp | 126 +++++++++++++++++++++++++------------------ authservice.h | 23 ++++---- gamelauncher.cpp | 37 +++++++------ gamelauncher.h | 12 ++--- javalocator.cpp | 16 +++--- javalocator.h | 5 +- launcherbackend.cpp | 88 ++++++++++++++++++------------ launcherbackend.h | 6 +-- launcherpaths.cpp | 73 +++++++++++++++++++++++++ launcherpaths.h | 29 ++++++++++ minecraftversion.cpp | 52 ++++++++++-------- minecraftversion.h | 28 +++++----- 13 files changed, 328 insertions(+), 169 deletions(-) create mode 100644 launcherpaths.cpp create mode 100644 launcherpaths.h diff --git a/CMakeLists.txt b/CMakeLists.txt index 490c7f6..db6fa0a 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -18,6 +18,8 @@ qt_add_qml_module(appMinecraft_launcher SOURCES launcherbackend.h launcherbackend.cpp + launcherpaths.h + launcherpaths.cpp minecraftversion.h minecraftversion.cpp javalocator.h diff --git a/authservice.cpp b/authservice.cpp index 4d353fd..4acbda5 100644 --- a/authservice.cpp +++ b/authservice.cpp @@ -13,7 +13,8 @@ namespace { const QString kAuthServer = QStringLiteral("https://authserver.ely.by"); -const QString kInjectorManifest = QStringLiteral("https://authlib-injector.yushi.moe/artifact/latest.json"); +const QString kInjectorManifest = QStringLiteral( + "https://authlib-injector.yushi.moe/artifact/latest.json"); const QString kInjectorFileName = QStringLiteral("authlib-injector.jar"); // Ely.by отдаёт UUID без дефисов, вручную введённый мог прийти и с ними. @@ -38,8 +39,7 @@ QString describeElyError(const QJsonObject &response, int status) AuthService::AuthService(QObject *parent) : QObject(parent) , m_network(new QNetworkAccessManager(this)) -{ -} +{} AuthResult AuthService::offline(const QString &nickname) { @@ -49,10 +49,11 @@ AuthResult AuthService::offline(const QString &nickname) return result; } - QByteArray digest = QCryptographicHash::hash( - QStringLiteral("OfflinePlayer:%1").arg(nickname).toUtf8(), QCryptographicHash::Md5); - digest[6] = static_cast((digest[6] & 0x0f) | 0x30); // версия UUID = 3 - digest[8] = static_cast((digest[8] & 0x3f) | 0x80); // вариант RFC 4122 + QByteArray digest + = QCryptographicHash::hash(QStringLiteral("OfflinePlayer:%1").arg(nickname).toUtf8(), + QCryptographicHash::Md5); + digest[6] = static_cast((digest[6] & 0x0f) | 0x30); // версия UUID = 3 + digest[8] = static_cast((digest[8] & 0x3f) | 0x80); // вариант RFC 4122 result.ok = true; result.playerName = nickname; @@ -68,17 +69,17 @@ QString AuthService::generateClientToken() return QUuid::createUuid().toString(QUuid::WithoutBraces); } -void AuthService::postJson( - const QString &endpoint, - const QJsonObject &body, - std::function handler) +void AuthService::postJson(const QString &endpoint, + const QJsonObject &body, + std::function handler) { QNetworkRequest request{QUrl(kAuthServer + endpoint)}; request.setHeader(QNetworkRequest::ContentTypeHeader, QStringLiteral("application/json")); request.setAttribute(QNetworkRequest::RedirectPolicyAttribute, QNetworkRequest::NoLessSafeRedirectPolicy); - QNetworkReply *reply = m_network->post(request, QJsonDocument(body).toJson(QJsonDocument::Compact)); + QNetworkReply *reply = m_network->post(request, + QJsonDocument(body).toJson(QJsonDocument::Compact)); connect(reply, &QNetworkReply::finished, this, [reply, handler = std::move(handler)]() { reply->deleteLater(); @@ -118,8 +119,9 @@ void AuthService::loginElyBy(const QString &login, emit progress(tr("Проверка сохранённого токена Ely.by…")); postJson(QStringLiteral("/auth/validate"), {{QStringLiteral("accessToken"), accessToken}}, - [this, login, password, token, accessToken, callback]( - int status, const QJsonObject &, const QString &transportError) { + [this, login, password, token, accessToken, callback](int status, + const QJsonObject &, + const QString &transportError) { if (!transportError.isEmpty()) { AuthResult result; result.error = tr("Нет связи с authserver.ely.by: %1").arg(transportError); @@ -143,17 +145,21 @@ void AuthService::loginElyBy(const QString &login, {{QStringLiteral("accessToken"), accessToken}, {QStringLiteral("clientToken"), token}, {QStringLiteral("requestUser"), true}}, - [this, login, password, token, callback]( - int refreshStatus, const QJsonObject &response, const QString &error) { + [this, login, password, token, callback](int refreshStatus, + const QJsonObject &response, + const QString &error) { if (error.isEmpty() && refreshStatus == 200) { AuthResult result; result.ok = true; - result.accessToken = response.value(QStringLiteral("accessToken")).toString(); + result.accessToken = response.value(QStringLiteral("accessToken")) + .toString(); result.clientToken = token; - const QJsonObject profile = - response.value(QStringLiteral("selectedProfile")).toObject(); - result.playerName = profile.value(QStringLiteral("name")).toString(); - result.uuid = normalizeUuid(profile.value(QStringLiteral("id")).toString()); + const QJsonObject profile + = response.value(QStringLiteral("selectedProfile")).toObject(); + result.playerName = profile.value(QStringLiteral("name")) + .toString(); + result.uuid = normalizeUuid( + profile.value(QStringLiteral("id")).toString()); result.userType = QStringLiteral("ELYBY"); callback(result); return; @@ -194,12 +200,14 @@ void AuthService::authenticate(const QString &login, {QStringLiteral("password"), password}, {QStringLiteral("clientToken"), clientToken}, {QStringLiteral("requestUser"), true}}, - [clientToken, callback](int status, const QJsonObject &response, const QString &transportError) { + [clientToken, + callback](int status, const QJsonObject &response, const QString &transportError) { AuthResult result; result.clientToken = clientToken; if (!transportError.isEmpty()) { - result.error = AuthService::tr("Нет связи с authserver.ely.by: %1").arg(transportError); + result.error = AuthService::tr("Нет связи с authserver.ely.by: %1") + .arg(transportError); callback(result); return; } @@ -210,27 +218,29 @@ void AuthService::authenticate(const QString &login, result.twoFactorRequired = message.contains(QStringLiteral("two factor"), Qt::CaseInsensitive); result.error = result.twoFactorRequired - ? AuthService::tr("Аккаунт защищён двухфакторной аутентификацией") + ? AuthService::tr( + "Аккаунт защищён двухфакторной аутентификацией") : message; callback(result); return; } - const QJsonObject profile = response.value(QStringLiteral("selectedProfile")).toObject(); + const QJsonObject profile = response.value(QStringLiteral("selectedProfile")) + .toObject(); result.playerName = profile.value(QStringLiteral("name")).toString(); result.uuid = normalizeUuid(profile.value(QStringLiteral("id")).toString()); result.accessToken = response.value(QStringLiteral("accessToken")).toString(); result.userType = QStringLiteral("ELYBY"); result.ok = !result.accessToken.isEmpty() && !result.playerName.isEmpty(); if (!result.ok) - result.error = AuthService::tr("Ely.by не вернул игровой профиль для этого аккаунта"); + result.error = AuthService::tr( + "Ely.by не вернул игровой профиль для этого аккаунта"); callback(result); }); } void AuthService::ensureAuthlibInjector( - const QString &targetDir, - std::function callback) + const QString &targetDir, std::function callback) { QDir().mkpath(targetDir); const QString jarPath = targetDir + u'/' + kInjectorFileName; @@ -248,8 +258,9 @@ void AuthService::ensureAuthlibInjector( connect(manifestReply, &QNetworkReply::finished, this, [this, manifestReply, jarPath, callback]() { manifestReply->deleteLater(); if (manifestReply->error() != QNetworkReply::NoError) { - callback({}, tr("Не удалось получить сведения об authlib-injector: %1") - .arg(manifestReply->errorString())); + callback({}, + tr("Не удалось получить сведения об authlib-injector: %1") + .arg(manifestReply->errorString())); return; } @@ -268,30 +279,39 @@ void AuthService::ensureAuthlibInjector( jarRequest.setAttribute(QNetworkRequest::RedirectPolicyAttribute, QNetworkRequest::NoLessSafeRedirectPolicy); QNetworkReply *jarReply = m_network->get(jarRequest); - connect(jarReply, &QNetworkReply::finished, this, [jarReply, jarPath, expectedSha256, callback]() { - jarReply->deleteLater(); - if (jarReply->error() != QNetworkReply::NoError) { - callback({}, AuthService::tr("Не удалось скачать authlib-injector: %1").arg(jarReply->errorString())); - return; - } + connect(jarReply, + &QNetworkReply::finished, + this, + [jarReply, jarPath, expectedSha256, callback]() { + jarReply->deleteLater(); + if (jarReply->error() != QNetworkReply::NoError) { + callback({}, + AuthService::tr("Не удалось скачать authlib-injector: %1") + .arg(jarReply->errorString())); + return; + } - const QByteArray payload = jarReply->readAll(); - if (!expectedSha256.isEmpty()) { - const QString actual = QString::fromLatin1( - QCryptographicHash::hash(payload, QCryptographicHash::Sha256).toHex()); - if (actual != expectedSha256) { - callback({}, AuthService::tr("Контрольная сумма authlib-injector не совпала")); - return; - } - } + const QByteArray payload = jarReply->readAll(); + if (!expectedSha256.isEmpty()) { + const QString actual = QString::fromLatin1( + QCryptographicHash::hash(payload, QCryptographicHash::Sha256).toHex()); + if (actual != expectedSha256) { + callback({}, + AuthService::tr( + "Контрольная сумма authlib-injector не совпала")); + return; + } + } - QFile file(jarPath); - if (!file.open(QIODevice::WriteOnly) || file.write(payload) != payload.size()) { - callback({}, AuthService::tr("Не удалось сохранить %1").arg(QDir::toNativeSeparators(jarPath))); - return; - } - file.close(); - callback(jarPath, {}); - }); + QFile file(jarPath); + if (!file.open(QIODevice::WriteOnly) || file.write(payload) != payload.size()) { + callback({}, + AuthService::tr("Не удалось сохранить %1") + .arg(QDir::toNativeSeparators(jarPath))); + return; + } + file.close(); + callback(jarPath, {}); + }); }); } diff --git a/authservice.h b/authservice.h index e957419..72111fe 100644 --- a/authservice.h +++ b/authservice.h @@ -14,11 +14,11 @@ struct AuthResult bool twoFactorRequired = false; QString error; - QString playerName; // ${auth_player_name} - QString uuid; // ${auth_uuid}, hex без дефисов - QString accessToken; // ${auth_access_token} - QString clientToken; // ${clientid} - QString userType; // ${user_type}: legacy / msa / ELYBY + QString playerName; // ${auth_player_name} + QString uuid; // ${auth_uuid}, hex без дефисов + QString accessToken; // ${auth_access_token} + QString clientToken; // ${clientid} + QString userType; // ${user_type}: legacy / msa / ELYBY }; // Yggdrasil-клиент Ely.by плюс офлайн-режим. Все сетевые методы асинхронные: @@ -56,16 +56,19 @@ public: // Скачивает authlib-injector в targetDir, если его там ещё нет. // Колбэк получает путь к jar либо текст ошибки. - void ensureAuthlibInjector(const QString &targetDir, - std::function callback); + void ensureAuthlibInjector( + const QString &targetDir, + std::function callback); signals: void progress(const QString &message); private: - void postJson(const QString &endpoint, - const QJsonObject &body, - std::function handler); + void postJson( + const QString &endpoint, + const QJsonObject &body, + std::function + handler); void authenticate(const QString &login, const QString &password, diff --git a/gamelauncher.cpp b/gamelauncher.cpp index 13e2615..e0aab41 100644 --- a/gamelauncher.cpp +++ b/gamelauncher.cpp @@ -45,15 +45,14 @@ bool isNativeLibraryFile(const QString &fileName) for (const QString &suffix : suffixes) if (fileName.endsWith(suffix, Qt::CaseInsensitive)) return true; - return fileName.contains(QStringLiteral(".so.")); // libfoo.so.1 + return fileName.contains(QStringLiteral(".so.")); // libfoo.so.1 } } // namespace GameLauncher::GameLauncher(QObject *parent) : QObject(parent) -{ -} +{} bool GameLauncher::isRunning() const { @@ -101,7 +100,7 @@ QStringList GameLauncher::buildArguments(const LaunchOptions &options, QStringList classpath; for (const MinecraftLibrary &library : version.libraries) { if (library.native) - continue; // натив-библиотеки только распаковываются + continue; // натив-библиотеки только распаковываются classpath << nativeToken(libraryPath(options.gameDir, library)); } classpath << nativeToken(version.clientJarPath); @@ -114,11 +113,11 @@ QStringList GameLauncher::buildArguments(const LaunchOptions &options, variables[QStringLiteral("auth_player_name")] = options.playerName; variables[QStringLiteral("auth_uuid")] = options.uuid; variables[QStringLiteral("auth_access_token")] = options.accessToken; - variables[QStringLiteral("auth_session")] = - QStringLiteral("token:%1:%2").arg(options.accessToken, options.uuid); + variables[QStringLiteral("auth_session")] = QStringLiteral("token:%1:%2") + .arg(options.accessToken, options.uuid); variables[QStringLiteral("auth_xuid")] = QStringLiteral("0"); - variables[QStringLiteral("clientid")] = - options.clientToken.isEmpty() ? QStringLiteral("0") : options.clientToken; + variables[QStringLiteral("clientid")] = options.clientToken.isEmpty() ? QStringLiteral("0") + : options.clientToken; variables[QStringLiteral("user_type")] = options.userType; variables[QStringLiteral("user_properties")] = QStringLiteral("{}"); variables[QStringLiteral("version_name")] = version.id; @@ -126,11 +125,11 @@ QStringList GameLauncher::buildArguments(const LaunchOptions &options, variables[QStringLiteral("game_directory")] = nativeToken(options.gameDir); variables[QStringLiteral("assets_root")] = nativeToken(assetsRoot); variables[QStringLiteral("assets_index_name")] = version.assetIndexId; - variables[QStringLiteral("game_assets")] = - nativeToken(legacyAssets ? assetsRoot + QStringLiteral("/virtual/legacy") : assetsRoot); + variables[QStringLiteral("game_assets")] = nativeToken( + legacyAssets ? assetsRoot + QStringLiteral("/virtual/legacy") : assetsRoot); variables[QStringLiteral("natives_directory")] = nativeToken(nativesDir); - variables[QStringLiteral("library_directory")] = - nativeToken(options.gameDir + QStringLiteral("/libraries")); + variables[QStringLiteral("library_directory")] = nativeToken(options.gameDir + + QStringLiteral("/libraries")); variables[QStringLiteral("classpath")] = classpath.join(kClasspathSeparator); variables[QStringLiteral("classpath_separator")] = QString(kClasspathSeparator); variables[QStringLiteral("primary_jar")] = nativeToken(version.clientJarPath); @@ -174,8 +173,8 @@ QStringList GameLauncher::buildArguments(const LaunchOptions &options, } // Разрешение окна: у старых версий фич-флага нет, аргументы всё равно понимаются. - if (options.windowWidth > 0 && options.windowHeight > 0 - && !version.hasCustomResolutionArgs && !arguments.contains(QStringLiteral("--width"))) { + if (options.windowWidth > 0 && options.windowHeight > 0 && !version.hasCustomResolutionArgs + && !arguments.contains(QStringLiteral("--width"))) { arguments << QStringLiteral("--width") << QString::number(options.windowWidth) << QStringLiteral("--height") << QString::number(options.windowHeight); } @@ -187,7 +186,8 @@ QStringList GameLauncher::buildArguments(const LaunchOptions &options, arguments << QStringLiteral("--quickPlayMultiplayer") << options.serverAddress; } else { const int colon = options.serverAddress.lastIndexOf(u':'); - const QString host = colon > 0 ? options.serverAddress.left(colon) : options.serverAddress; + const QString host = colon > 0 ? options.serverAddress.left(colon) + : options.serverAddress; const QString port = colon > 0 ? options.serverAddress.mid(colon + 1) : QStringLiteral("25565"); arguments << QStringLiteral("--server") << host << QStringLiteral("--port") << port; @@ -247,7 +247,7 @@ bool GameLauncher::extractNatives(const LaunchOptions &options, const QString target = nativesDir + u'/' + fileName; const QByteArray data = reader.fileData(entry.filePath); if (QFileInfo(target).size() == data.size() && QFile::exists(target)) - continue; // уже распаковано этим же лаунчером + continue; // уже распаковано этим же лаунчером QFile file(target); if (!file.open(QIODevice::WriteOnly) || file.write(data) != data.size()) { @@ -289,7 +289,10 @@ bool GameLauncher::launch(const LaunchOptions &options, return false; emit progress(tr("Поиск Java %1…").arg(version.javaMajor)); - const QString java = JavaLocator::select(options.gameDir, version.javaMajor, options.javaPath, error); + const QString java = JavaLocator::select(options.gameDir, + version.javaMajor, + options.javaPath, + error); if (java.isEmpty()) return false; diff --git a/gamelauncher.h b/gamelauncher.h index 09469e1..3c845c1 100644 --- a/gamelauncher.h +++ b/gamelauncher.h @@ -10,8 +10,8 @@ class QProcess; // Всё, что лаунчер знает к моменту нажатия PLAY. struct LaunchOptions { - QString gameDir; // .minecraft - QString versionId; // Папка в versions + QString gameDir; // .minecraft + QString versionId; // Папка в versions QString playerName; QString uuid; @@ -19,17 +19,17 @@ struct LaunchOptions QString userType; QString clientToken; - QString javaPath; // Пусто — ищем сами + QString javaPath; // Пусто — ищем сами int minMemoryMb = 512; int maxMemoryMb = 4096; QStringList extraJvmArgs; - int windowWidth = 0; // 0 — не передавать --width/--height + int windowWidth = 0; // 0 — не передавать --width/--height int windowHeight = 0; bool fullscreen = false; - QString serverAddress; // host[:port] — автозаход на сервер - QString authlibInjectorPath; // Пусто — не подключаем + QString serverAddress; // host[:port] — автозаход на сервер + QString authlibInjectorPath; // Пусто — не подключаем QString authlibInjectorApi = QStringLiteral("ely.by"); QString launcherName = QStringLiteral("KishkaLauncher"); diff --git a/javalocator.cpp b/javalocator.cpp index 2c0005d..5382de1 100644 --- a/javalocator.cpp +++ b/javalocator.cpp @@ -4,9 +4,9 @@ #include #include #include +#include #include #include -#include #include #include #include @@ -75,8 +75,10 @@ QStringList JavaLocator::findAll(const QString &gameDir) // 1. Рантайм, скачанный самим Minecraft: .minecraft/runtime////bin const QString runtime = gameDir + QStringLiteral("/runtime"); if (QDir(runtime).exists()) { - QDirIterator it(runtime, QStringList{QString::fromLatin1(kExecutable)}, - QDir::Files | QDir::Executable, QDirIterator::Subdirectories); + QDirIterator it(runtime, + QStringList{QString::fromLatin1(kExecutable)}, + QDir::Files | QDir::Executable, + QDirIterator::Subdirectories); while (it.hasNext()) { it.next(); appendIfExecutable(result, it.fileInfo().absolutePath()); @@ -103,7 +105,7 @@ QStringList JavaLocator::findAll(const QString &gameDir) for (const QString &entry : entries) { const QString home = dir.absoluteFilePath(entry); appendIfExecutable(result, home + QStringLiteral("/bin")); - appendIfExecutable(result, home + QStringLiteral("/Contents/Home/bin")); // macOS + appendIfExecutable(result, home + QStringLiteral("/Contents/Home/bin")); // macOS appendIfExecutable(result, home + QStringLiteral("/jre/bin")); } } @@ -172,7 +174,8 @@ QString JavaLocator::select(const QString &gameDir, if (candidates.isEmpty()) { if (error) - *error = QObject::tr("Java не найдена. Установите JDK %1 или укажите путь в настройках.") + *error = QObject::tr( + "Java не найдена. Установите JDK %1 или укажите путь в настройках.") .arg(requiredMajor); return {}; } @@ -191,7 +194,8 @@ QString JavaLocator::select(const QString &gameDir, if (error) { *error = bestBelowMajor > 0 - ? QObject::tr("Для этой версии нужна Java %1 или новее, найдена только Java %2 (%3).") + ? QObject::tr( + "Для этой версии нужна Java %1 или новее, найдена только Java %2 (%3).") .arg(requiredMajor) .arg(bestBelowMajor) .arg(QDir::toNativeSeparators(bestBelow)) diff --git a/javalocator.h b/javalocator.h index f4b7ea2..46429e1 100644 --- a/javalocator.h +++ b/javalocator.h @@ -17,9 +17,6 @@ QString windowlessVariant(const QString &javaPath); // Выбирает java не ниже requiredMajor. preferred, если задан, проверяется первым. // При неудаче возвращает пустую строку и заполняет error. -QString select(const QString &gameDir, - int requiredMajor, - const QString &preferred, - QString *error); +QString select(const QString &gameDir, int requiredMajor, const QString &preferred, QString *error); } // namespace JavaLocator diff --git a/launcherbackend.cpp b/launcherbackend.cpp index 5614398..41896f8 100644 --- a/launcherbackend.cpp +++ b/launcherbackend.cpp @@ -3,6 +3,7 @@ #include "authservice.h" #include "gamelauncher.h" #include "javalocator.h" +#include "launcherpaths.h" #include #include @@ -11,7 +12,7 @@ #include #include #include -#include +#include #include #include @@ -32,13 +33,16 @@ int settingInt(const QVariantMap &settings, const QString &key, int fallback) LauncherBackend::LauncherBackend(QObject *parent) : QObject(parent) - , m_dataDir(QStandardPaths::writableLocation(QStandardPaths::AppDataLocation)) , m_auth(new AuthService(this)) , m_launcher(new GameLauncher(this)) { - QDir().mkpath(m_dataDir); + // Папка лаунчера создаётся при первом запуске и проверяется при каждом + // следующем — если её удалили, она появится снова. + LauncherPaths::ensureRootExists(&m_storageError); + loadSettings(); loadData(); + ensureDataFiles(); connect(m_auth, &AuthService::progress, this, &LauncherBackend::launchProgress); connect(m_launcher, &GameLauncher::progress, this, &LauncherBackend::launchProgress); @@ -51,6 +55,13 @@ LauncherBackend::LauncherBackend(QObject *parent) emit gameRunningChanged(); emit gameFinished(exitCode, crashed); }); + + // Интерфейс подключается к сигналам уже после конструктора — сообщаем отложенно. + if (!m_storageError.isEmpty()) { + QTimer::singleShot(0, this, [this] { + emit launchError(tr("Не удалось создать папку лаунчера: %1").arg(m_storageError)); + }); + } } LauncherBackend::~LauncherBackend() = default; @@ -98,7 +109,9 @@ void LauncherBackend::addProfile(const QString &name, emit profilesChanged(); } -void LauncherBackend::addVersion(const QString &name, const QString &serverUrl, const QString &versionId) +void LauncherBackend::addVersion(const QString &name, + const QString &serverUrl, + const QString &versionId) { m_versions.append({m_nextVersionId++, name, serverUrl, versionId}); saveVersions(); @@ -234,11 +247,11 @@ void LauncherBackend::launchGame(int profileIndex, int versionIndex) const QString id = versionIdOf(m_versions[versionIndex]); const QStringList installed = VersionLoader::installedVersions(directory); if (!installed.contains(id)) { - emit launchError(installed.isEmpty() - ? tr("В %1 нет ни одной установленной версии") - .arg(QDir::toNativeSeparators(directory + QStringLiteral("/versions"))) - : tr("Версия «%1» не установлена. Доступны: %2") - .arg(id, installed.join(QStringLiteral(", ")))); + emit launchError(installed.isEmpty() ? tr("В %1 нет ни одной установленной версии") + .arg(QDir::toNativeSeparators( + directory + QStringLiteral("/versions"))) + : tr("Версия «%1» не установлена. Доступны: %2") + .arg(id, installed.join(QStringLiteral(", ")))); return; } @@ -291,9 +304,10 @@ void LauncherBackend::beginAuthentication() // Ely.by работает только вместе с authlib-injector — без него игра пойдёт // в сессионный сервер Mojang и токен окажется недействительным. - m_auth->ensureAuthlibInjector(m_dataDir, [this](const QString &path, const QString &error) { + m_auth->ensureAuthlibInjector(LauncherPaths::runtimeDir(), + [this](const QString &path, const QString &error) { if (m_pendingProfile < 0) - return; // запуск отменили, пока качался инжектор + return; // запуск отменили, пока качался инжектор if (!error.isEmpty()) { failLaunch(error); return; @@ -445,21 +459,11 @@ void LauncherBackend::setBusy(bool busy) emit busyChanged(); } -QString LauncherBackend::defaultMinecraftDir() const -{ -#if defined(Q_OS_WIN) - return QDir::homePath() + QStringLiteral("/AppData/Roaming/.minecraft"); -#elif defined(Q_OS_MACOS) - return QDir::homePath() + QStringLiteral("/Library/Application Support/minecraft"); -#else // Linux и прочие Unix-системы - return QDir::homePath() + QStringLiteral("/.minecraft"); -#endif -} - QString LauncherBackend::gameDir() const { const QString configured = m_settings.value(QStringLiteral("gameDir")).toString().trimmed(); - return configured.isEmpty() ? defaultMinecraftDir() : QDir::fromNativeSeparators(configured); + return configured.isEmpty() ? LauncherPaths::defaultMinecraftDir() + : QDir::fromNativeSeparators(configured); } QVariantMap LauncherBackend::settings() const @@ -522,7 +526,7 @@ void LauncherBackend::loadData() handler(val.toObject()); }; - loadFile(m_dataDir + "/profiles.json", [this](const QJsonObject &o) { + loadFile(LauncherPaths::profilesFile(), [this](const QJsonObject &o) { Profile profile; profile.id = o["id"].toInt(); profile.name = o["name"].toString(); @@ -540,18 +544,32 @@ void LauncherBackend::loadData() 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; }); + std::sort(m_profiles.begin(), m_profiles.end(), [](const Profile &a, const Profile &b) { + return a.id < b.id; + }); - loadFile(m_dataDir + "/versions.json", [this](const QJsonObject &o) { + loadFile(LauncherPaths::versionsFile(), [this](const QJsonObject &o) { const int id = o["id"].toInt(); - m_versions.append({id, o["name"].toString(), o["serverUrl"].toString(), - o.value("versionId").toString()}); + m_versions.append( + {id, o["name"].toString(), o["serverUrl"].toString(), o.value("versionId").toString()}); if (id >= m_nextVersionId) m_nextVersionId = id + 1; }); - std::sort(m_versions.begin(), m_versions.end(), - [](const Version &a, const Version &b) { return a.id < b.id; }); + std::sort(m_versions.begin(), m_versions.end(), [](const Version &a, const Version &b) { + return a.id < b.id; + }); +} + +// Файлы могли не появиться при первом запуске или быть удалены — создаём заново, +// чтобы папка лаунчера всегда была укомплектована. +void LauncherBackend::ensureDataFiles() +{ + if (!QFile::exists(LauncherPaths::settingsFile())) + saveSettings(); + if (!QFile::exists(LauncherPaths::profilesFile())) + saveProfiles(); + if (!QFile::exists(LauncherPaths::versionsFile())) + saveVersions(); } void LauncherBackend::saveProfiles() @@ -569,7 +587,7 @@ void LauncherBackend::saveProfiles() {"resolvedName", p.resolvedName}, {"userType", p.userType}}); - QFile f(m_dataDir + "/profiles.json"); + QFile f(LauncherPaths::profilesFile()); if (f.open(QIODevice::WriteOnly)) f.write(QJsonDocument(arr).toJson()); } @@ -583,7 +601,7 @@ void LauncherBackend::saveVersions() {"serverUrl", v.serverUrl}, {"versionId", v.versionId}}); - QFile f(m_dataDir + "/versions.json"); + QFile f(LauncherPaths::versionsFile()); if (f.open(QIODevice::WriteOnly)) f.write(QJsonDocument(arr).toJson()); } @@ -601,7 +619,7 @@ void LauncherBackend::loadSettings() {QStringLiteral("launcherName"), QStringLiteral("KishkaLauncher")}, {QStringLiteral("launcherVersion"), QStringLiteral("1.0")}}; - QFile f(m_dataDir + "/settings.json"); + QFile f(LauncherPaths::settingsFile()); if (!f.open(QIODevice::ReadOnly)) return; @@ -612,7 +630,7 @@ void LauncherBackend::loadSettings() void LauncherBackend::saveSettings() { - QFile f(m_dataDir + "/settings.json"); + QFile f(LauncherPaths::settingsFile()); if (f.open(QIODevice::WriteOnly)) f.write(QJsonDocument(QJsonObject::fromVariantMap(m_settings)).toJson()); } diff --git a/launcherbackend.h b/launcherbackend.h index 5d4d5ac..f804653 100644 --- a/launcherbackend.h +++ b/launcherbackend.h @@ -90,7 +90,7 @@ private: { int id = 0; QString name, login, password; - QString authType = QStringLiteral("offline"); // offline | elyby + QString authType = QStringLiteral("offline"); // offline | elyby QString clientToken, accessToken, uuid, resolvedName, userType; }; struct Version @@ -101,11 +101,11 @@ private: void loadData(); void loadSettings(); + void ensureDataFiles(); void saveProfiles(); void saveVersions(); void saveSettings(); - QString defaultMinecraftDir() const; QString gameDir() const; QString versionIdOf(const Version &version) const; @@ -117,7 +117,7 @@ private: QList m_profiles; QList m_versions; QVariantMap m_settings; - QString m_dataDir; + QString m_storageError; int m_nextProfileId = 1; int m_nextVersionId = 1; diff --git a/launcherpaths.cpp b/launcherpaths.cpp new file mode 100644 index 0000000..c2382f6 --- /dev/null +++ b/launcherpaths.cpp @@ -0,0 +1,73 @@ +#include "launcherpaths.h" + +#include +#include + +namespace { + +const QString kFolderName = QStringLiteral("galeonLauncher"); + +} // namespace + +QString LauncherPaths::containerDir() +{ +#if defined(Q_OS_WIN) + return QDir::homePath() + QStringLiteral("/AppData/Roaming"); +#elif defined(Q_OS_MACOS) + return QDir::homePath() + QStringLiteral("/Library/Application Support"); +#else // Linux и прочие Unix-системы + return QDir::homePath(); +#endif +} + +QString LauncherPaths::rootDir() +{ + return containerDir() + u'/' + kFolderName; +} + +QString LauncherPaths::defaultMinecraftDir() +{ +#if defined(Q_OS_MACOS) + // На macOS папка игры исторически без точки. + return containerDir() + QStringLiteral("/minecraft"); +#else + return containerDir() + QStringLiteral("/.minecraft"); +#endif +} + +QString LauncherPaths::settingsFile() +{ + return rootDir() + QStringLiteral("/settings.json"); +} + +QString LauncherPaths::profilesFile() +{ + return rootDir() + QStringLiteral("/profiles.json"); +} + +QString LauncherPaths::versionsFile() +{ + return rootDir() + QStringLiteral("/versions.json"); +} + +QString LauncherPaths::runtimeDir() +{ + return rootDir() + QStringLiteral("/runtime"); +} + +bool LauncherPaths::ensureRootExists(QString *error) +{ + const QStringList required{rootDir(), runtimeDir()}; + for (const QString &path : required) { + if (QDir().mkpath(path)) + continue; + + if (error) { + *error = QCoreApplication::translate("LauncherPaths", + "не удалось создать %1") + .arg(QDir::toNativeSeparators(path)); + } + return false; + } + return true; +} diff --git a/launcherpaths.h b/launcherpaths.h new file mode 100644 index 0000000..a6afb82 --- /dev/null +++ b/launcherpaths.h @@ -0,0 +1,29 @@ +#pragma once + +#include + +// Пути к данным лаунчера. Собственная папка galeonLauncher лежит рядом со +// стандартной .minecraft, а не в системном каталоге приложения. +namespace LauncherPaths { + +// Родительская папка, в которой лежит .minecraft (и наша galeonLauncher). +QString containerDir(); + +// /galeonLauncher — корень данных лаунчера. +QString rootDir(); + +// Стандартная папка игры. +QString defaultMinecraftDir(); + +QString settingsFile(); +QString profilesFile(); +QString versionsFile(); + +// Сюда качается authlib-injector. +QString runtimeDir(); + +// Создаёт папку лаунчера, если её нет. Вызывается при каждом запуске. +// false и текст в error, если создать не удалось. +bool ensureRootExists(QString *error = nullptr); + +} // namespace LauncherPaths diff --git a/minecraftversion.cpp b/minecraftversion.cpp index efc76a3..5aa4b4e 100644 --- a/minecraftversion.cpp +++ b/minecraftversion.cpp @@ -47,8 +47,8 @@ QString mavenToPath(const QString &name) if (parts.size() > 3) file += u'-' + parts[3]; - return QString(parts[0]).replace(u'.', u'/') + u'/' + parts[1] + u'/' + parts[2] - + u'/' + file + u'.' + extension; + return QString(parts[0]).replace(u'.', u'/') + u'/' + parts[1] + u'/' + parts[2] + u'/' + file + + u'.' + extension; } // Ключ для дедупликации: у наследника и родителя одна и та же библиотека часто @@ -163,9 +163,12 @@ void parseLibraries(const QJsonObject &root, // Основной артефакт. В новом формате (1.19+) натив-библиотеки — это // отдельные записи с классификатором natives-*, у них тоже есть artifact. - if (downloads.contains(QStringLiteral("artifact")) || !downloads.contains(QStringLiteral("classifiers"))) { - MinecraftLibrary library = libraryFromDownload( - name, downloads.value(QStringLiteral("artifact")).toObject(), relativePath); + if (downloads.contains(QStringLiteral("artifact")) + || !downloads.contains(QStringLiteral("classifiers"))) { + MinecraftLibrary library + = libraryFromDownload(name, + downloads.value(QStringLiteral("artifact")).toObject(), + relativePath); // У Fabric у части библиотек нет блока downloads вообще — только // name и адрес Maven-репозитория, путь собираем из координат. @@ -193,13 +196,12 @@ void parseLibraries(const QJsonObject &root, continue; classifier.replace(QStringLiteral("${arch}"), VersionLoader::osArch() == QStringLiteral("x86") ? QStringLiteral("32") - : QStringLiteral("64")); + : QStringLiteral("64")); const QJsonObject classifiers = downloads.value(QStringLiteral("classifiers")).toObject(); - MinecraftLibrary library = libraryFromDownload( - name + u':' + classifier, - classifiers.value(classifier).toObject(), - mavenToPath(name + u':' + classifier)); + MinecraftLibrary library = libraryFromDownload(name + u':' + classifier, + classifiers.value(classifier).toObject(), + mavenToPath(name + u':' + classifier)); if (library.path.isEmpty()) continue; @@ -219,7 +221,7 @@ void pickNativesForCurrentArch(QList &libraries) : VersionLoader::osName(); const QString base = QStringLiteral("natives-") + osPart; - QHash chosen; // group:artifact -> классификатор + QHash chosen; // group:artifact -> классификатор for (const MinecraftLibrary &library : libraries) { if (!library.native) continue; @@ -263,7 +265,7 @@ QString VersionLoader::osArch() const QString arch = QSysInfo::currentCpuArchitecture(); if (arch == QLatin1String("i386") || arch == QLatin1String("i686")) return QStringLiteral("x86"); - return arch; // x86_64 / arm64 — совпадают с тем, что пишет Mojang + return arch; // x86_64 / arm64 — совпадают с тем, что пишет Mojang } QString VersionLoader::nativeClassifier() @@ -326,12 +328,14 @@ MinecraftVersion VersionLoader::load(const QString &gameDir, while (!currentId.isEmpty()) { if (chainIds.contains(currentId)) { if (error) - *error = QObject::tr("Циклическая зависимость inheritsFrom в версии «%1»").arg(currentId); + *error = QObject::tr("Циклическая зависимость inheritsFrom в версии «%1»") + .arg(currentId); return version; } if (chain.size() >= kMaxInheritanceDepth) { if (error) - *error = QObject::tr("Слишком длинная цепочка inheritsFrom у версии «%1»").arg(versionId); + *error = QObject::tr("Слишком длинная цепочка inheritsFrom у версии «%1»") + .arg(versionId); return version; } @@ -392,7 +396,8 @@ MinecraftVersion VersionLoader::load(const QString &gameDir, .value(QStringLiteral("id")) .toString(); if (!fileId.isEmpty()) - version.loggingConfigPath = gameDir + QStringLiteral("/assets/log_configs/") + fileId; + version.loggingConfigPath = gameDir + QStringLiteral("/assets/log_configs/") + + fileId; } // minecraftArguments (до 1.13) заменяет список целиком, arguments.game — дополняет. @@ -405,9 +410,12 @@ MinecraftVersion VersionLoader::load(const QString &gameDir, const QJsonObject arguments = object.value(QStringLiteral("arguments")).toObject(); appendArguments(arguments.value(QStringLiteral("jvm")).toArray(), version.jvmArgs, features); - appendArguments(arguments.value(QStringLiteral("game")).toArray(), version.gameArgs, features); + appendArguments(arguments.value(QStringLiteral("game")).toArray(), + version.gameArgs, + features); - const QByteArray raw = QJsonDocument(arguments.value(QStringLiteral("game")).toArray()).toJson(); + const QByteArray raw = QJsonDocument(arguments.value(QStringLiteral("game")).toArray()) + .toJson(); if (raw.contains("quickPlayMultiplayer")) version.supportsQuickPlay = true; if (raw.contains("resolution_width")) @@ -437,10 +445,12 @@ MinecraftVersion VersionLoader::load(const QString &gameDir, pickNativesForCurrentArch(version.libraries); // До 1.13 jvm-аргументов в json нет — собираем их сами. - const bool hasClasspathArg = std::any_of( - version.jvmArgs.cbegin(), version.jvmArgs.cend(), [](const QString &argument) { - return argument == QLatin1String("-cp") || argument == QLatin1String("-classpath"); - }); + const bool hasClasspathArg = std::any_of(version.jvmArgs.cbegin(), + version.jvmArgs.cend(), + [](const QString &argument) { + return argument == QLatin1String("-cp") + || argument == QLatin1String("-classpath"); + }); if (!hasClasspathArg) { version.jvmArgs << QStringLiteral("-Djava.library.path=${natives_directory}") << QStringLiteral("-Dminecraft.launcher.brand=${launcher_name}") diff --git a/minecraftversion.h b/minecraftversion.h index 5d0e27f..d5923ca 100644 --- a/minecraftversion.h +++ b/minecraftversion.h @@ -9,34 +9,34 @@ // Одна библиотека из client.json уже с разрешёнными правилами. struct MinecraftLibrary { - QString name; // Maven-координаты: org.lwjgl:lwjgl:3.3.1:natives-windows - QString path; // Путь относительно .minecraft/libraries - QString url; // Откуда качать, если файла нет (может быть пустым) + QString name; // Maven-координаты: org.lwjgl:lwjgl:3.3.1:natives-windows + QString path; // Путь относительно .minecraft/libraries + QString url; // Откуда качать, если файла нет (может быть пустым) QString sha1; qint64 size = 0; - bool native = false; // Распаковывается в natives, а не кладётся в classpath - QStringList extractExclude; // Префиксы, которые не распаковываем + bool native = false; // Распаковывается в natives, а не кладётся в classpath + QStringList extractExclude; // Префиксы, которые не распаковываем }; // client.json, «схлопнутый» по всей цепочке inheritsFrom. struct MinecraftVersion { - QString id; // Имя папки в versions, оно же --version + QString id; // Имя папки в versions, оно же --version QString mainClass; - QString type; // release / snapshot / modified — идёт в --versionType - QString assetIndexId; // --assetIndex - QString assetsKind; // Поле "assets": legacy, pre-1.6 либо тот же id - QString clientJarPath; // Абсолютный путь к .jar (может лежать у родителя) + QString type; // release / snapshot / modified — идёт в --versionType + QString assetIndexId; // --assetIndex + QString assetsKind; // Поле "assets": legacy, pre-1.6 либо тот же id + QString clientJarPath; // Абсолютный путь к .jar (может лежать у родителя) int javaMajor = 8; - QStringList jvmArgs; // Ещё с неподставленными ${...} + QStringList jvmArgs; // Ещё с неподставленными ${...} QStringList gameArgs; QList libraries; - QString loggingArgument; // -Dlog4j.configurationFile=${path} - QString loggingConfigPath; // Абсолютный путь к xml, пустой если конфига нет + QString loggingArgument; // -Dlog4j.configurationFile=${path} + QString loggingConfigPath; // Абсолютный путь к xml, пустой если конфига нет - bool supportsQuickPlay = false; // 1.20+ понимает --quickPlayMultiplayer + bool supportsQuickPlay = false; // 1.20+ понимает --quickPlayMultiplayer bool hasCustomResolutionArgs = false; bool isValid() const { return !id.isEmpty() && !mainClass.isEmpty(); }