diff --git a/CMakeLists.txt b/CMakeLists.txt index db6fa0a..490c7f6 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -18,8 +18,6 @@ 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 4acbda5..4d353fd 100644 --- a/authservice.cpp +++ b/authservice.cpp @@ -13,8 +13,7 @@ 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 без дефисов, вручную введённый мог прийти и с ними. @@ -39,7 +38,8 @@ 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,11 +49,10 @@ 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; @@ -69,17 +68,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(); @@ -119,9 +118,8 @@ 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); @@ -145,21 +143,17 @@ 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; @@ -200,14 +194,12 @@ 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; } @@ -218,29 +210,27 @@ 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; @@ -258,9 +248,8 @@ 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; } @@ -279,39 +268,30 @@ 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 72111fe..e957419 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,19 +56,16 @@ 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 e0aab41..13e2615 100644 --- a/gamelauncher.cpp +++ b/gamelauncher.cpp @@ -45,14 +45,15 @@ 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 { @@ -100,7 +101,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); @@ -113,11 +114,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; @@ -125,11 +126,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); @@ -173,8 +174,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); } @@ -186,8 +187,7 @@ 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,10 +289,7 @@ 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 3c845c1..09469e1 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 5382de1..2c0005d 100644 --- a/javalocator.cpp +++ b/javalocator.cpp @@ -4,9 +4,9 @@ #include #include #include -#include #include #include +#include #include #include #include @@ -75,10 +75,8 @@ 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()); @@ -105,7 +103,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")); } } @@ -174,8 +172,7 @@ 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 {}; } @@ -194,8 +191,7 @@ 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 46429e1..f4b7ea2 100644 --- a/javalocator.h +++ b/javalocator.h @@ -17,6 +17,9 @@ 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 41896f8..5614398 100644 --- a/launcherbackend.cpp +++ b/launcherbackend.cpp @@ -3,7 +3,6 @@ #include "authservice.h" #include "gamelauncher.h" #include "javalocator.h" -#include "launcherpaths.h" #include #include @@ -12,7 +11,7 @@ #include #include #include -#include +#include #include #include @@ -33,16 +32,13 @@ 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)) { - // Папка лаунчера создаётся при первом запуске и проверяется при каждом - // следующем — если её удалили, она появится снова. - LauncherPaths::ensureRootExists(&m_storageError); - + QDir().mkpath(m_dataDir); loadSettings(); loadData(); - ensureDataFiles(); connect(m_auth, &AuthService::progress, this, &LauncherBackend::launchProgress); connect(m_launcher, &GameLauncher::progress, this, &LauncherBackend::launchProgress); @@ -55,13 +51,6 @@ 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; @@ -109,9 +98,7 @@ 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(); @@ -247,11 +234,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; } @@ -304,10 +291,9 @@ void LauncherBackend::beginAuthentication() // Ely.by работает только вместе с authlib-injector — без него игра пойдёт // в сессионный сервер Mojang и токен окажется недействительным. - m_auth->ensureAuthlibInjector(LauncherPaths::runtimeDir(), - [this](const QString &path, const QString &error) { + m_auth->ensureAuthlibInjector(m_dataDir, [this](const QString &path, const QString &error) { if (m_pendingProfile < 0) - return; // запуск отменили, пока качался инжектор + return; // запуск отменили, пока качался инжектор if (!error.isEmpty()) { failLaunch(error); return; @@ -459,11 +445,21 @@ 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() ? LauncherPaths::defaultMinecraftDir() - : QDir::fromNativeSeparators(configured); + return configured.isEmpty() ? defaultMinecraftDir() : QDir::fromNativeSeparators(configured); } QVariantMap LauncherBackend::settings() const @@ -526,7 +522,7 @@ void LauncherBackend::loadData() handler(val.toObject()); }; - loadFile(LauncherPaths::profilesFile(), [this](const QJsonObject &o) { + loadFile(m_dataDir + "/profiles.json", [this](const QJsonObject &o) { Profile profile; profile.id = o["id"].toInt(); profile.name = o["name"].toString(); @@ -544,32 +540,18 @@ 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(LauncherPaths::versionsFile(), [this](const QJsonObject &o) { + loadFile(m_dataDir + "/versions.json", [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; - }); -} - -// Файлы могли не появиться при первом запуске или быть удалены — создаём заново, -// чтобы папка лаунчера всегда была укомплектована. -void LauncherBackend::ensureDataFiles() -{ - if (!QFile::exists(LauncherPaths::settingsFile())) - saveSettings(); - if (!QFile::exists(LauncherPaths::profilesFile())) - saveProfiles(); - if (!QFile::exists(LauncherPaths::versionsFile())) - saveVersions(); + std::sort(m_versions.begin(), m_versions.end(), + [](const Version &a, const Version &b) { return a.id < b.id; }); } void LauncherBackend::saveProfiles() @@ -587,7 +569,7 @@ void LauncherBackend::saveProfiles() {"resolvedName", p.resolvedName}, {"userType", p.userType}}); - QFile f(LauncherPaths::profilesFile()); + QFile f(m_dataDir + "/profiles.json"); if (f.open(QIODevice::WriteOnly)) f.write(QJsonDocument(arr).toJson()); } @@ -601,7 +583,7 @@ void LauncherBackend::saveVersions() {"serverUrl", v.serverUrl}, {"versionId", v.versionId}}); - QFile f(LauncherPaths::versionsFile()); + QFile f(m_dataDir + "/versions.json"); if (f.open(QIODevice::WriteOnly)) f.write(QJsonDocument(arr).toJson()); } @@ -619,7 +601,7 @@ void LauncherBackend::loadSettings() {QStringLiteral("launcherName"), QStringLiteral("KishkaLauncher")}, {QStringLiteral("launcherVersion"), QStringLiteral("1.0")}}; - QFile f(LauncherPaths::settingsFile()); + QFile f(m_dataDir + "/settings.json"); if (!f.open(QIODevice::ReadOnly)) return; @@ -630,7 +612,7 @@ void LauncherBackend::loadSettings() void LauncherBackend::saveSettings() { - QFile f(LauncherPaths::settingsFile()); + QFile f(m_dataDir + "/settings.json"); if (f.open(QIODevice::WriteOnly)) f.write(QJsonDocument(QJsonObject::fromVariantMap(m_settings)).toJson()); } diff --git a/launcherbackend.h b/launcherbackend.h index f804653..5d4d5ac 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_storageError; + QString m_dataDir; int m_nextProfileId = 1; int m_nextVersionId = 1; diff --git a/launcherpaths.cpp b/launcherpaths.cpp deleted file mode 100644 index c2382f6..0000000 --- a/launcherpaths.cpp +++ /dev/null @@ -1,73 +0,0 @@ -#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 deleted file mode 100644 index a6afb82..0000000 --- a/launcherpaths.h +++ /dev/null @@ -1,29 +0,0 @@ -#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 5aa4b4e..efc76a3 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,12 +163,9 @@ 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-репозитория, путь собираем из координат. @@ -196,12 +193,13 @@ 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; @@ -221,7 +219,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; @@ -265,7 +263,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() @@ -328,14 +326,12 @@ 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; } @@ -396,8 +392,7 @@ 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 — дополняет. @@ -410,12 +405,9 @@ 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")) @@ -445,12 +437,10 @@ 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 d5923ca..5d0e27f 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(); }