#include "gamelauncher.h" #include "javalocator.h" #include #include #include #include #include #include namespace { #if defined(Q_OS_WIN) constexpr QChar kClasspathSeparator = u';'; #else constexpr QChar kClasspathSeparator = u':'; #endif QString libraryPath(const QString &gameDir, const MinecraftLibrary &library) { return gameDir + QStringLiteral("/libraries/") + library.path; } QString nativeToken(const QString &path) { return QDir::toNativeSeparators(path); } // Подставляет ${...}. Аргументы, в которых после подстановки остались // неизвестные плейсхолдеры, вызывающий выбрасывает. QString substitute(QString argument, const QHash &variables) { for (auto it = variables.cbegin(); it != variables.cend(); ++it) argument.replace(QStringLiteral("${") + it.key() + u'}', it.value()); return argument; } bool isNativeLibraryFile(const QString &fileName) { static const QStringList suffixes = {QStringLiteral(".dll"), QStringLiteral(".so"), QStringLiteral(".dylib"), QStringLiteral(".jnilib")}; for (const QString &suffix : suffixes) if (fileName.endsWith(suffix, Qt::CaseInsensitive)) return true; return fileName.contains(QStringLiteral(".so.")); // libfoo.so.1 } } // namespace GameLauncher::GameLauncher(QObject *parent) : QObject(parent) { } bool GameLauncher::isRunning() const { return m_process && m_process->state() != QProcess::NotRunning; } QStringList GameLauncher::missingFiles(const LaunchOptions &options, const MinecraftVersion &version, int limit) { QStringList missing; const auto add = [&missing, limit](const QString &path) { if (missing.size() < limit) missing << QDir::toNativeSeparators(path); }; if (!QDir(options.gameDir).exists()) return {QDir::toNativeSeparators(options.gameDir)}; if (!QFile::exists(version.clientJarPath)) add(version.clientJarPath); for (const MinecraftLibrary &library : version.libraries) { const QString path = libraryPath(options.gameDir, library); if (!QFile::exists(path)) add(path); } if (!version.assetIndexId.isEmpty()) { const QString index = options.gameDir + QStringLiteral("/assets/indexes/") + version.assetIndexId + QStringLiteral(".json"); if (!QFile::exists(index)) add(index); else if (!QDir(options.gameDir + QStringLiteral("/assets/objects")).exists()) add(options.gameDir + QStringLiteral("/assets/objects")); } return missing; } namespace { // -DignoreList у Forge 1.17+ — список префиксов имён файлов, которые // BootstrapLauncher не берёт в слой модулей. Сравнение идёт по началу имени. bool ignoreListCovers(const QString &ignoreListArgument, const QString &fileName) { const QString value = ignoreListArgument.mid(QStringLiteral("-DignoreList=").size()); const QStringList prefixes = value.split(u',', Qt::SkipEmptyParts); for (const QString &prefix : prefixes) { if (!prefix.isEmpty() && fileName.startsWith(prefix)) return true; } return false; } } // namespace QStringList GameLauncher::buildArguments(const LaunchOptions &options, const MinecraftVersion &version, const QString &nativesDir) { QStringList classpath; for (const MinecraftLibrary &library : version.libraries) { if (library.native) continue; // натив-библиотеки только распаковываются classpath << nativeToken(libraryPath(options.gameDir, library)); } classpath << nativeToken(version.clientJarPath); const QString assetsRoot = options.gameDir + QStringLiteral("/assets"); const bool legacyAssets = version.assetsKind == QLatin1String("legacy") || version.assetsKind == QLatin1String("pre-1.6"); QHash variables; 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_xuid")] = options.xuid.isEmpty() ? QStringLiteral("0") : options.xuid; 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; variables[QStringLiteral("version_type")] = version.type; 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("natives_directory")] = nativeToken(nativesDir); 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); variables[QStringLiteral("launcher_name")] = options.launcherName; variables[QStringLiteral("launcher_version")] = options.launcherVersion; variables[QStringLiteral("resolution_width")] = QString::number(options.windowWidth); variables[QStringLiteral("resolution_height")] = QString::number(options.windowHeight); QStringList arguments; // Свои jvm-аргументы идут первыми — как в строке запуска из примера. if (!options.authlibInjectorPath.isEmpty()) { arguments << QStringLiteral("-javaagent:%1=%2") .arg(nativeToken(options.authlibInjectorPath), options.authlibInjectorApi) << QStringLiteral("-Dauthlibinjector.side=client"); } if (options.minMemoryMb > 0) arguments << QStringLiteral("-Xms%1M").arg(options.minMemoryMb); if (options.maxMemoryMb > 0) arguments << QStringLiteral("-Xmx%1M").arg(options.maxMemoryMb); arguments += options.extraJvmArgs; // Forge 1.17+ собирает слой модулей из classpath, пропуская всё, чьё имя // начинается с записи в -DignoreList. Ванильный клиентский jar записан там // как ${version_name}.jar — с расчётом на лаунчер Mojang, который кладёт в // classpath jar самого профиля. Своего jar у профиля Forge нет, мы кладём // родительский, и без его имени в списке он становится вторым модулем с // классами Minecraft: «Modules minecraft and _1._20._1 export package ...». const QString clientJarName = QFileInfo(version.clientJarPath).fileName(); for (const QString &argument : version.jvmArgs) { QString resolved = substitute(argument, variables); if (resolved.contains(QStringLiteral("${"))) continue; if (!clientJarName.isEmpty() && resolved.startsWith(QLatin1String("-DignoreList=")) && !ignoreListCovers(resolved, clientJarName)) resolved += u',' + clientJarName; arguments << resolved; } if (!version.loggingArgument.isEmpty() && QFile::exists(version.loggingConfigPath)) { QHash loggingVariable; loggingVariable[QStringLiteral("path")] = nativeToken(version.loggingConfigPath); arguments << substitute(version.loggingArgument, loggingVariable); } arguments << version.mainClass; for (const QString &argument : version.gameArgs) { const QString resolved = substitute(argument, variables); if (!resolved.contains(QStringLiteral("${"))) arguments << resolved; } // Разрешение окна: у старых версий фич-флага нет, аргументы всё равно понимаются. 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); } if (options.fullscreen && !arguments.contains(QStringLiteral("--fullscreen"))) arguments << QStringLiteral("--fullscreen"); if (!options.serverAddress.isEmpty()) { if (version.supportsQuickPlay) { 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 port = colon > 0 ? options.serverAddress.mid(colon + 1) : QStringLiteral("25565"); arguments << QStringLiteral("--server") << host << QStringLiteral("--port") << port; } } return arguments; } bool GameLauncher::extractNatives(const LaunchOptions &options, const MinecraftVersion &version, const QString &nativesDir, QString *error) { if (!QDir().mkpath(nativesDir)) { if (error) *error = tr("Не удалось создать папку %1").arg(QDir::toNativeSeparators(nativesDir)); return false; } for (const MinecraftLibrary &library : version.libraries) { if (!library.native) continue; const QString jar = libraryPath(options.gameDir, library); QZipReader reader(jar); if (!reader.isReadable()) { if (error) *error = tr("Не удалось прочитать натив-библиотеку %1") .arg(QDir::toNativeSeparators(jar)); return false; } const QList entries = reader.fileInfoList(); for (const QZipReader::FileInfo &entry : entries) { if (!entry.isFile) continue; if (entry.filePath.startsWith(QStringLiteral("META-INF/"))) continue; bool excluded = false; for (const QString &prefix : library.extractExclude) { if (entry.filePath.startsWith(prefix)) { excluded = true; break; } } if (excluded) continue; // java.library.path ищет библиотеки только в корне папки, поэтому // складываем их плоско, без структуры каталогов внутри jar. const QString fileName = QFileInfo(entry.filePath).fileName(); if (!isNativeLibraryFile(fileName)) continue; const QString target = nativesDir + u'/' + fileName; const QByteArray data = reader.fileData(entry.filePath); if (QFileInfo(target).size() == data.size() && QFile::exists(target)) continue; // уже распаковано этим же лаунчером QFile file(target); if (!file.open(QIODevice::WriteOnly) || file.write(data) != data.size()) { if (error) *error = tr("Не удалось распаковать %1 в %2") .arg(fileName, QDir::toNativeSeparators(nativesDir)); return false; } } } return true; } bool GameLauncher::launch(const LaunchOptions &options, const MinecraftVersion &version, QString *error) { if (isRunning()) { if (error) *error = tr("Игра уже запущена"); return false; } emit progress(tr("Проверка файлов игры…")); const QStringList missing = missingFiles(options, version); if (!missing.isEmpty()) { if (error) *error = tr("В .minecraft не хватает файлов (%1):\n%2") .arg(missing.size()) .arg(missing.join(u'\n')); return false; } emit progress(tr("Распаковка нативных библиотек…")); const QString nativesDir = options.gameDir + QStringLiteral("/versions/") + version.id + QStringLiteral("/natives"); if (!extractNatives(options, version, nativesDir, error)) return false; emit progress(tr("Поиск Java %1…").arg(version.javaMajor)); const QString java = JavaLocator::select(options.gameDir, version.javaMajor, options.javaPath, error); if (java.isEmpty()) return false; const QStringList arguments = buildArguments(options, version, nativesDir); const QString executable = JavaLocator::windowlessVariant(java); m_process = new QProcess(this); m_process->setProgram(executable); m_process->setArguments(arguments); m_process->setWorkingDirectory(options.gameDir); m_process->setProcessChannelMode(QProcess::MergedChannels); connect(m_process, &QProcess::readyReadStandardOutput, this, [this]() { const QString chunk = QString::fromLocal8Bit(m_process->readAllStandardOutput()); const QStringList lines = chunk.split(u'\n', Qt::SkipEmptyParts); for (const QString &line : lines) emit output(line.trimmed()); }); connect(m_process, &QProcess::finished, this, [this](int exitCode, QProcess::ExitStatus status) { emit gameFinished(exitCode, status == QProcess::CrashExit); m_process->deleteLater(); m_process = nullptr; }); // Токен в лог не попадает — маскируем его в отображаемой строке запуска. QStringList shown = arguments; const int tokenIndex = shown.indexOf(QStringLiteral("--accessToken")); if (tokenIndex >= 0 && tokenIndex + 1 < shown.size()) shown[tokenIndex + 1] = QStringLiteral("<скрыт>"); m_process->start(); if (!m_process->waitForStarted(15000)) { if (error) *error = tr("Не удалось запустить %1: %2") .arg(QDir::toNativeSeparators(executable), m_process->errorString()); m_process->deleteLater(); m_process = nullptr; return false; } emit gameStarted(QDir::toNativeSeparators(executable) + QStringLiteral(" ") + shown.join(QStringLiteral(" "))); return true; } void GameLauncher::terminate() { if (isRunning()) m_process->kill(); }