add launcher folder and some checks to it

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