diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 8212af2339..18b1bca66a 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -266,23 +266,23 @@ jobs: - name: Configure CMake (macOS) if: runner.os == 'macOS' && matrix.qt_ver == 6 run: | - cmake -S . -B ${{ env.BUILD_DIR }} -DCMAKE_INSTALL_PREFIX=${{ env.INSTALL_DIR }} -DCMAKE_BUILD_TYPE=${{ inputs.build_type }} -DENABLE_LTO=ON -DLauncher_BUILD_PLATFORM=official -DCMAKE_C_COMPILER_LAUNCHER=${{ env.CCACHE_VAR }} -DCMAKE_CXX_COMPILER_LAUNCHER=${{ env.CCACHE_VAR }} -DLauncher_QT_VERSION_MAJOR=${{ matrix.qt_ver }} -DCMAKE_OSX_ARCHITECTURES="x86_64;arm64" -G Ninja + cmake -S . -B ${{ env.BUILD_DIR }} -DCMAKE_INSTALL_PREFIX=${{ env.INSTALL_DIR }} -DCMAKE_BUILD_TYPE=${{ inputs.build_type }} -DENABLE_LTO=ON -DENABLE_JAVA_DOWNLOADER=ON -DLauncher_BUILD_PLATFORM=official -DCMAKE_C_COMPILER_LAUNCHER=${{ env.CCACHE_VAR }} -DCMAKE_CXX_COMPILER_LAUNCHER=${{ env.CCACHE_VAR }} -DLauncher_QT_VERSION_MAJOR=${{ matrix.qt_ver }} -DCMAKE_OSX_ARCHITECTURES="x86_64;arm64" -G Ninja - name: Configure CMake (macOS-Legacy) if: runner.os == 'macOS' && matrix.qt_ver == 5 run: | - cmake -S . -B ${{ env.BUILD_DIR }} -DCMAKE_INSTALL_PREFIX=${{ env.INSTALL_DIR }} -DCMAKE_BUILD_TYPE=${{ inputs.build_type }} -DENABLE_LTO=ON -DLauncher_BUILD_PLATFORM=official -DCMAKE_C_COMPILER_LAUNCHER=${{ env.CCACHE_VAR }} -DCMAKE_CXX_COMPILER_LAUNCHER=${{ env.CCACHE_VAR }} -DLauncher_QT_VERSION_MAJOR=${{ matrix.qt_ver }} -DMACOSX_SPARKLE_UPDATE_PUBLIC_KEY="" -DMACOSX_SPARKLE_UPDATE_FEED_URL="" -G Ninja + cmake -S . -B ${{ env.BUILD_DIR }} -DCMAKE_INSTALL_PREFIX=${{ env.INSTALL_DIR }} -DCMAKE_BUILD_TYPE=${{ inputs.build_type }} -DENABLE_LTO=ON -DENABLE_JAVA_DOWNLOADER=ON -DLauncher_BUILD_PLATFORM=official -DCMAKE_C_COMPILER_LAUNCHER=${{ env.CCACHE_VAR }} -DCMAKE_CXX_COMPILER_LAUNCHER=${{ env.CCACHE_VAR }} -DLauncher_QT_VERSION_MAJOR=${{ matrix.qt_ver }} -DMACOSX_SPARKLE_UPDATE_PUBLIC_KEY="" -DMACOSX_SPARKLE_UPDATE_FEED_URL="" -G Ninja - name: Configure CMake (Windows MinGW-w64) if: runner.os == 'Windows' && matrix.msystem != '' shell: msys2 {0} run: | - cmake -S . -B ${{ env.BUILD_DIR }} -DCMAKE_INSTALL_PREFIX=${{ env.INSTALL_DIR }} -DCMAKE_BUILD_TYPE=${{ inputs.build_type }} -DENABLE_LTO=ON -DLauncher_BUILD_PLATFORM=official -DCMAKE_C_COMPILER_LAUNCHER=${{ env.CCACHE_VAR }} -DCMAKE_CXX_COMPILER_LAUNCHER=${{ env.CCACHE_VAR }} -DLauncher_QT_VERSION_MAJOR=6 -DCMAKE_OBJDUMP=/mingw64/bin/objdump.exe -DLauncher_BUILD_ARTIFACT=${{ matrix.name }}-Qt${{ matrix.qt_ver }} -G Ninja + cmake -S . -B ${{ env.BUILD_DIR }} -DCMAKE_INSTALL_PREFIX=${{ env.INSTALL_DIR }} -DCMAKE_BUILD_TYPE=${{ inputs.build_type }} -DENABLE_LTO=ON -DENABLE_JAVA_DOWNLOADER=ON -DLauncher_BUILD_PLATFORM=official -DCMAKE_C_COMPILER_LAUNCHER=${{ env.CCACHE_VAR }} -DCMAKE_CXX_COMPILER_LAUNCHER=${{ env.CCACHE_VAR }} -DLauncher_QT_VERSION_MAJOR=6 -DCMAKE_OBJDUMP=/mingw64/bin/objdump.exe -DLauncher_BUILD_ARTIFACT=${{ matrix.name }}-Qt${{ matrix.qt_ver }} -G Ninja - name: Configure CMake (Windows MSVC) if: runner.os == 'Windows' && matrix.msystem == '' run: | - cmake -S . -B ${{ env.BUILD_DIR }} -DCMAKE_INSTALL_PREFIX=${{ env.INSTALL_DIR }} -DCMAKE_BUILD_TYPE=${{ inputs.build_type }} -DENABLE_LTO=ON -DLauncher_BUILD_PLATFORM=official -DLauncher_QT_VERSION_MAJOR=${{ matrix.qt_ver }} -DCMAKE_MSVC_RUNTIME_LIBRARY="MultiThreadedDLL" -A${{ matrix.architecture}} -DLauncher_FORCE_BUNDLED_LIBS=ON -DLauncher_BUILD_ARTIFACT=${{ matrix.name }}-Qt${{ matrix.qt_ver }} + cmake -S . -B ${{ env.BUILD_DIR }} -DCMAKE_INSTALL_PREFIX=${{ env.INSTALL_DIR }} -DCMAKE_BUILD_TYPE=${{ inputs.build_type }} -DENABLE_LTO=ON -DENABLE_JAVA_DOWNLOADER=ON -DLauncher_BUILD_PLATFORM=official -DLauncher_QT_VERSION_MAJOR=${{ matrix.qt_ver }} -DCMAKE_MSVC_RUNTIME_LIBRARY="MultiThreadedDLL" -A${{ matrix.architecture}} -DLauncher_FORCE_BUNDLED_LIBS=ON -DLauncher_BUILD_ARTIFACT=${{ matrix.name }}-Qt${{ matrix.qt_ver }} # https://github.com/ccache/ccache/wiki/MS-Visual-Studio (I coudn't figure out the compiler prefix) if ("${{ env.CCACHE_VAR }}") { @@ -297,7 +297,7 @@ jobs: - name: Configure CMake (Linux) if: runner.os == 'Linux' run: | - cmake -S . -B ${{ env.BUILD_DIR }} -DCMAKE_INSTALL_PREFIX=/usr -DCMAKE_BUILD_TYPE=${{ inputs.build_type }} -DENABLE_LTO=ON -DLauncher_BUILD_PLATFORM=official -DCMAKE_C_COMPILER_LAUNCHER=${{ env.CCACHE_VAR }} -DCMAKE_CXX_COMPILER_LAUNCHER=${{ env.CCACHE_VAR }} -DLauncher_QT_VERSION_MAJOR=${{ matrix.qt_ver }} -DLauncher_BUILD_ARTIFACT=Linux-Qt${{ matrix.qt_ver }} -G Ninja + cmake -S . -B ${{ env.BUILD_DIR }} -DCMAKE_INSTALL_PREFIX=/usr -DCMAKE_BUILD_TYPE=${{ inputs.build_type }} -DENABLE_LTO=ON -DENABLE_JAVA_DOWNLOADER=ON -DLauncher_BUILD_PLATFORM=official -DCMAKE_C_COMPILER_LAUNCHER=${{ env.CCACHE_VAR }} -DCMAKE_CXX_COMPILER_LAUNCHER=${{ env.CCACHE_VAR }} -DLauncher_QT_VERSION_MAJOR=${{ matrix.qt_ver }} -DLauncher_BUILD_ARTIFACT=Linux-Qt${{ matrix.qt_ver }} -G Ninja ## # BUILD diff --git a/CMakeLists.txt b/CMakeLists.txt index 5c39874068..7445e574ab 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -219,6 +219,19 @@ set(Launcher_SUBREDDIT_URL "https://prismlauncher.org/reddit" CACHE STRING "URL set(Launcher_FORCE_BUNDLED_LIBS OFF CACHE BOOL "Prevent using system libraries, if they are available as submodules") set(Launcher_QT_VERSION_MAJOR "6" CACHE STRING "Major Qt version to build against") +# Java downloader +set(ENABLE_JAVA_DOWNLOADER_DEFAULT ON) + +# Although we recommend enabling this, we cannot guarantee binary compatibility on +# differing Linux/BSD/etc distributions. Downstream packagers should be explicitly opt-ing into this +# feature if they know it will work with their distribution. +if(UNIX AND NOT APPLE) + set(ENABLE_JAVA_DOWNLOADER_DEFAULT OFF) +endif() + +# Java downloader +option(ENABLE_JAVA_DOWNLOADER "Build the java downloader feature" ${ENABLE_JAVA_DOWNLOADER_DEFAULT}) + # Native libraries if(UNIX AND APPLE) set(Launcher_GLFW_LIBRARY_NAME "libglfw.dylib" CACHE STRING "Name of native glfw library") diff --git a/buildconfig/BuildConfig.cpp.in b/buildconfig/BuildConfig.cpp.in index b40cacb0f3..a2b5c21871 100644 --- a/buildconfig/BuildConfig.cpp.in +++ b/buildconfig/BuildConfig.cpp.in @@ -81,6 +81,9 @@ Config::Config() UPDATER_ENABLED = true; } + #cmakedefine01 ENABLE_JAVA_DOWNLOADER + JAVA_DOWNLOADER_ENABLED = ENABLE_JAVA_DOWNLOADER; + GIT_COMMIT = "@Launcher_GIT_COMMIT@"; GIT_TAG = "@Launcher_GIT_TAG@"; GIT_REFSPEC = "@Launcher_GIT_REFSPEC@"; diff --git a/buildconfig/BuildConfig.h b/buildconfig/BuildConfig.h index bda80ac72e..bb633f297b 100644 --- a/buildconfig/BuildConfig.h +++ b/buildconfig/BuildConfig.h @@ -67,6 +67,7 @@ class Config { QString VERSION_CHANNEL; bool UPDATER_ENABLED = false; + bool JAVA_DOWNLOADER_ENABLED = false; /// A short string identifying this build's platform or distribution. QString BUILD_PLATFORM; diff --git a/launcher/Application.cpp b/launcher/Application.cpp index 7ae66e13ae..40c85fdd72 100644 --- a/launcher/Application.cpp +++ b/launcher/Application.cpp @@ -44,6 +44,7 @@ #include "BuildConfig.h" #include "DataMigrationTask.h" +#include "java/JavaInstallList.h" #include "net/PasteUpload.h" #include "pathmatcher/MultiMatcher.h" #include "pathmatcher/SimplePrefixMatcher.h" @@ -126,6 +127,7 @@ #include #include +#include "SysInfo.h" #ifdef Q_OS_LINUX #include @@ -595,6 +597,7 @@ Application::Application(int& argc, char** argv) : QApplication(argc, argv) m_settings->registerSetting("DownloadsDir", QStandardPaths::writableLocation(QStandardPaths::DownloadLocation)); m_settings->registerSetting("DownloadsDirWatchRecursive", false); m_settings->registerSetting("SkinsDir", "skins"); + m_settings->registerSetting("JavaDir", "java"); // Editors m_settings->registerSetting("JsonEditor", QString()); @@ -623,7 +626,7 @@ Application::Application(int& argc, char** argv) : QApplication(argc, argv) // Memory m_settings->registerSetting({ "MinMemAlloc", "MinMemoryAlloc" }, 512); - m_settings->registerSetting({ "MaxMemAlloc", "MaxMemoryAlloc" }, suitableMaxMem()); + m_settings->registerSetting({ "MaxMemAlloc", "MaxMemoryAlloc" }, SysInfo::suitableMaxMem()); m_settings->registerSetting("PermGen", 128); // Java Settings @@ -637,6 +640,8 @@ Application::Application(int& argc, char** argv) : QApplication(argc, argv) m_settings->registerSetting("JvmArgs", ""); m_settings->registerSetting("IgnoreJavaCompatibility", false); m_settings->registerSetting("IgnoreJavaWizard", false); + m_settings->registerSetting("AutomaticJavaSwitch", false); + m_settings->registerSetting("AutomaticJavaDownload", false); // Legacy settings m_settings->registerSetting("OnlineFixes", false); @@ -865,6 +870,7 @@ Application::Application(int& argc, char** argv) : QApplication(argc, argv) m_metacache->addBase("ModrinthModpacks", QDir("cache/ModrinthModpacks").absolutePath()); m_metacache->addBase("translations", QDir("translations").absolutePath()); m_metacache->addBase("meta", QDir("meta").absolutePath()); + m_metacache->addBase("java", QDir("cache/java").absolutePath()); m_metacache->Load(); qDebug() << "<> Cache initialized."; } @@ -1731,20 +1737,6 @@ QString Application::getUserAgentUncached() return BuildConfig.USER_AGENT_UNCACHED; } -int Application::suitableMaxMem() -{ - float totalRAM = (float)Sys::getSystemRam() / (float)Sys::mebibyte; - int maxMemoryAlloc; - - // If totalRAM < 6GB, use (totalRAM / 1.5), else 4GB - if (totalRAM < (4096 * 1.5)) - maxMemoryAlloc = (int)(totalRAM / 1.5); - else - maxMemoryAlloc = 4096; - - return maxMemoryAlloc; -} - bool Application::handleDataMigration(const QString& currentData, const QString& oldData, const QString& name, @@ -1851,3 +1843,7 @@ QUrl Application::normalizeImportUrl(QString const& url) return QUrl::fromUserInput(url); } } +const QString Application::javaPath() +{ + return m_settings->get("JavaDir").toString(); +} diff --git a/launcher/Application.h b/launcher/Application.h index 8303c74753..aa3a8e510b 100644 --- a/launcher/Application.h +++ b/launcher/Application.h @@ -161,6 +161,9 @@ class Application : public QApplication { /// the data path the application is using const QString& dataRoot() { return m_dataPath; } + /// the java installed path the application is using + const QString javaPath(); + bool isPortable() { return m_portable; } const Capabilities capabilities() { return m_capabilities; } @@ -179,8 +182,6 @@ class Application : public QApplication { void ShowGlobalSettings(class QWidget* parent, QString open_page = QString()); - int suitableMaxMem(); - bool updaterEnabled(); QString updaterBinaryName(); diff --git a/launcher/BaseVersionList.cpp b/launcher/BaseVersionList.cpp index e11560d5ec..22077c9623 100644 --- a/launcher/BaseVersionList.cpp +++ b/launcher/BaseVersionList.cpp @@ -78,6 +78,14 @@ QVariant BaseVersionList::data(const QModelIndex& index, int role) const case TypeRole: return version->typeString(); + case JavaMajorRole: { + auto major = version->name(); + if (major.startsWith("java")) { + major = "Java " + major.mid(4); + } + return major; + } + default: return QVariant(); } @@ -110,6 +118,8 @@ QHash BaseVersionList::roleNames() const roles.insert(TypeRole, "type"); roles.insert(BranchRole, "branch"); roles.insert(PathRole, "path"); - roles.insert(ArchitectureRole, "architecture"); + roles.insert(JavaNameRole, "javaName"); + roles.insert(CPUArchitectureRole, "architecture"); + roles.insert(JavaMajorRole, "javaMajor"); return roles; } diff --git a/launcher/BaseVersionList.h b/launcher/BaseVersionList.h index 231887c4ea..673d135628 100644 --- a/launcher/BaseVersionList.h +++ b/launcher/BaseVersionList.h @@ -48,7 +48,9 @@ class BaseVersionList : public QAbstractListModel { TypeRole, BranchRole, PathRole, - ArchitectureRole, + JavaNameRole, + JavaMajorRole, + CPUArchitectureRole, SortRole }; using RoleList = QList; diff --git a/launcher/CMakeLists.txt b/launcher/CMakeLists.txt index 366719dec3..4758bf77bd 100644 --- a/launcher/CMakeLists.txt +++ b/launcher/CMakeLists.txt @@ -24,6 +24,8 @@ set(CORE_SOURCES NullInstance.h MMCZip.h MMCZip.cpp + Untar.h + Untar.cpp StringUtils.h StringUtils.cpp QVariantUtils.h @@ -273,6 +275,8 @@ set(MINECRAFT_SOURCES minecraft/launch/ScanModFolders.h minecraft/launch/VerifyJavaInstall.cpp minecraft/launch/VerifyJavaInstall.h + minecraft/launch/AutoInstallJava.cpp + minecraft/launch/AutoInstallJava.h minecraft/GradleSpecifier.h minecraft/MinecraftInstance.cpp @@ -418,8 +422,6 @@ set(SETTINGS_SOURCES set(JAVA_SOURCES java/JavaChecker.h java/JavaChecker.cpp - java/JavaCheckerJob.h - java/JavaCheckerJob.cpp java/JavaInstall.h java/JavaInstall.cpp java/JavaInstallList.h @@ -428,6 +430,18 @@ set(JAVA_SOURCES java/JavaUtils.cpp java/JavaVersion.h java/JavaVersion.cpp + + java/JavaMetadata.h + java/JavaMetadata.cpp + java/download/ArchiveDownloadTask.cpp + java/download/ArchiveDownloadTask.h + java/download/ManifestDownloadTask.cpp + java/download/ManifestDownloadTask.h + + ui/java/InstallJavaDialog.h + ui/java/InstallJavaDialog.cpp + ui/java/VersionList.h + ui/java/VersionList.cpp ) set(TRANSLATIONS_SOURCES @@ -747,6 +761,8 @@ SET(LAUNCHER_SOURCES DataMigrationTask.cpp ApplicationMessage.h ApplicationMessage.cpp + SysInfo.h + SysInfo.cpp # GUI - general utilities DesktopServices.h diff --git a/launcher/FileSystem.cpp b/launcher/FileSystem.cpp index 9e399bbfb3..c62296149e 100644 --- a/launcher/FileSystem.cpp +++ b/launcher/FileSystem.cpp @@ -276,6 +276,9 @@ bool ensureFolderPathExists(const QFileInfo folderPath) { QDir dir; QString ensuredPath = folderPath.filePath(); + if (folderPath.exists()) + return true; + bool success = dir.mkpath(ensuredPath); return success; } diff --git a/launcher/JavaCommon.cpp b/launcher/JavaCommon.cpp index e16ac92556..3cbf9f9d54 100644 --- a/launcher/JavaCommon.cpp +++ b/launcher/JavaCommon.cpp @@ -63,7 +63,7 @@ bool JavaCommon::checkJVMArgs(QString jvmargs, QWidget* parent) return true; } -void JavaCommon::javaWasOk(QWidget* parent, const JavaCheckResult& result) +void JavaCommon::javaWasOk(QWidget* parent, const JavaChecker::Result& result) { QString text; text += QObject::tr( @@ -79,7 +79,7 @@ void JavaCommon::javaWasOk(QWidget* parent, const JavaCheckResult& result) CustomMessageBox::selectable(parent, QObject::tr("Java test success"), text, QMessageBox::Information)->show(); } -void JavaCommon::javaArgsWereBad(QWidget* parent, const JavaCheckResult& result) +void JavaCommon::javaArgsWereBad(QWidget* parent, const JavaChecker::Result& result) { auto htmlError = result.errorLog; QString text; @@ -89,7 +89,7 @@ void JavaCommon::javaArgsWereBad(QWidget* parent, const JavaCheckResult& result) CustomMessageBox::selectable(parent, QObject::tr("Java test failure"), text, QMessageBox::Warning)->show(); } -void JavaCommon::javaBinaryWasBad(QWidget* parent, const JavaCheckResult& result) +void JavaCommon::javaBinaryWasBad(QWidget* parent, const JavaChecker::Result& result) { QString text; text += QObject::tr( @@ -116,34 +116,26 @@ void JavaCommon::TestCheck::run() emit finished(); return; } - checker.reset(new JavaChecker()); + checker.reset(new JavaChecker(m_path, "", 0, 0, 0, 0, this)); connect(checker.get(), &JavaChecker::checkFinished, this, &JavaCommon::TestCheck::checkFinished); - checker->m_path = m_path; - checker->performCheck(); + checker->start(); } -void JavaCommon::TestCheck::checkFinished(JavaCheckResult result) +void JavaCommon::TestCheck::checkFinished(const JavaChecker::Result& result) { - if (result.validity != JavaCheckResult::Validity::Valid) { + if (result.validity != JavaChecker::Result::Validity::Valid) { javaBinaryWasBad(m_parent, result); emit finished(); return; } - checker.reset(new JavaChecker()); + checker.reset(new JavaChecker(m_path, m_args, m_maxMem, m_maxMem, result.javaVersion.requiresPermGen() ? m_permGen : 0, 0, this)); connect(checker.get(), &JavaChecker::checkFinished, this, &JavaCommon::TestCheck::checkFinishedWithArgs); - checker->m_path = m_path; - checker->m_args = m_args; - checker->m_minMem = m_minMem; - checker->m_maxMem = m_maxMem; - if (result.javaVersion.requiresPermGen()) { - checker->m_permGen = m_permGen; - } - checker->performCheck(); + checker->start(); } -void JavaCommon::TestCheck::checkFinishedWithArgs(JavaCheckResult result) +void JavaCommon::TestCheck::checkFinishedWithArgs(const JavaChecker::Result& result) { - if (result.validity == JavaCheckResult::Validity::Valid) { + if (result.validity == JavaChecker::Result::Validity::Valid) { javaWasOk(m_parent, result); emit finished(); return; diff --git a/launcher/JavaCommon.h b/launcher/JavaCommon.h index cf3b75c9c2..a21b5a4949 100644 --- a/launcher/JavaCommon.h +++ b/launcher/JavaCommon.h @@ -10,11 +10,11 @@ namespace JavaCommon { bool checkJVMArgs(QString args, QWidget* parent); // Show a dialog saying that the Java binary was usable -void javaWasOk(QWidget* parent, const JavaCheckResult& result); +void javaWasOk(QWidget* parent, const JavaChecker::Result& result); // Show a dialog saying that the Java binary was not usable because of bad options -void javaArgsWereBad(QWidget* parent, const JavaCheckResult& result); +void javaArgsWereBad(QWidget* parent, const JavaChecker::Result& result); // Show a dialog saying that the Java binary was not usable -void javaBinaryWasBad(QWidget* parent, const JavaCheckResult& result); +void javaBinaryWasBad(QWidget* parent, const JavaChecker::Result& result); // Show a dialog if we couldn't find Java Checker void javaCheckNotFound(QWidget* parent); @@ -32,11 +32,11 @@ class TestCheck : public QObject { void finished(); private slots: - void checkFinished(JavaCheckResult result); - void checkFinishedWithArgs(JavaCheckResult result); + void checkFinished(const JavaChecker::Result& result); + void checkFinishedWithArgs(const JavaChecker::Result& result); private: - std::shared_ptr checker; + JavaChecker::Ptr checker; QWidget* m_parent = nullptr; QString m_path; QString m_args; diff --git a/launcher/MMCZip.cpp b/launcher/MMCZip.cpp index 00658b42b8..91c0bf2cc1 100644 --- a/launcher/MMCZip.cpp +++ b/launcher/MMCZip.cpp @@ -2,7 +2,7 @@ /* * Prism Launcher - Minecraft Launcher * Copyright (C) 2022 Sefa Eyeoglu - * Copyright (c) 2023 Trial97 + * Copyright (c) 2023-2024 Trial97 * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by @@ -525,6 +525,10 @@ bool ExportToZipTask::abort() void ExtractZipTask::executeTask() { + if (!m_input->isOpen() && !m_input->open(QuaZip::mdUnzip)) { + emitFailed(tr("Unable to open supplied zip file.")); + return; + } m_zip_future = QtConcurrent::run(QThreadPool::globalInstance(), [this]() { return extractZip(); }); connect(&m_zip_watcher, &QFutureWatcher::finished, this, &ExtractZipTask::finish); m_zip_watcher.setFuture(m_zip_future); diff --git a/launcher/MMCZip.h b/launcher/MMCZip.h index 35baa6ee3d..1635f8b328 100644 --- a/launcher/MMCZip.h +++ b/launcher/MMCZip.h @@ -2,7 +2,7 @@ /* * Prism Launcher - Minecraft Launcher * Copyright (C) 2022 Sefa Eyeoglu - * Copyright (c) 2023 Trial97 + * Copyright (c) 2023-2024 Trial97 * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by @@ -208,6 +208,9 @@ class ExportToZipTask : public Task { class ExtractZipTask : public Task { public: + ExtractZipTask(QString input, QDir outputDir, QString subdirectory = "") + : ExtractZipTask(std::make_shared(input), outputDir, subdirectory) + {} ExtractZipTask(std::shared_ptr input, QDir outputDir, QString subdirectory = "") : m_input(input), m_output_dir(outputDir), m_subdirectory(subdirectory) {} diff --git a/launcher/SysInfo.cpp b/launcher/SysInfo.cpp new file mode 100644 index 0000000000..0dfa74de7f --- /dev/null +++ b/launcher/SysInfo.cpp @@ -0,0 +1,99 @@ +#include +#include +#include "sys.h" +#ifdef Q_OS_MACOS +#include +#endif +#include +#include +#include +#include + +#ifdef Q_OS_MACOS +bool rosettaDetect() +{ + int ret = 0; + size_t size = sizeof(ret); + if (sysctlbyname("sysctl.proc_translated", &ret, &size, NULL, 0) == -1) { + return false; + } + return ret == 1; +} +#endif + +namespace SysInfo { +QString currentSystem() +{ +#if defined(Q_OS_LINUX) + return "linux"; +#elif defined(Q_OS_MACOS) + return "osx"; +#elif defined(Q_OS_WINDOWS) + return "windows"; +#elif defined(Q_OS_FREEBSD) + return "freebsd"; +#elif defined(Q_OS_OPENBSD) + return "openbsd"; +#else + return "unknown"; +#endif +} + +QString useQTForArch() +{ +#if defined(Q_OS_MACOS) && !defined(Q_PROCESSOR_ARM) + if (rosettaDetect()) { + return "arm64"; + } else { + return "x86_64"; + } +#endif + return QSysInfo::currentCpuArchitecture(); +} + +int suitableMaxMem() +{ + float totalRAM = (float)Sys::getSystemRam() / (float)Sys::mebibyte; + int maxMemoryAlloc; + + // If totalRAM < 6GB, use (totalRAM / 1.5), else 4GB + if (totalRAM < (4096 * 1.5)) + maxMemoryAlloc = (int)(totalRAM / 1.5); + else + maxMemoryAlloc = 4096; + + return maxMemoryAlloc; +} + +QString getSupportedJavaArchitecture() +{ + auto sys = currentSystem(); + auto arch = useQTForArch(); + if (sys == "windows") { + if (arch == "x86_64") + return "windows-x64"; + if (arch == "i386") + return "windows-x86"; + // Unknown, maybe arm, appending arch + return "windows-" + arch; + } + if (sys == "osx") { + if (arch == "arm64") + return "mac-os-arm64"; + if (arch.contains("64")) + return "mac-os-64"; + if (arch.contains("86")) + return "mac-os-86"; + // Unknown, maybe something new, appending arch + return "mac-os-" + arch; + } else if (sys == "linux") { + if (arch == "x86_64") + return "linux-x64"; + if (arch == "i386") + return "linux-x86"; + // will work for arm32 arm(64) + return "linux-" + arch; + } + return {}; +} +} // namespace SysInfo diff --git a/launcher/SysInfo.h b/launcher/SysInfo.h new file mode 100644 index 0000000000..f3688d60d6 --- /dev/null +++ b/launcher/SysInfo.h @@ -0,0 +1,8 @@ +#include + +namespace SysInfo { +QString currentSystem(); +QString useQTForArch(); +QString getSupportedJavaArchitecture(); +int suitableMaxMem(); +} // namespace SysInfo diff --git a/launcher/Untar.cpp b/launcher/Untar.cpp new file mode 100644 index 0000000000..f1963e7aaf --- /dev/null +++ b/launcher/Untar.cpp @@ -0,0 +1,260 @@ +// SPDX-License-Identifier: GPL-3.0-only +/* + * Prism Launcher - Minecraft Launcher + * Copyright (c) 2023-2024 Trial97 + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, version 3. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + * + * This file incorporates work covered by the following copyright and + * permission notice: + * + * Copyright 2013-2021 MultiMC Contributors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +#include "Untar.h" +#include +#include +#include +#include +#include +#include "FileSystem.h" + +// adaptation of the: +// - https://github.com/madler/zlib/blob/develop/contrib/untgz/untgz.c +// - https://en.wikipedia.org/wiki/Tar_(computing) +// - https://github.com/euroelessar/cutereader/blob/master/karchive/src/ktar.cpp + +#define BLOCKSIZE 512 +#define SHORTNAMESIZE 100 + +enum class TypeFlag : char { + Regular = '0', // regular file + ARegular = 0, // regular file + Link = '1', // link + Symlink = '2', // reserved + Character = '3', // character special + Block = '4', // block special + Directory = '5', // directory + FIFO = '6', // FIFO special + Contiguous = '7', // reserved + // Posix stuff + GlobalPosixHeader = 'g', + ExtendedPosixHeader = 'x', + // 'A'– 'Z' Vendor specific extensions(POSIX .1 - 1988) + // GNU + GNULongLink = 'K', /* long link name */ + GNULongName = 'L', /* long file name */ +}; + +// struct Header { /* byte offset */ +// char name[100]; /* 0 */ +// char mode[8]; /* 100 */ +// char uid[8]; /* 108 */ +// char gid[8]; /* 116 */ +// char size[12]; /* 124 */ +// char mtime[12]; /* 136 */ +// char chksum[8]; /* 148 */ +// TypeFlag typeflag; /* 156 */ +// char linkname[100]; /* 157 */ +// char magic[6]; /* 257 */ +// char version[2]; /* 263 */ +// char uname[32]; /* 265 */ +// char gname[32]; /* 297 */ +// char devmajor[8]; /* 329 */ +// char devminor[8]; /* 337 */ +// char prefix[155]; /* 345 */ +// /* 500 */ +// }; + +bool readLonglink(QIODevice* in, qint64 size, QByteArray& longlink) +{ + qint64 n = 0; + size--; // ignore trailing null + if (size < 0) { + qCritical() << "The filename size is negative"; + return false; + } + longlink.resize(size + (BLOCKSIZE - size % BLOCKSIZE)); // make the size divisible by BLOCKSIZE + for (qint64 offset = 0; offset < longlink.size(); offset += BLOCKSIZE) { + n = in->read(longlink.data() + offset, BLOCKSIZE); + if (n != BLOCKSIZE) { + qCritical() << "The expected blocksize was not respected for the name"; + return false; + } + } + longlink.truncate(qstrlen(longlink.constData())); + return true; +} + +int getOctal(char* buffer, int maxlenght, bool* ok) +{ + return QByteArray(buffer, qstrnlen(buffer, maxlenght)).toInt(ok, 8); +} + +QString decodeName(char* name) +{ + return QFile::decodeName(QByteArray(name, qstrnlen(name, 100))); +} +bool Tar::extract(QIODevice* in, QString dst) +{ + char buffer[BLOCKSIZE]; + QString name, symlink, firstFolderName; + bool doNotReset = false, ok; + while (true) { + auto n = in->read(buffer, BLOCKSIZE); + if (n != BLOCKSIZE) { // allways expect complete blocks + qCritical() << "The expected blocksize was not respected"; + return false; + } + if (buffer[0] == 0) { // end of archive + return true; + } + int mode = getOctal(buffer + 100, 8, &ok) | QFile::ReadUser | QFile::WriteUser; // hack to ensure write and read permisions + if (!ok) { + qCritical() << "The file mode can't be read"; + return false; + } + // there are names that are exactly 100 bytes long + // and neither longlink nor \0 terminated (bug:101472) + + if (name.isEmpty()) { + name = decodeName(buffer); + if (!firstFolderName.isEmpty() && name.startsWith(firstFolderName)) { + name = name.mid(firstFolderName.size()); + } + } + if (symlink.isEmpty()) + symlink = decodeName(buffer); + qint64 size = getOctal(buffer + 124, 12, &ok); + if (!ok) { + qCritical() << "The file size can't be read"; + return false; + } + switch (TypeFlag(buffer[156])) { + case TypeFlag::Regular: + /* fallthrough */ + case TypeFlag::ARegular: { + auto fileName = FS::PathCombine(dst, name); + if (!FS::ensureFilePathExists(fileName)) { + qCritical() << "Can't ensure the file path to exist: " << fileName; + return false; + } + QFile out(fileName); + if (!out.open(QFile::WriteOnly)) { + qCritical() << "Can't open file:" << fileName; + return false; + } + out.setPermissions(QFile::Permissions(mode)); + while (size > 0) { + QByteArray tmp(BLOCKSIZE, 0); + n = in->read(tmp.data(), BLOCKSIZE); + if (n != BLOCKSIZE) { + qCritical() << "The expected blocksize was not respected when reading file"; + return false; + } + tmp.truncate(qMin(qint64(BLOCKSIZE), size)); + out.write(tmp); + size -= BLOCKSIZE; + } + break; + } + case TypeFlag::Directory: { + if (firstFolderName.isEmpty()) { + firstFolderName = name; + break; + } + auto folderPath = FS::PathCombine(dst, name); + if (!FS::ensureFolderPathExists(folderPath)) { + qCritical() << "Can't ensure that folder exists: " << folderPath; + return false; + } + break; + } + case TypeFlag::GNULongLink: { + doNotReset = true; + QByteArray longlink; + if (readLonglink(in, size, longlink)) { + symlink = QFile::decodeName(longlink.constData()); + } else { + qCritical() << "Failed to read long link"; + return false; + } + break; + } + case TypeFlag::GNULongName: { + doNotReset = true; + QByteArray longlink; + if (readLonglink(in, size, longlink)) { + name = QFile::decodeName(longlink.constData()); + } else { + qCritical() << "Failed to read long name"; + return false; + } + break; + } + case TypeFlag::Link: + /* fallthrough */ + case TypeFlag::Symlink: { + auto fileName = FS::PathCombine(dst, name); + if (!FS::create_link(FS::PathCombine(QFileInfo(fileName).path(), symlink), fileName)()) { // do not use symlinks + qCritical() << "Can't create link for:" << fileName << " to:" << FS::PathCombine(QFileInfo(fileName).path(), symlink); + return false; + } + FS::ensureFilePathExists(fileName); + QFile::setPermissions(fileName, QFile::Permissions(mode)); + break; + } + case TypeFlag::Character: + /* fallthrough */ + case TypeFlag::Block: + /* fallthrough */ + case TypeFlag::FIFO: + /* fallthrough */ + case TypeFlag::Contiguous: + /* fallthrough */ + case TypeFlag::GlobalPosixHeader: + /* fallthrough */ + case TypeFlag::ExtendedPosixHeader: + /* fallthrough */ + default: + break; + } + if (!doNotReset) { + name.truncate(0); + symlink.truncate(0); + } + doNotReset = false; + } + return true; +} + +bool GZTar::extract(QString src, QString dst) +{ + QuaGzipFile a(src); + if (!a.open(QIODevice::ReadOnly)) { + qCritical() << "Can't open tar file:" << src; + return false; + } + return Tar::extract(&a, dst); +} \ No newline at end of file diff --git a/launcher/Untar.h b/launcher/Untar.h new file mode 100644 index 0000000000..50e3a16e32 --- /dev/null +++ b/launcher/Untar.h @@ -0,0 +1,46 @@ +// SPDX-License-Identifier: GPL-3.0-only +/* + * Prism Launcher - Minecraft Launcher + * Copyright (c) 2023-2024 Trial97 + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, version 3. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + * + * This file incorporates work covered by the following copyright and + * permission notice: + * + * Copyright 2013-2021 MultiMC Contributors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +#pragma once +#include + +// this is a hack used for the java downloader (feel free to remove it in favor of a library) +// both extract functions will extract the first folder inside dest(disregarding the prefix) +namespace Tar { +bool extract(QIODevice* in, QString dst); +} + +namespace GZTar { +bool extract(QString src, QString dst); +} \ No newline at end of file diff --git a/launcher/VersionProxyModel.cpp b/launcher/VersionProxyModel.cpp index 0ab9ae2c3b..62cf4c2215 100644 --- a/launcher/VersionProxyModel.cpp +++ b/launcher/VersionProxyModel.cpp @@ -114,10 +114,14 @@ QVariant VersionProxyModel::headerData(int section, Qt::Orientation orientation, return tr("Branch"); case Type: return tr("Type"); - case Architecture: + case CPUArchitecture: return tr("Architecture"); case Path: return tr("Path"); + case JavaName: + return tr("Java Name"); + case JavaMajor: + return tr("Major"); case Time: return tr("Released"); } @@ -131,10 +135,14 @@ QVariant VersionProxyModel::headerData(int section, Qt::Orientation orientation, return tr("The version's branch"); case Type: return tr("The version's type"); - case Architecture: + case CPUArchitecture: return tr("CPU Architecture"); case Path: return tr("Filesystem path to this version"); + case JavaName: + return tr("The alternative name of the java version"); + case JavaMajor: + return tr("The java major version"); case Time: return tr("Release date of this version"); } @@ -165,10 +173,14 @@ QVariant VersionProxyModel::data(const QModelIndex& index, int role) const return sourceModel()->data(parentIndex, BaseVersionList::BranchRole); case Type: return sourceModel()->data(parentIndex, BaseVersionList::TypeRole); - case Architecture: - return sourceModel()->data(parentIndex, BaseVersionList::ArchitectureRole); + case CPUArchitecture: + return sourceModel()->data(parentIndex, BaseVersionList::CPUArchitectureRole); case Path: return sourceModel()->data(parentIndex, BaseVersionList::PathRole); + case JavaName: + return sourceModel()->data(parentIndex, BaseVersionList::JavaNameRole); + case JavaMajor: + return sourceModel()->data(parentIndex, BaseVersionList::JavaMajorRole); case Time: return sourceModel()->data(parentIndex, Meta::VersionList::TimeRole).toDate(); default: @@ -308,12 +320,18 @@ void VersionProxyModel::setSourceModel(QAbstractItemModel* replacingRaw) m_columns.push_back(ParentVersion); } */ - if (roles.contains(BaseVersionList::ArchitectureRole)) { - m_columns.push_back(Architecture); + if (roles.contains(BaseVersionList::CPUArchitectureRole)) { + m_columns.push_back(CPUArchitecture); } if (roles.contains(BaseVersionList::PathRole)) { m_columns.push_back(Path); } + if (roles.contains(BaseVersionList::JavaNameRole)) { + m_columns.push_back(JavaName); + } + if (roles.contains(BaseVersionList::JavaMajorRole)) { + m_columns.push_back(JavaMajor); + } if (roles.contains(Meta::VersionList::TimeRole)) { m_columns.push_back(Time); } diff --git a/launcher/VersionProxyModel.h b/launcher/VersionProxyModel.h index efea1a0bb5..7965af0ad8 100644 --- a/launcher/VersionProxyModel.h +++ b/launcher/VersionProxyModel.h @@ -9,7 +9,7 @@ class VersionFilterModel; class VersionProxyModel : public QAbstractProxyModel { Q_OBJECT public: - enum Column { Name, ParentVersion, Branch, Type, Architecture, Path, Time }; + enum Column { Name, ParentVersion, Branch, Type, CPUArchitecture, Path, Time, JavaName, JavaMajor }; using FilterMap = QHash>; public: diff --git a/launcher/java/JavaChecker.cpp b/launcher/java/JavaChecker.cpp index fc8da55c2b..c54a5b04b9 100644 --- a/launcher/java/JavaChecker.cpp +++ b/launcher/java/JavaChecker.cpp @@ -40,14 +40,15 @@ #include #include -#include "Application.h" #include "Commandline.h" #include "FileSystem.h" -#include "JavaUtils.h" +#include "java/JavaUtils.h" -JavaChecker::JavaChecker(QObject* parent) : QObject(parent) {} +JavaChecker::JavaChecker(QString path, QString args, int minMem, int maxMem, int permGen, int id, QObject* parent) + : Task(parent), m_path(path), m_args(args), m_minMem(minMem), m_maxMem(maxMem), m_permGen(permGen), m_id(id) +{} -void JavaChecker::performCheck() +void JavaChecker::executeTask() { QString checkerJar = JavaUtils::getJavaCheckPath(); @@ -72,7 +73,7 @@ void JavaChecker::performCheck() if (m_maxMem != 0) { args << QString("-Xmx%1m").arg(m_maxMem); } - if (m_permGen != 64) { + if (m_permGen != 64 && m_permGen != 0) { args << QString("-XX:PermSize=%1m").arg(m_permGen); } @@ -115,11 +116,10 @@ void JavaChecker::finished(int exitcode, QProcess::ExitStatus status) QProcessPtr _process = process; process.reset(); - JavaCheckResult result; - { - result.path = m_path; - result.id = m_id; - } + Result result = { + m_path, + m_id, + }; result.errorLog = m_stderr; result.outLog = m_stdout; qDebug() << "STDOUT" << m_stdout; @@ -127,8 +127,9 @@ void JavaChecker::finished(int exitcode, QProcess::ExitStatus status) qDebug() << "Java checker finished with status" << status << "exit code" << exitcode; if (status == QProcess::CrashExit || exitcode == 1) { - result.validity = JavaCheckResult::Validity::Errored; + result.validity = Result::Validity::Errored; emit checkFinished(result); + emitSucceeded(); return; } @@ -161,8 +162,9 @@ void JavaChecker::finished(int exitcode, QProcess::ExitStatus status) } if (!results.contains("os.arch") || !results.contains("java.version") || !results.contains("java.vendor") || !success) { - result.validity = JavaCheckResult::Validity::ReturnedInvalidData; + result.validity = Result::Validity::ReturnedInvalidData; emit checkFinished(result); + emitSucceeded(); return; } @@ -171,7 +173,7 @@ void JavaChecker::finished(int exitcode, QProcess::ExitStatus status) auto java_vendor = results["java.vendor"]; bool is_64 = os_arch == "x86_64" || os_arch == "amd64" || os_arch == "aarch64" || os_arch == "arm64"; - result.validity = JavaCheckResult::Validity::Valid; + result.validity = Result::Validity::Valid; result.is_64bit = is_64; result.mojangPlatform = is_64 ? "64" : "32"; result.realPlatform = os_arch; @@ -179,6 +181,7 @@ void JavaChecker::finished(int exitcode, QProcess::ExitStatus status) result.javaVendor = java_vendor; qDebug() << "Java checker succeeded."; emit checkFinished(result); + emitSucceeded(); } void JavaChecker::error(QProcess::ProcessError err) @@ -190,15 +193,9 @@ void JavaChecker::error(QProcess::ProcessError err) qDebug() << "Native environment:"; qDebug() << QProcessEnvironment::systemEnvironment().toStringList(); killTimer.stop(); - JavaCheckResult result; - { - result.path = m_path; - result.id = m_id; - } - - emit checkFinished(result); - return; + emit checkFinished({ m_path, m_id }); } + emitSucceeded(); } void JavaChecker::timeout() diff --git a/launcher/java/JavaChecker.h b/launcher/java/JavaChecker.h index 7111f85227..171a18b763 100644 --- a/launcher/java/JavaChecker.h +++ b/launcher/java/JavaChecker.h @@ -3,49 +3,51 @@ #include #include -#include "QObjectPtr.h" - #include "JavaVersion.h" +#include "QObjectPtr.h" +#include "tasks/Task.h" -class JavaChecker; - -struct JavaCheckResult { - QString path; - QString mojangPlatform; - QString realPlatform; - JavaVersion javaVersion; - QString javaVendor; - QString outLog; - QString errorLog; - bool is_64bit = false; - int id; - enum class Validity { Errored, ReturnedInvalidData, Valid } validity = Validity::Errored; -}; - -using QProcessPtr = shared_qobject_ptr; -using JavaCheckerPtr = shared_qobject_ptr; -class JavaChecker : public QObject { +class JavaChecker : public Task { Q_OBJECT public: - explicit JavaChecker(QObject* parent = 0); - void performCheck(); - - QString m_path; - QString m_args; - int m_id = 0; - int m_minMem = 0; - int m_maxMem = 0; - int m_permGen = 64; + using QProcessPtr = shared_qobject_ptr; + using Ptr = shared_qobject_ptr; + + struct Result { + QString path; + int id; + QString mojangPlatform; + QString realPlatform; + JavaVersion javaVersion; + QString javaVendor; + QString outLog; + QString errorLog; + bool is_64bit = false; + enum class Validity { Errored, ReturnedInvalidData, Valid } validity = Validity::Errored; + }; + + explicit JavaChecker(QString path, QString args, int minMem = 0, int maxMem = 0, int permGen = 0, int id = 0, QObject* parent = 0); signals: - void checkFinished(JavaCheckResult result); + void checkFinished(const Result& result); + + protected: + virtual void executeTask() override; private: QProcessPtr process; QTimer killTimer; QString m_stdout; QString m_stderr; - public slots: + + QString m_path; + QString m_args; + int m_minMem = 0; + int m_maxMem = 0; + int m_permGen = 64; + int m_id = 0; + + private slots: void timeout(); void finished(int exitcode, QProcess::ExitStatus); void error(QProcess::ProcessError); diff --git a/launcher/java/JavaCheckerJob.cpp b/launcher/java/JavaCheckerJob.cpp deleted file mode 100644 index 870e2a09ad..0000000000 --- a/launcher/java/JavaCheckerJob.cpp +++ /dev/null @@ -1,41 +0,0 @@ -/* Copyright 2013-2021 MultiMC Contributors - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -#include "JavaCheckerJob.h" - -#include - -void JavaCheckerJob::partFinished(JavaCheckResult result) -{ - num_finished++; - qDebug() << m_job_name.toLocal8Bit() << "progress:" << num_finished << "/" << javacheckers.size(); - setProgress(num_finished, javacheckers.size()); - - javaresults.replace(result.id, result); - - if (num_finished == javacheckers.size()) { - emitSucceeded(); - } -} - -void JavaCheckerJob::executeTask() -{ - qDebug() << m_job_name.toLocal8Bit() << " started."; - for (auto iter : javacheckers) { - javaresults.append(JavaCheckResult()); - connect(iter.get(), &JavaChecker::checkFinished, this, &JavaCheckerJob::partFinished); - iter->performCheck(); - } -} diff --git a/launcher/java/JavaCheckerJob.h b/launcher/java/JavaCheckerJob.h deleted file mode 100644 index 16b572632f..0000000000 --- a/launcher/java/JavaCheckerJob.h +++ /dev/null @@ -1,56 +0,0 @@ -/* Copyright 2013-2021 MultiMC Contributors - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -#pragma once - -#include -#include "JavaChecker.h" -#include "tasks/Task.h" - -class JavaCheckerJob; -using JavaCheckerJobPtr = shared_qobject_ptr; - -// FIXME: this just seems horribly redundant -class JavaCheckerJob : public Task { - Q_OBJECT - public: - explicit JavaCheckerJob(QString job_name) : Task(), m_job_name(job_name) {}; - virtual ~JavaCheckerJob() {}; - - bool addJavaCheckerAction(JavaCheckerPtr base) - { - javacheckers.append(base); - // if this is already running, the action needs to be started right away! - if (isRunning()) { - setProgress(num_finished, javacheckers.size()); - connect(base.get(), &JavaChecker::checkFinished, this, &JavaCheckerJob::partFinished); - base->performCheck(); - } - return true; - } - QList getResults() { return javaresults; } - - private slots: - void partFinished(JavaCheckResult result); - - protected: - virtual void executeTask() override; - - private: - QString m_job_name; - QList javacheckers; - QList javaresults; - int num_finished = 0; -}; diff --git a/launcher/java/JavaInstall.cpp b/launcher/java/JavaInstall.cpp index cfa471402f..8e97e0e144 100644 --- a/launcher/java/JavaInstall.cpp +++ b/launcher/java/JavaInstall.cpp @@ -1,7 +1,7 @@ // SPDX-License-Identifier: GPL-3.0-only /* * Prism Launcher - Minecraft Launcher - * Copyright (c) 2023 Trial97 + * Copyright (c) 2023-2024 Trial97 * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by diff --git a/launcher/java/JavaInstall.h b/launcher/java/JavaInstall.h index 8c2743a00e..7d8d392fab 100644 --- a/launcher/java/JavaInstall.h +++ b/launcher/java/JavaInstall.h @@ -1,7 +1,7 @@ // SPDX-License-Identifier: GPL-3.0-only /* * Prism Launcher - Minecraft Launcher - * Copyright (c) 2023 Trial97 + * Copyright (c) 2023-2024 Trial97 * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by @@ -40,6 +40,7 @@ struct JavaInstall : public BaseVersion { QString arch; QString path; bool recommended = false; + bool is_64bit = false; }; using JavaInstallPtr = std::shared_ptr; diff --git a/launcher/java/JavaInstallList.cpp b/launcher/java/JavaInstallList.cpp index d8be4963f5..569fda306b 100644 --- a/launcher/java/JavaInstallList.cpp +++ b/launcher/java/JavaInstallList.cpp @@ -38,13 +38,17 @@ #include #include +#include -#include "java/JavaCheckerJob.h" +#include "Application.h" +#include "java/JavaChecker.h" #include "java/JavaInstallList.h" #include "java/JavaUtils.h" -#include "minecraft/VersionFilterData.h" +#include "tasks/ConcurrentTask.h" -JavaInstallList::JavaInstallList(QObject* parent) : BaseVersionList(parent) {} +JavaInstallList::JavaInstallList(QObject* parent, bool onlyManagedVersions) + : BaseVersionList(parent), m_only_managed_versions(onlyManagedVersions) +{} Task::Ptr JavaInstallList::getLoadTask() { @@ -55,7 +59,7 @@ Task::Ptr JavaInstallList::getLoadTask() Task::Ptr JavaInstallList::getCurrentTask() { if (m_status == Status::InProgress) { - return m_loadTask; + return m_load_task; } return nullptr; } @@ -64,8 +68,8 @@ void JavaInstallList::load() { if (m_status != Status::InProgress) { m_status = Status::InProgress; - m_loadTask.reset(new JavaListLoadTask(this)); - m_loadTask->start(); + m_load_task.reset(new JavaListLoadTask(this, m_only_managed_versions)); + m_load_task->start(); } } @@ -106,7 +110,7 @@ QVariant JavaInstallList::data(const QModelIndex& index, int role) const return version->recommended; case PathRole: return version->path; - case ArchitectureRole: + case CPUArchitectureRole: return version->arch; default: return QVariant(); @@ -115,7 +119,7 @@ QVariant JavaInstallList::data(const QModelIndex& index, int role) const BaseVersionList::RoleList JavaInstallList::providesRoles() const { - return { VersionPointerRole, VersionIdRole, VersionRole, RecommendedRole, PathRole, ArchitectureRole }; + return { VersionPointerRole, VersionIdRole, VersionRole, RecommendedRole, PathRole, CPUArchitectureRole }; } void JavaInstallList::updateListData(QList versions) @@ -129,7 +133,7 @@ void JavaInstallList::updateListData(QList versions) } endResetModel(); m_status = Status::Done; - m_loadTask.reset(); + m_load_task.reset(); } bool sortJavas(BaseVersion::Ptr left, BaseVersion::Ptr right) @@ -146,35 +150,30 @@ void JavaInstallList::sortVersions() endResetModel(); } -JavaListLoadTask::JavaListLoadTask(JavaInstallList* vlist) : Task() +JavaListLoadTask::JavaListLoadTask(JavaInstallList* vlist, bool onlyManagedVersions) : Task(), m_only_managed_versions(onlyManagedVersions) { m_list = vlist; - m_currentRecommended = NULL; + m_current_recommended = NULL; } -JavaListLoadTask::~JavaListLoadTask() {} - void JavaListLoadTask::executeTask() { setStatus(tr("Detecting Java installations...")); JavaUtils ju; - QList candidate_paths = ju.FindJavaPaths(); + QList candidate_paths = m_only_managed_versions ? getPrismJavaBundle() : ju.FindJavaPaths(); - m_job.reset(new JavaCheckerJob("Java detection")); + ConcurrentTask::Ptr job(new ConcurrentTask(this, "Java detection", APPLICATION->settings()->get("NumberOfConcurrentTasks").toInt())); + m_job.reset(job); connect(m_job.get(), &Task::finished, this, &JavaListLoadTask::javaCheckerFinished); connect(m_job.get(), &Task::progress, this, &Task::setProgress); qDebug() << "Probing the following Java paths: "; int id = 0; for (QString candidate : candidate_paths) { - qDebug() << " " << candidate; - - auto candidate_checker = new JavaChecker(); - candidate_checker->m_path = candidate; - candidate_checker->m_id = id; - m_job->addJavaCheckerAction(JavaCheckerPtr(candidate_checker)); - + auto checker = new JavaChecker(candidate, "", 0, 0, 0, id, this); + connect(checker, &JavaChecker::checkFinished, [this](const JavaChecker::Result& result) { m_results << result; }); + job->addTask(Task::Ptr(checker)); id++; } @@ -184,16 +183,17 @@ void JavaListLoadTask::executeTask() void JavaListLoadTask::javaCheckerFinished() { QList candidates; - auto results = m_job->getResults(); + std::sort(m_results.begin(), m_results.end(), [](const JavaChecker::Result& a, const JavaChecker::Result& b) { return a.id < b.id; }); qDebug() << "Found the following valid Java installations:"; - for (JavaCheckResult result : results) { - if (result.validity == JavaCheckResult::Validity::Valid) { + for (auto result : m_results) { + if (result.validity == JavaChecker::Result::Validity::Valid) { JavaInstallPtr javaVersion(new JavaInstall()); javaVersion->id = result.javaVersion; javaVersion->arch = result.realPlatform; javaVersion->path = result.path; + javaVersion->is_64bit = result.is_64bit; candidates.append(javaVersion); qDebug() << " " << javaVersion->id.toString() << javaVersion->arch << javaVersion->path; diff --git a/launcher/java/JavaInstallList.h b/launcher/java/JavaInstallList.h index 1eebadf234..c68c2a3be0 100644 --- a/launcher/java/JavaInstallList.h +++ b/launcher/java/JavaInstallList.h @@ -19,9 +19,9 @@ #include #include "BaseVersionList.h" +#include "java/JavaChecker.h" #include "tasks/Task.h" -#include "JavaCheckerJob.h" #include "JavaInstall.h" #include "QObjectPtr.h" @@ -33,7 +33,7 @@ class JavaInstallList : public BaseVersionList { enum class Status { NotDone, InProgress, Done }; public: - explicit JavaInstallList(QObject* parent = 0); + explicit JavaInstallList(QObject* parent = 0, bool onlyManagedVersions = false); Task::Ptr getLoadTask() override; bool isLoaded() override; @@ -53,23 +53,27 @@ class JavaInstallList : public BaseVersionList { protected: Status m_status = Status::NotDone; - shared_qobject_ptr m_loadTask; + shared_qobject_ptr m_load_task; QList m_vlist; + bool m_only_managed_versions; }; class JavaListLoadTask : public Task { Q_OBJECT public: - explicit JavaListLoadTask(JavaInstallList* vlist); - virtual ~JavaListLoadTask(); + explicit JavaListLoadTask(JavaInstallList* vlist, bool onlyManagedVersions = false); + virtual ~JavaListLoadTask() = default; + protected: void executeTask() override; public slots: void javaCheckerFinished(); protected: - shared_qobject_ptr m_job; + Task::Ptr m_job; JavaInstallList* m_list; - JavaInstall* m_currentRecommended; + JavaInstall* m_current_recommended; + QList m_results; + bool m_only_managed_versions; }; diff --git a/launcher/java/JavaMetadata.cpp b/launcher/java/JavaMetadata.cpp new file mode 100644 index 0000000000..2d68f55c8c --- /dev/null +++ b/launcher/java/JavaMetadata.cpp @@ -0,0 +1,128 @@ +// SPDX-License-Identifier: GPL-3.0-only +/* + * Prism Launcher - Minecraft Launcher + * Copyright (c) 2023-2024 Trial97 + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, version 3. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +#include "java/JavaMetadata.h" + +#include + +#include "Json.h" +#include "StringUtils.h" +#include "java/JavaVersion.h" +#include "minecraft/ParseUtils.h" + +namespace Java { + +DownloadType parseDownloadType(QString javaDownload) +{ + if (javaDownload == "manifest") + return DownloadType::Manifest; + else if (javaDownload == "archive") + return DownloadType::Archive; + else + return DownloadType::Unknown; +} +QString downloadTypeToString(DownloadType javaDownload) +{ + switch (javaDownload) { + case DownloadType::Manifest: + return "manifest"; + case DownloadType::Archive: + return "archive"; + case DownloadType::Unknown: + break; + } + return "unknown"; +} +MetadataPtr parseJavaMeta(const QJsonObject& in) +{ + auto meta = std::make_shared(); + + meta->m_name = Json::ensureString(in, "name", ""); + meta->vendor = Json::ensureString(in, "vendor", ""); + meta->url = Json::ensureString(in, "url", ""); + meta->releaseTime = timeFromS3Time(Json::ensureString(in, "releaseTime", "")); + meta->downloadType = parseDownloadType(Json::ensureString(in, "downloadType", "")); + meta->packageType = Json::ensureString(in, "packageType", ""); + meta->runtimeOS = Json::ensureString(in, "runtimeOS", "unknown"); + + if (in.contains("checksum")) { + auto obj = Json::requireObject(in, "checksum"); + meta->checksumHash = Json::ensureString(obj, "hash", ""); + meta->checksumType = Json::ensureString(obj, "type", ""); + } + + if (in.contains("version")) { + auto obj = Json::requireObject(in, "version"); + auto name = Json::ensureString(obj, "name", ""); + auto major = Json::ensureInteger(obj, "major", 0); + auto minor = Json::ensureInteger(obj, "minor", 0); + auto security = Json::ensureInteger(obj, "security", 0); + auto build = Json::ensureInteger(obj, "build", 0); + meta->version = JavaVersion(major, minor, security, build, name); + } + return meta; +} + +bool Metadata::operator<(const Metadata& rhs) +{ + auto id = version; + if (id < rhs.version) { + return true; + } + if (id > rhs.version) { + return false; + } + auto date = releaseTime; + if (date < rhs.releaseTime) { + return true; + } + if (date > rhs.releaseTime) { + return false; + } + return StringUtils::naturalCompare(m_name, rhs.m_name, Qt::CaseInsensitive) < 0; +} + +bool Metadata::operator==(const Metadata& rhs) +{ + return version == rhs.version && m_name == rhs.m_name; +} + +bool Metadata::operator>(const Metadata& rhs) +{ + return (!operator<(rhs)) && (!operator==(rhs)); +} + +bool Metadata::operator<(BaseVersion& a) +{ + try { + return operator<(dynamic_cast(a)); + } catch (const std::bad_cast& e) { + return BaseVersion::operator<(a); + } +} + +bool Metadata::operator>(BaseVersion& a) +{ + try { + return operator>(dynamic_cast(a)); + } catch (const std::bad_cast& e) { + return BaseVersion::operator>(a); + } +} + +} // namespace Java diff --git a/launcher/java/JavaMetadata.h b/launcher/java/JavaMetadata.h new file mode 100644 index 0000000000..77a42fd78f --- /dev/null +++ b/launcher/java/JavaMetadata.h @@ -0,0 +1,64 @@ +// SPDX-License-Identifier: GPL-3.0-only +/* + * Prism Launcher - Minecraft Launcher + * Copyright (c) 2023-2024 Trial97 + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, version 3. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ +#pragma once + +#include +#include +#include + +#include + +#include "BaseVersion.h" +#include "java/JavaVersion.h" + +namespace Java { + +enum class DownloadType { Manifest, Archive, Unknown }; + +class Metadata : public BaseVersion { + public: + virtual QString descriptor() override { return version.toString(); } + + virtual QString name() override { return m_name; } + + virtual QString typeString() const override { return vendor; } + + virtual bool operator<(BaseVersion& a) override; + virtual bool operator>(BaseVersion& a) override; + bool operator<(const Metadata& rhs); + bool operator==(const Metadata& rhs); + bool operator>(const Metadata& rhs); + + QString m_name; + QString vendor; + QString url; + QDateTime releaseTime; + QString checksumType; + QString checksumHash; + DownloadType downloadType; + QString packageType; + JavaVersion version; + QString runtimeOS; +}; +using MetadataPtr = std::shared_ptr; + +DownloadType parseDownloadType(QString javaDownload); +QString downloadTypeToString(DownloadType javaDownload); +MetadataPtr parseJavaMeta(const QJsonObject& libObj); + +} // namespace Java \ No newline at end of file diff --git a/launcher/java/JavaUtils.cpp b/launcher/java/JavaUtils.cpp index 809d810877..86fa3da049 100644 --- a/launcher/java/JavaUtils.cpp +++ b/launcher/java/JavaUtils.cpp @@ -345,6 +345,7 @@ QList JavaUtils::FindJavaPaths() } candidates.append(getMinecraftJavaBundle()); + candidates.append(getPrismJavaBundle()); candidates = addJavasFromEnv(candidates); candidates.removeDuplicates(); return candidates; @@ -389,6 +390,7 @@ QList JavaUtils::FindJavaPaths() } javas.append(getMinecraftJavaBundle()); + javas.append(getPrismJavaBundle()); javas = addJavasFromEnv(javas); javas.removeDuplicates(); return javas; @@ -448,6 +450,7 @@ QList JavaUtils::FindJavaPaths() scanJavaDirs(FS::PathCombine(home, ".gradle/jdks")); javas.append(getMinecraftJavaBundle()); + javas.append(getPrismJavaBundle()); javas = addJavasFromEnv(javas); javas.removeDuplicates(); return javas; @@ -461,6 +464,8 @@ QList JavaUtils::FindJavaPaths() javas.append(this->GetDefaultJava()->path); javas.append(getMinecraftJavaBundle()); + javas.append(getPrismJavaBundle()); + javas.removeDuplicates(); return addJavasFromEnv(javas); } #endif @@ -472,12 +477,10 @@ QString JavaUtils::getJavaCheckPath() QStringList getMinecraftJavaBundle() { - QString executable = "java"; QStringList processpaths; #if defined(Q_OS_OSX) processpaths << FS::PathCombine(QDir::homePath(), FS::PathCombine("Library", "Application Support", "minecraft", "runtime")); #elif defined(Q_OS_WIN32) - executable += "w.exe"; auto appDataPath = QProcessEnvironment::systemEnvironment().value("APPDATA", ""); processpaths << FS::PathCombine(QFileInfo(appDataPath).absoluteFilePath(), ".minecraft", "runtime"); @@ -502,7 +505,7 @@ QStringList getMinecraftJavaBundle() auto binFound = false; for (auto& entry : entries) { if (entry.baseName() == "bin") { - javas.append(FS::PathCombine(entry.canonicalFilePath(), executable)); + javas.append(FS::PathCombine(entry.canonicalFilePath(), JavaUtils::javaExecutable)); binFound = true; break; } @@ -515,3 +518,33 @@ QStringList getMinecraftJavaBundle() } return javas; } + +#if defined(Q_OS_WIN32) +const QString JavaUtils::javaExecutable = "javaw.exe"; +#else +const QString JavaUtils::javaExecutable = "java"; +#endif + +QStringList getPrismJavaBundle() +{ + QList javas; + + auto scanDir = [&](QString prefix) { + javas.append(FS::PathCombine(prefix, "jre", "bin", JavaUtils::javaExecutable)); + javas.append(FS::PathCombine(prefix, "bin", JavaUtils::javaExecutable)); + javas.append(FS::PathCombine(prefix, JavaUtils::javaExecutable)); + }; + auto scanJavaDir = [&](const QString& dirPath) { + QDir dir(dirPath); + if (!dir.exists()) + return; + auto entries = dir.entryInfoList(QDir::Dirs | QDir::NoDotAndDotDot); + for (auto& entry : entries) { + scanDir(entry.canonicalFilePath()); + } + }; + + scanJavaDir(APPLICATION->javaPath()); + + return javas; +} diff --git a/launcher/java/JavaUtils.h b/launcher/java/JavaUtils.h index 2fb03af7ae..eb3a17316b 100644 --- a/launcher/java/JavaUtils.h +++ b/launcher/java/JavaUtils.h @@ -15,10 +15,9 @@ #pragma once +#include #include - -#include "JavaChecker.h" -#include "JavaInstallList.h" +#include "java/JavaInstall.h" #ifdef Q_OS_WIN #include @@ -27,6 +26,7 @@ QString stripVariableEntries(QString name, QString target, QString remove); QProcessEnvironment CleanEnviroment(); QStringList getMinecraftJavaBundle(); +QStringList getPrismJavaBundle(); class JavaUtils : public QObject { Q_OBJECT @@ -42,4 +42,5 @@ class JavaUtils : public QObject { #endif static QString getJavaCheckPath(); + static const QString javaExecutable; }; diff --git a/launcher/java/JavaVersion.cpp b/launcher/java/JavaVersion.cpp index b77bf2adfe..5e97000121 100644 --- a/launcher/java/JavaVersion.cpp +++ b/launcher/java/JavaVersion.cpp @@ -43,12 +43,12 @@ QString JavaVersion::toString() const return m_string; } -bool JavaVersion::requiresPermGen() +bool JavaVersion::requiresPermGen() const { return !m_parseable || m_major < 8; } -bool JavaVersion::isModular() +bool JavaVersion::isModular() const { return m_parseable && m_major >= 9; } @@ -59,12 +59,6 @@ bool JavaVersion::operator<(const JavaVersion& rhs) auto major = m_major; auto rmajor = rhs.m_major; - // HACK: discourage using java 9 - if (major > 8) - major = -major; - if (rmajor > 8) - rmajor = -rmajor; - if (major < rmajor) return true; if (major > rmajor) @@ -109,3 +103,24 @@ bool JavaVersion::operator>(const JavaVersion& rhs) { return (!operator<(rhs)) && (!operator==(rhs)); } + +JavaVersion::JavaVersion(int major, int minor, int security, int build, QString name) + : m_major(major), m_minor(minor), m_security(security), m_name(name), m_parseable(true) +{ + QStringList versions; + if (build != 0) { + m_prerelease = QString::number(build); + versions.push_front(m_prerelease); + } + if (m_security != 0) + versions.push_front(QString::number(m_security)); + else if (!versions.isEmpty()) + versions.push_front("0"); + + if (m_minor != 0) + versions.push_front(QString::number(m_minor)); + else if (!versions.isEmpty()) + versions.push_front("0"); + versions.push_front(QString::number(m_major)); + m_string = versions.join("."); +} diff --git a/launcher/java/JavaVersion.h b/launcher/java/JavaVersion.h index 421578ea1a..dfb4770da5 100644 --- a/launcher/java/JavaVersion.h +++ b/launcher/java/JavaVersion.h @@ -16,6 +16,7 @@ class JavaVersion { public: JavaVersion() {} JavaVersion(const QString& rhs); + JavaVersion(int major, int minor, int security, int build = 0, QString name = ""); JavaVersion& operator=(const QString& rhs); @@ -23,21 +24,24 @@ class JavaVersion { bool operator==(const JavaVersion& rhs); bool operator>(const JavaVersion& rhs); - bool requiresPermGen(); + bool requiresPermGen() const; - bool isModular(); + bool isModular() const; QString toString() const; - int major() { return m_major; } - int minor() { return m_minor; } - int security() { return m_security; } + int major() const { return m_major; } + int minor() const { return m_minor; } + int security() const { return m_security; } + QString build() const { return m_prerelease; } + QString name() const { return m_name; } private: QString m_string; int m_major = 0; int m_minor = 0; int m_security = 0; + QString m_name = ""; bool m_parseable = false; QString m_prerelease; }; diff --git a/launcher/java/download/ArchiveDownloadTask.cpp b/launcher/java/download/ArchiveDownloadTask.cpp new file mode 100644 index 0000000000..af5381dfc4 --- /dev/null +++ b/launcher/java/download/ArchiveDownloadTask.cpp @@ -0,0 +1,140 @@ +// SPDX-License-Identifier: GPL-3.0-only +/* + * Prism Launcher - Minecraft Launcher + * Copyright (c) 2023-2024 Trial97 + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, version 3. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ +#include "java/download/ArchiveDownloadTask.h" +#include +#include +#include "MMCZip.h" + +#include "Application.h" +#include "Untar.h" +#include "net/ChecksumValidator.h" +#include "net/NetJob.h" +#include "tasks/Task.h" + +namespace Java { +ArchiveDownloadTask::ArchiveDownloadTask(QUrl url, QString final_path, QString checksumType, QString checksumHash) + : m_url(url), m_final_path(final_path), m_checksum_type(checksumType), m_checksum_hash(checksumHash) +{} + +void ArchiveDownloadTask::executeTask() +{ + // JRE found ! download the zip + setStatus(tr("Downloading Java")); + + MetaEntryPtr entry = APPLICATION->metacache()->resolveEntry("java", m_url.fileName()); + + auto download = makeShared(QString("JRE::DownloadJava"), APPLICATION->network()); + auto action = Net::Download::makeCached(m_url, entry); + if (!m_checksum_hash.isEmpty() && !m_checksum_type.isEmpty()) { + auto hashType = QCryptographicHash::Algorithm::Sha1; + if (m_checksum_type == "sha256") { + hashType = QCryptographicHash::Algorithm::Sha256; + } + action->addValidator(new Net::ChecksumValidator(hashType, QByteArray::fromHex(m_checksum_hash.toUtf8()))); + } + download->addNetAction(action); + auto fullPath = entry->getFullPath(); + + connect(download.get(), &Task::failed, this, &ArchiveDownloadTask::emitFailed); + connect(download.get(), &Task::progress, this, &ArchiveDownloadTask::setProgress); + connect(download.get(), &Task::stepProgress, this, &ArchiveDownloadTask::propagateStepProgress); + connect(download.get(), &Task::status, this, &ArchiveDownloadTask::setStatus); + connect(download.get(), &Task::details, this, &ArchiveDownloadTask::setDetails); + connect(download.get(), &Task::succeeded, [this, fullPath] { + // This should do all of the extracting and creating folders + extractJava(fullPath); + }); + m_task = download; + m_task->start(); +} + +void ArchiveDownloadTask::extractJava(QString input) +{ + setStatus(tr("Extracting java")); + if (input.endsWith("tar")) { + setStatus(tr("Extracting Java (Progress is not reported for tar archives)")); + QFile in(input); + if (!in.open(QFile::ReadOnly)) { + emitFailed(tr("Unable to open supplied tar file.")); + return; + } + if (!Tar::extract(&in, QDir(m_final_path).absolutePath())) { + emitFailed(tr("Unable to extract supplied tar file.")); + return; + } + emitSucceeded(); + return; + } else if (input.endsWith("tar.gz") || input.endsWith("taz") || input.endsWith("tgz")) { + setStatus(tr("Extracting Java (Progress is not reported for tar archives)")); + if (!GZTar::extract(input, QDir(m_final_path).absolutePath())) { + emitFailed(tr("Unable to extract supplied tar file.")); + return; + } + emitSucceeded(); + return; + } else if (input.endsWith("zip")) { + auto zip = std::make_shared(input); + if (!zip->open(QuaZip::mdUnzip)) { + emitFailed(tr("Unable to open supplied zip file.")); + return; + } + auto files = zip->getFileNameList(); + if (files.isEmpty()) { + emitFailed(tr("No files were found in the supplied zip file,")); + return; + } + m_task = makeShared(zip, m_final_path, files[0]); + + auto progressStep = std::make_shared(); + connect(m_task.get(), &Task::finished, this, [this, progressStep] { + progressStep->state = TaskStepState::Succeeded; + stepProgress(*progressStep); + }); + + connect(m_task.get(), &Task::succeeded, this, &ArchiveDownloadTask::emitSucceeded); + connect(m_task.get(), &Task::aborted, this, &ArchiveDownloadTask::emitAborted); + connect(m_task.get(), &Task::failed, this, [this, progressStep](QString reason) { + progressStep->state = TaskStepState::Failed; + stepProgress(*progressStep); + emitFailed(reason); + }); + connect(m_task.get(), &Task::stepProgress, this, &ArchiveDownloadTask::propagateStepProgress); + + connect(m_task.get(), &Task::progress, this, [this, progressStep](qint64 current, qint64 total) { + progressStep->update(current, total); + stepProgress(*progressStep); + }); + connect(m_task.get(), &Task::status, this, [this, progressStep](QString status) { + progressStep->status = status; + stepProgress(*progressStep); + }); + m_task->start(); + } + + emitFailed(tr("Could not determine archive type!")); +} + +bool ArchiveDownloadTask::abort() +{ + auto aborted = canAbort(); + if (m_task) + aborted = m_task->abort(); + emitAborted(); + return aborted; +}; +} // namespace Java \ No newline at end of file diff --git a/launcher/java/download/ArchiveDownloadTask.h b/launcher/java/download/ArchiveDownloadTask.h new file mode 100644 index 0000000000..1db33763ac --- /dev/null +++ b/launcher/java/download/ArchiveDownloadTask.h @@ -0,0 +1,45 @@ +// SPDX-License-Identifier: GPL-3.0-only +/* + * Prism Launcher - Minecraft Launcher + * Copyright (c) 2023-2024 Trial97 + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, version 3. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +#pragma once + +#include +#include "tasks/Task.h" + +namespace Java { +class ArchiveDownloadTask : public Task { + Q_OBJECT + public: + ArchiveDownloadTask(QUrl url, QString final_path, QString checksumType = "", QString checksumHash = ""); + virtual ~ArchiveDownloadTask() = default; + + [[nodiscard]] bool canAbort() const override { return true; } + void executeTask() override; + virtual bool abort() override; + + private slots: + void extractJava(QString input); + + protected: + QUrl m_url; + QString m_final_path; + QString m_checksum_type; + QString m_checksum_hash; + Task::Ptr m_task; +}; +} // namespace Java \ No newline at end of file diff --git a/launcher/java/download/ManifestDownloadTask.cpp b/launcher/java/download/ManifestDownloadTask.cpp new file mode 100644 index 0000000000..836afeaac9 --- /dev/null +++ b/launcher/java/download/ManifestDownloadTask.cpp @@ -0,0 +1,138 @@ +// SPDX-License-Identifier: GPL-3.0-only +/* + * Prism Launcher - Minecraft Launcher + * Copyright (c) 2023-2024 Trial97 + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, version 3. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ +#include "java/download/ManifestDownloadTask.h" + +#include "Application.h" +#include "FileSystem.h" +#include "Json.h" +#include "net/ChecksumValidator.h" +#include "net/NetJob.h" + +struct File { + QString path; + QString url; + QByteArray hash; + bool isExec; +}; + +namespace Java { +ManifestDownloadTask::ManifestDownloadTask(QUrl url, QString final_path, QString checksumType, QString checksumHash) + : m_url(url), m_final_path(final_path), m_checksum_type(checksumType), m_checksum_hash(checksumHash) +{} + +void ManifestDownloadTask::executeTask() +{ + setStatus(tr("Downloading Java")); + auto download = makeShared(QString("JRE::DownloadJava"), APPLICATION->network()); + auto files = std::make_shared(); + + auto action = Net::Download::makeByteArray(m_url, files); + if (!m_checksum_hash.isEmpty() && !m_checksum_type.isEmpty()) { + auto hashType = QCryptographicHash::Algorithm::Sha1; + if (m_checksum_type == "sha256") { + hashType = QCryptographicHash::Algorithm::Sha256; + } + action->addValidator(new Net::ChecksumValidator(hashType, QByteArray::fromHex(m_checksum_hash.toUtf8()))); + } + download->addNetAction(action); + + connect(download.get(), &Task::failed, this, &ManifestDownloadTask::emitFailed); + connect(download.get(), &Task::progress, this, &ManifestDownloadTask::setProgress); + connect(download.get(), &Task::stepProgress, this, &ManifestDownloadTask::propagateStepProgress); + connect(download.get(), &Task::status, this, &ManifestDownloadTask::setStatus); + connect(download.get(), &Task::details, this, &ManifestDownloadTask::setDetails); + + connect(download.get(), &Task::succeeded, [files, this] { + QJsonParseError parse_error{}; + QJsonDocument doc = QJsonDocument::fromJson(*files, &parse_error); + if (parse_error.error != QJsonParseError::NoError) { + qWarning() << "Error while parsing JSON response at " << parse_error.offset << ". Reason: " << parse_error.errorString(); + qWarning() << *files; + emitFailed(parse_error.errorString()); + return; + } + downloadJava(doc); + }); + m_task = download; + m_task->start(); +} + +void ManifestDownloadTask::downloadJava(const QJsonDocument& doc) +{ + // valid json doc, begin making jre spot + FS::ensureFolderPathExists(m_final_path); + std::vector toDownload; + auto list = Json::ensureObject(Json::ensureObject(doc.object()), "files"); + for (const auto& paths : list.keys()) { + auto file = FS::PathCombine(m_final_path, paths); + + const QJsonObject& meta = Json::ensureObject(list, paths); + auto type = Json::ensureString(meta, "type"); + if (type == "directory") { + FS::ensureFolderPathExists(file); + } else if (type == "link") { + // this is linux only ! + auto path = Json::ensureString(meta, "target"); + if (!path.isEmpty()) { + auto target = FS::PathCombine(file, "../" + path); + QFile(target).link(file); + } + } else if (type == "file") { + // TODO download compressed version if it exists ? + auto raw = Json::ensureObject(Json::ensureObject(meta, "downloads"), "raw"); + auto isExec = Json::ensureBoolean(meta, "executable", false); + auto url = Json::ensureString(raw, "url"); + if (!url.isEmpty() && QUrl(url).isValid()) { + auto f = File{ file, url, QByteArray::fromHex(Json::ensureString(raw, "sha1").toLatin1()), isExec }; + toDownload.push_back(f); + } + } + } + auto elementDownload = makeShared("JRE::FileDownload", APPLICATION->network()); + for (const auto& file : toDownload) { + auto dl = Net::Download::makeFile(file.url, file.path); + if (!file.hash.isEmpty()) { + dl->addValidator(new Net::ChecksumValidator(QCryptographicHash::Sha1, file.hash)); + } + if (file.isExec) { + connect(dl.get(), &Net::Download::succeeded, + [file] { QFile(file.path).setPermissions(QFile(file.path).permissions() | QFileDevice::Permissions(0x1111)); }); + } + elementDownload->addNetAction(dl); + } + + connect(elementDownload.get(), &Task::failed, this, &ManifestDownloadTask::emitFailed); + connect(elementDownload.get(), &Task::progress, this, &ManifestDownloadTask::setProgress); + connect(elementDownload.get(), &Task::stepProgress, this, &ManifestDownloadTask::propagateStepProgress); + connect(elementDownload.get(), &Task::status, this, &ManifestDownloadTask::setStatus); + connect(elementDownload.get(), &Task::details, this, &ManifestDownloadTask::setDetails); + + connect(elementDownload.get(), &Task::succeeded, this, &ManifestDownloadTask::emitSucceeded); + m_task = elementDownload; + m_task->start(); +} + +bool ManifestDownloadTask::abort() +{ + auto aborted = canAbort(); + if (m_task) + aborted = m_task->abort(); + emitAborted(); + return aborted; +}; +} // namespace Java \ No newline at end of file diff --git a/launcher/java/download/ManifestDownloadTask.h b/launcher/java/download/ManifestDownloadTask.h new file mode 100644 index 0000000000..ae9e0d0edc --- /dev/null +++ b/launcher/java/download/ManifestDownloadTask.h @@ -0,0 +1,46 @@ +// SPDX-License-Identifier: GPL-3.0-only +/* + * Prism Launcher - Minecraft Launcher + * Copyright (c) 2023-2024 Trial97 + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, version 3. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +#pragma once + +#include +#include "tasks/Task.h" + +namespace Java { + +class ManifestDownloadTask : public Task { + Q_OBJECT + public: + ManifestDownloadTask(QUrl url, QString final_path, QString checksumType = "", QString checksumHash = ""); + virtual ~ManifestDownloadTask() = default; + + [[nodiscard]] bool canAbort() const override { return true; } + void executeTask() override; + virtual bool abort() override; + + private slots: + void downloadJava(const QJsonDocument& doc); + + protected: + QUrl m_url; + QString m_final_path; + QString m_checksum_type; + QString m_checksum_hash; + Task::Ptr m_task; +}; +} // namespace Java \ No newline at end of file diff --git a/launcher/launch/steps/CheckJava.cpp b/launcher/launch/steps/CheckJava.cpp index 81337a88e2..b5b85563ae 100644 --- a/launcher/launch/steps/CheckJava.cpp +++ b/launcher/launch/steps/CheckJava.cpp @@ -37,6 +37,7 @@ #include #include #include +#include #include #include #include "java/JavaUtils.h" @@ -46,7 +47,7 @@ void CheckJava::executeTask() auto instance = m_parent->instance(); auto settings = instance->settings(); m_javaPath = FS::ResolveExecutable(settings->get("JavaPath").toString()); - bool perInstance = settings->get("OverrideJava").toBool() || settings->get("OverrideJavaLocation").toBool(); + bool perInstance = settings->get("OverrideJavaLocation").toBool(); auto realJavaPath = QStandardPaths::findExecutable(m_javaPath); if (realJavaPath.isEmpty()) { @@ -90,11 +91,10 @@ void CheckJava::executeTask() // if timestamps are not the same, or something is missing, check! if (m_javaSignature != storedSignature || storedVersion.size() == 0 || storedArchitecture.size() == 0 || storedRealArchitecture.size() == 0 || storedVendor.size() == 0) { - m_JavaChecker.reset(new JavaChecker); + m_JavaChecker.reset(new JavaChecker(realJavaPath, "", 0, 0, 0, 0, this)); emit logLine(QString("Checking Java version..."), MessageLevel::Launcher); connect(m_JavaChecker.get(), &JavaChecker::checkFinished, this, &CheckJava::checkJavaFinished); - m_JavaChecker->m_path = realJavaPath; - m_JavaChecker->performCheck(); + m_JavaChecker->start(); return; } else { auto verString = instance->settings()->get("JavaVersion").toString(); @@ -106,10 +106,10 @@ void CheckJava::executeTask() emitSucceeded(); } -void CheckJava::checkJavaFinished(JavaCheckResult result) +void CheckJava::checkJavaFinished(const JavaChecker::Result& result) { switch (result.validity) { - case JavaCheckResult::Validity::Errored: { + case JavaChecker::Result::Validity::Errored: { // Error message displayed if java can't start emit logLine(QString("Could not start java:"), MessageLevel::Error); emit logLines(result.errorLog.split('\n'), MessageLevel::Error); @@ -117,14 +117,14 @@ void CheckJava::checkJavaFinished(JavaCheckResult result) emitFailed(QString("Could not start java!")); return; } - case JavaCheckResult::Validity::ReturnedInvalidData: { + case JavaChecker::Result::Validity::ReturnedInvalidData: { emit logLine(QString("Java checker returned some invalid data we don't understand:"), MessageLevel::Error); emit logLines(result.outLog.split('\n'), MessageLevel::Warning); emit logLine("\nMinecraft might not start properly.", MessageLevel::Launcher); emitSucceeded(); return; } - case JavaCheckResult::Validity::Valid: { + case JavaChecker::Result::Validity::Valid: { auto instance = m_parent->instance(); printJavaInfo(result.javaVersion.toString(), result.mojangPlatform, result.realPlatform, result.javaVendor); instance->settings()->set("JavaVersion", result.javaVersion.toString()); diff --git a/launcher/launch/steps/CheckJava.h b/launcher/launch/steps/CheckJava.h index ac9c362443..7bb1ceb7e7 100644 --- a/launcher/launch/steps/CheckJava.h +++ b/launcher/launch/steps/CheckJava.h @@ -28,7 +28,7 @@ class CheckJava : public LaunchStep { virtual void executeTask(); virtual bool canAbort() const { return false; } private slots: - void checkJavaFinished(JavaCheckResult result); + void checkJavaFinished(const JavaChecker::Result& result); private: void printJavaInfo(const QString& version, const QString& architecture, const QString& realArchitecture, const QString& vendor); @@ -37,5 +37,5 @@ class CheckJava : public LaunchStep { private: QString m_javaPath; QString m_javaSignature; - JavaCheckerPtr m_JavaChecker; + JavaChecker::Ptr m_JavaChecker; }; diff --git a/launcher/meta/VersionList.cpp b/launcher/meta/VersionList.cpp index 7b7ae1fa32..66412d6a40 100644 --- a/launcher/meta/VersionList.cpp +++ b/launcher/meta/VersionList.cpp @@ -92,6 +92,13 @@ QVariant VersionList::data(const QModelIndex& index, int role) const return QVariant::fromValue(version); case RecommendedRole: return version->isRecommended(); + case JavaMajorRole: { + auto major = version->version(); + if (major.startsWith("java")) { + major = "Java " + major.mid(4); + } + return major; + } // FIXME: this should be determined in whatever view/proxy is used... // case LatestRole: return version == getLatestStable(); default: @@ -101,10 +108,14 @@ QVariant VersionList::data(const QModelIndex& index, int role) const BaseVersionList::RoleList VersionList::providesRoles() const { - return { VersionPointerRole, VersionRole, VersionIdRole, ParentVersionRole, TypeRole, UidRole, - TimeRole, RequiresRole, SortRole, RecommendedRole, LatestRole, VersionPtrRole }; + return m_provided_roles; } +void VersionList::setProvidedRoles(RoleList roles) +{ + m_provided_roles = roles; +}; + QHash VersionList::roleNames() const { QHash roles = BaseVersionList::roleNames(); diff --git a/launcher/meta/VersionList.h b/launcher/meta/VersionList.h index 2c5624701b..0890caf60a 100644 --- a/launcher/meta/VersionList.h +++ b/launcher/meta/VersionList.h @@ -47,6 +47,8 @@ class VersionList : public BaseVersionList, public BaseEntity { RoleList providesRoles() const override; QHash roleNames() const override; + void setProvidedRoles(RoleList roles); + QString localFilename() const override; QString uid() const { return m_uid; } @@ -79,6 +81,9 @@ class VersionList : public BaseVersionList, public BaseEntity { Version::Ptr m_recommended; + RoleList m_provided_roles = { VersionPointerRole, VersionRole, VersionIdRole, ParentVersionRole, TypeRole, UidRole, + TimeRole, RequiresRole, SortRole, RecommendedRole, LatestRole, VersionPtrRole }; + void setupAddedVersion(int row, const Version::Ptr& version); }; } // namespace Meta diff --git a/launcher/minecraft/LaunchProfile.cpp b/launcher/minecraft/LaunchProfile.cpp index cf819b411c..4687988506 100644 --- a/launcher/minecraft/LaunchProfile.cpp +++ b/launcher/minecraft/LaunchProfile.cpp @@ -164,6 +164,11 @@ void LaunchProfile::applyCompatibleJavaMajors(QList& javaMajor) { m_compatibleJavaMajors.append(javaMajor); } +void LaunchProfile::applyCompatibleJavaName(QString javaName) +{ + if (!javaName.isEmpty()) + m_compatibleJavaName = javaName; +} void LaunchProfile::applyLibrary(LibraryPtr library, const RuntimeContext& runtimeContext) { @@ -334,6 +339,11 @@ const QList& LaunchProfile::getCompatibleJavaMajors() const return m_compatibleJavaMajors; } +const QString LaunchProfile::getCompatibleJavaName() const +{ + return m_compatibleJavaName; +} + void LaunchProfile::getLibraryFiles(const RuntimeContext& runtimeContext, QStringList& jars, QStringList& nativeJars, diff --git a/launcher/minecraft/LaunchProfile.h b/launcher/minecraft/LaunchProfile.h index 12b312383a..f1be6fee0b 100644 --- a/launcher/minecraft/LaunchProfile.h +++ b/launcher/minecraft/LaunchProfile.h @@ -59,6 +59,7 @@ class LaunchProfile : public ProblemProvider { void applyMavenFile(LibraryPtr library, const RuntimeContext& runtimeContext); void applyAgent(AgentPtr agent, const RuntimeContext& runtimeContext); void applyCompatibleJavaMajors(QList& javaMajor); + void applyCompatibleJavaName(QString javaName); void applyMainJar(LibraryPtr jar); void applyProblemSeverity(ProblemSeverity severity); /// clear the profile @@ -80,6 +81,7 @@ class LaunchProfile : public ProblemProvider { const QList& getMavenFiles() const; const QList& getAgents() const; const QList& getCompatibleJavaMajors() const; + const QString getCompatibleJavaName() const; const LibraryPtr getMainJar() const; void getLibraryFiles(const RuntimeContext& runtimeContext, QStringList& jars, @@ -150,5 +152,7 @@ class LaunchProfile : public ProblemProvider { /// compatible java major versions QList m_compatibleJavaMajors; + QString m_compatibleJavaName; + ProblemSeverity m_problemSeverity = ProblemSeverity::None; }; diff --git a/launcher/minecraft/MinecraftInstance.cpp b/launcher/minecraft/MinecraftInstance.cpp index d119104fe7..28e0281d04 100644 --- a/launcher/minecraft/MinecraftInstance.cpp +++ b/launcher/minecraft/MinecraftInstance.cpp @@ -38,6 +38,8 @@ #include "MinecraftInstance.h" #include "Application.h" #include "BuildConfig.h" +#include "QObjectPtr.h" +#include "minecraft/launch/AutoInstallJava.h" #include "minecraft/launch/CreateGameFolders.h" #include "minecraft/launch/ExtractNatives.h" #include "minecraft/launch/PrintInstanceInfo.h" @@ -134,25 +136,20 @@ void MinecraftInstance::loadSpecificSettings() return; // Java Settings - auto javaOverride = m_settings->registerSetting("OverrideJava", false); auto locationOverride = m_settings->registerSetting("OverrideJavaLocation", false); auto argsOverride = m_settings->registerSetting("OverrideJavaArgs", false); - // combinations - auto javaOrLocation = std::make_shared("JavaOrLocationOverride", javaOverride, locationOverride); - auto javaOrArgs = std::make_shared("JavaOrArgsOverride", javaOverride, argsOverride); - if (auto global_settings = globalSettings()) { - m_settings->registerOverride(global_settings->getSetting("JavaPath"), javaOrLocation); - m_settings->registerOverride(global_settings->getSetting("JvmArgs"), javaOrArgs); - m_settings->registerOverride(global_settings->getSetting("IgnoreJavaCompatibility"), javaOrLocation); + m_settings->registerOverride(global_settings->getSetting("JavaPath"), locationOverride); + m_settings->registerOverride(global_settings->getSetting("JvmArgs"), argsOverride); + m_settings->registerOverride(global_settings->getSetting("IgnoreJavaCompatibility"), locationOverride); // special! - m_settings->registerPassthrough(global_settings->getSetting("JavaSignature"), javaOrLocation); - m_settings->registerPassthrough(global_settings->getSetting("JavaArchitecture"), javaOrLocation); - m_settings->registerPassthrough(global_settings->getSetting("JavaRealArchitecture"), javaOrLocation); - m_settings->registerPassthrough(global_settings->getSetting("JavaVersion"), javaOrLocation); - m_settings->registerPassthrough(global_settings->getSetting("JavaVendor"), javaOrLocation); + m_settings->registerPassthrough(global_settings->getSetting("JavaSignature"), locationOverride); + m_settings->registerPassthrough(global_settings->getSetting("JavaArchitecture"), locationOverride); + m_settings->registerPassthrough(global_settings->getSetting("JavaRealArchitecture"), locationOverride); + m_settings->registerPassthrough(global_settings->getSetting("JavaVersion"), locationOverride); + m_settings->registerPassthrough(global_settings->getSetting("JavaVendor"), locationOverride); // Window Size auto windowSetting = m_settings->registerSetting("OverrideWindow", false); @@ -1048,11 +1045,6 @@ shared_qobject_ptr MinecraftInstance::createLaunchTask(AuthSessionPt process->appendStep(makeShared(pptr, "Minecraft folder is:\n" + gameRoot() + "\n\n", MessageLevel::Launcher)); } - // check java - { - process->appendStep(makeShared(pptr)); - } - // create the .minecraft folder and server-resource-packs (workaround for Minecraft bug MCL-3732) { process->appendStep(makeShared(pptr)); @@ -1088,6 +1080,12 @@ shared_qobject_ptr MinecraftInstance::createLaunchTask(AuthSessionPt process->appendStep(makeShared(pptr, Net::Mode::Offline)); } + // check java + { + process->appendStep(makeShared(pptr)); + process->appendStep(makeShared(pptr)); + } + // if there are any jar mods { process->appendStep(makeShared(pptr)); diff --git a/launcher/minecraft/MojangVersionFormat.cpp b/launcher/minecraft/MojangVersionFormat.cpp index bb782e47fe..d17a3a21f0 100644 --- a/launcher/minecraft/MojangVersionFormat.cpp +++ b/launcher/minecraft/MojangVersionFormat.cpp @@ -185,6 +185,9 @@ void MojangVersionFormat::readVersionProperties(const QJsonObject& in, VersionFi out->compatibleJavaMajors.append(requireInteger(compatible)); } } + if (in.contains("compatibleJavaName")) { + out->compatibleJavaName = requireString(in.value("compatibleJavaName")); + } if (in.contains("downloads")) { auto downloadsObj = requireObject(in, "downloads"); @@ -259,6 +262,9 @@ void MojangVersionFormat::writeVersionProperties(const VersionFile* in, QJsonObj } out.insert("compatibleJavaMajors", compatibleJavaMajorsOut); } + if (!in->compatibleJavaName.isEmpty()) { + writeString(out, "compatibleJavaName", in->compatibleJavaName); + } } QJsonDocument MojangVersionFormat::versionFileToJson(const VersionFilePtr& patch) diff --git a/launcher/minecraft/OneSixVersionFormat.cpp b/launcher/minecraft/OneSixVersionFormat.cpp index 306c95a6ae..bd587beb27 100644 --- a/launcher/minecraft/OneSixVersionFormat.cpp +++ b/launcher/minecraft/OneSixVersionFormat.cpp @@ -36,6 +36,8 @@ #include "OneSixVersionFormat.h" #include #include +#include +#include "java/JavaMetadata.h" #include "minecraft/Agent.h" #include "minecraft/ParseUtils.h" @@ -255,6 +257,13 @@ VersionFilePtr OneSixVersionFormat::versionFileFromJson(const QJsonDocument& doc out->m_volatile = requireBoolean(root, "volatile"); } + if (root.contains("runtimes")) { + out->runtimes = {}; + for (auto runtime : ensureArray(root, "runtimes")) { + out->runtimes.append(Java::parseJavaMeta(ensureObject(runtime))); + } + } + /* removed features that shouldn't be used */ if (root.contains("tweakers")) { out->addProblem(ProblemSeverity::Error, QObject::tr("Version file contains unsupported element 'tweakers'")); diff --git a/launcher/minecraft/VersionFile.cpp b/launcher/minecraft/VersionFile.cpp index 6632bb8bf6..8ee61128f4 100644 --- a/launcher/minecraft/VersionFile.cpp +++ b/launcher/minecraft/VersionFile.cpp @@ -73,6 +73,7 @@ void VersionFile::applyTo(LaunchProfile* profile, const RuntimeContext& runtimeC profile->applyMods(mods); profile->applyTraits(traits); profile->applyCompatibleJavaMajors(compatibleJavaMajors); + profile->applyCompatibleJavaName(compatibleJavaName); for (auto library : libraries) { profile->applyLibrary(library, runtimeContext); diff --git a/launcher/minecraft/VersionFile.h b/launcher/minecraft/VersionFile.h index 280e35ee34..85ac554267 100644 --- a/launcher/minecraft/VersionFile.h +++ b/launcher/minecraft/VersionFile.h @@ -36,6 +36,8 @@ #pragma once #include +#include +#include #include #include #include @@ -45,6 +47,7 @@ #include "Agent.h" #include "Library.h" #include "ProblemProvider.h" +#include "java/JavaMetadata.h" #include "minecraft/Rule.h" class PackProfile; @@ -98,6 +101,9 @@ class VersionFile : public ProblemContainer { /// Mojang: list of compatible java majors QList compatibleJavaMajors; + /// Mojang: the name of recomended java version + QString compatibleJavaName; + /// Mojang: type of the Minecraft version QString type; @@ -149,6 +155,8 @@ class VersionFile : public ProblemContainer { /// is volatile -- may be removed as soon as it is no longer needed by something else bool m_volatile = false; + QList runtimes; + public: // Mojang: DEPRECATED list of 'downloads' - client jar, server jar, windows server exe, maybe more. QMap> mojangDownloads; diff --git a/launcher/minecraft/launch/AutoInstallJava.cpp b/launcher/minecraft/launch/AutoInstallJava.cpp new file mode 100644 index 0000000000..eefed9cf55 --- /dev/null +++ b/launcher/minecraft/launch/AutoInstallJava.cpp @@ -0,0 +1,235 @@ +// SPDX-License-Identifier: GPL-3.0-only +/* + * Prism Launcher - Minecraft Launcher + * Copyright (c) 2023-2024 Trial97 + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, version 3. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + * + * This file incorporates work covered by the following copyright and + * permission notice: + * + * Copyright 2013-2021 MultiMC Contributors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#include "AutoInstallJava.h" +#include +#include +#include + +#include "Application.h" +#include "FileSystem.h" +#include "MessageLevel.h" +#include "SysInfo.h" +#include "java/JavaInstall.h" +#include "java/JavaInstallList.h" +#include "java/JavaUtils.h" +#include "java/JavaVersion.h" +#include "java/download/ArchiveDownloadTask.h" +#include "java/download/ManifestDownloadTask.h" +#include "meta/Index.h" +#include "minecraft/MinecraftInstance.h" +#include "minecraft/PackProfile.h" +#include "net/Mode.h" + +AutoInstallJava::AutoInstallJava(LaunchTask* parent) + : LaunchStep(parent) + , m_instance(std::dynamic_pointer_cast(m_parent->instance())) + , m_supported_arch(SysInfo::getSupportedJavaArchitecture()) {}; + +void AutoInstallJava::executeTask() +{ + auto settings = m_instance->settings(); + if (!APPLICATION->settings()->get("AutomaticJavaSwitch").toBool() || + (settings->get("OverrideJavaLocation").toBool() && QFileInfo::exists(settings->get("JavaPath").toString()))) { + emitSucceeded(); + return; + } + auto packProfile = m_instance->getPackProfile(); + if (!APPLICATION->settings()->get("AutomaticJavaDownload").toBool()) { + auto javas = APPLICATION->javalist(); + m_current_task = javas->getLoadTask(); + connect(m_current_task.get(), &Task::finished, this, [this, javas, packProfile] { + for (auto i = 0; i < javas->count(); i++) { + auto java = std::dynamic_pointer_cast(javas->at(i)); + if (java && packProfile->getProfile()->getCompatibleJavaMajors().contains(java->id.major())) { + if (!java->is_64bit) { + emit logLine(tr("The automatic Java mechanism detected a 32-bit installation of Java."), MessageLevel::Info); + } + setJavaPath(java->path); + return; + } + } + emit logLine(tr("No compatible Java version was found. Using the default one."), MessageLevel::Warning); + emitSucceeded(); + }); + connect(m_current_task.get(), &Task::progress, this, &AutoInstallJava::setProgress); + connect(m_current_task.get(), &Task::stepProgress, this, &AutoInstallJava::propagateStepProgress); + connect(m_current_task.get(), &Task::status, this, &AutoInstallJava::setStatus); + connect(m_current_task.get(), &Task::details, this, &AutoInstallJava::setDetails); + emit progressReportingRequest(); + return; + } + if (m_supported_arch.isEmpty()) { + emit logLine(tr("Your system (%1-%2) is not compatible with automatic Java installation. Using the default Java path.") + .arg(SysInfo::currentSystem(), SysInfo::useQTForArch()), + MessageLevel::Warning); + emitSucceeded(); + return; + } + auto wantedJavaName = packProfile->getProfile()->getCompatibleJavaName(); + if (wantedJavaName.isEmpty()) { + emit logLine(tr("Your meta information is out of date or doesn't have the information necessary to determine what installation of " + "Java should be used. " + "Using the default Java path."), + MessageLevel::Warning); + emitSucceeded(); + return; + } + QDir javaDir(APPLICATION->javaPath()); + auto wantedJavaPath = javaDir.absoluteFilePath(wantedJavaName); + if (QFileInfo::exists(wantedJavaPath)) { + setJavaPathFromPartial(); + return; + } + auto versionList = APPLICATION->metadataIndex()->get("net.minecraft.java"); + m_current_task = versionList->getLoadTask(); + connect(m_current_task.get(), &Task::succeeded, this, &AutoInstallJava::tryNextMajorJava); + connect(m_current_task.get(), &Task::failed, this, &AutoInstallJava::emitFailed); + connect(m_current_task.get(), &Task::progress, this, &AutoInstallJava::setProgress); + connect(m_current_task.get(), &Task::stepProgress, this, &AutoInstallJava::propagateStepProgress); + connect(m_current_task.get(), &Task::status, this, &AutoInstallJava::setStatus); + connect(m_current_task.get(), &Task::details, this, &AutoInstallJava::setDetails); + emit progressReportingRequest(); +} + +void AutoInstallJava::setJavaPath(QString path) +{ + auto settings = m_instance->settings(); + settings->set("OverrideJavaLocation", true); + settings->set("JavaPath", path); + emit logLine(tr("Compatible Java found at: %1.").arg(path), MessageLevel::Info); + emitSucceeded(); +} + +void AutoInstallJava::setJavaPathFromPartial() +{ + auto packProfile = m_instance->getPackProfile(); + auto javaName = packProfile->getProfile()->getCompatibleJavaName(); + QDir javaDir(APPLICATION->javaPath()); + // just checking if the executable is there should suffice + // but if needed this can be achieved through refreshing the javalist + // and retrieving the path that contains the java name + auto relativeBinary = FS::PathCombine(javaName, "bin", JavaUtils::javaExecutable); + auto finalPath = javaDir.absoluteFilePath(relativeBinary); + if (QFileInfo::exists(finalPath)) { + setJavaPath(finalPath); + } else { + emit logLine(tr("No compatible Java version was found (the binary file does not exist). Using the default one."), + MessageLevel::Warning); + emitSucceeded(); + } + return; +} + +void AutoInstallJava::downloadJava(Meta::Version::Ptr version, QString javaName) +{ + auto runtimes = version->data()->runtimes; + for (auto java : runtimes) { + if (java->runtimeOS == m_supported_arch && java->name() == javaName) { + QDir javaDir(APPLICATION->javaPath()); + auto final_path = javaDir.absoluteFilePath(java->m_name); + switch (java->downloadType) { + case Java::DownloadType::Manifest: + m_current_task = makeShared(java->url, final_path, java->checksumType, java->checksumHash); + break; + case Java::DownloadType::Archive: + m_current_task = makeShared(java->url, final_path, java->checksumType, java->checksumHash); + break; + case Java::DownloadType::Unknown: + emitFailed(tr("Could not determine Java download type!")); + return; + } + auto deletePath = [final_path] { FS::deletePath(final_path); }; + connect(m_current_task.get(), &Task::failed, this, [this, deletePath](QString reason) { + deletePath(); + emitFailed(reason); + }); + connect(this, &Task::aborted, this, [this, deletePath] { + m_current_task->abort(); + deletePath(); + }); + connect(m_current_task.get(), &Task::succeeded, this, &AutoInstallJava::setJavaPathFromPartial); + connect(m_current_task.get(), &Task::failed, this, &AutoInstallJava::tryNextMajorJava); + connect(m_current_task.get(), &Task::progress, this, &AutoInstallJava::setProgress); + connect(m_current_task.get(), &Task::stepProgress, this, &AutoInstallJava::propagateStepProgress); + connect(m_current_task.get(), &Task::status, this, &AutoInstallJava::setStatus); + connect(m_current_task.get(), &Task::details, this, &AutoInstallJava::setDetails); + m_current_task->start(); + return; + } + } + tryNextMajorJava(); +} + +void AutoInstallJava::tryNextMajorJava() +{ + if (!isRunning()) + return; + auto versionList = APPLICATION->metadataIndex()->get("net.minecraft.java"); + auto packProfile = m_instance->getPackProfile(); + auto wantedJavaName = packProfile->getProfile()->getCompatibleJavaName(); + auto majorJavaVersions = packProfile->getProfile()->getCompatibleJavaMajors(); + if (m_majorJavaVersionIndex >= majorJavaVersions.length()) { + emit logLine( + tr("No versions of Java were found for your operating system: %1-%2").arg(SysInfo::currentSystem(), SysInfo::useQTForArch()), + MessageLevel::Warning); + emit logLine(tr("No compatible version of Java was found. Using the default one."), MessageLevel::Warning); + emitSucceeded(); + return; + } + auto majorJavaVersion = majorJavaVersions[m_majorJavaVersionIndex]; + m_majorJavaVersionIndex++; + + auto javaMajor = versionList->getVersion(QString("java%1").arg(majorJavaVersion)); + javaMajor->load(Net::Mode::Online); + m_current_task = javaMajor->getCurrentTask(); + if (javaMajor->isLoaded() || !m_current_task) { + downloadJava(javaMajor, wantedJavaName); + } else { + connect(m_current_task.get(), &Task::succeeded, this, + [this, javaMajor, wantedJavaName] { downloadJava(javaMajor, wantedJavaName); }); + connect(m_current_task.get(), &Task::failed, this, &AutoInstallJava::tryNextMajorJava); + connect(m_current_task.get(), &Task::progress, this, &AutoInstallJava::setProgress); + connect(m_current_task.get(), &Task::stepProgress, this, &AutoInstallJava::propagateStepProgress); + connect(m_current_task.get(), &Task::status, this, &AutoInstallJava::setStatus); + connect(m_current_task.get(), &Task::details, this, &AutoInstallJava::setDetails); + } +} +bool AutoInstallJava::abort() +{ + if (m_current_task && m_current_task->canAbort()) + return m_current_task->abort(); + return true; +} diff --git a/launcher/minecraft/launch/AutoInstallJava.h b/launcher/minecraft/launch/AutoInstallJava.h new file mode 100644 index 0000000000..7e4efc50cf --- /dev/null +++ b/launcher/minecraft/launch/AutoInstallJava.h @@ -0,0 +1,68 @@ +// SPDX-License-Identifier: GPL-3.0-only +/* + * Prism Launcher - Minecraft Launcher + * Copyright (c) 2023-2024 Trial97 + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, version 3. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + * + * This file incorporates work covered by the following copyright and + * permission notice: + * + * Copyright 2013-2021 MultiMC Contributors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#pragma once + +#include +#include +#include "java/JavaMetadata.h" +#include "meta/Version.h" +#include "minecraft/MinecraftInstance.h" +#include "tasks/Task.h" + +class AutoInstallJava : public LaunchStep { + Q_OBJECT + + public: + explicit AutoInstallJava(LaunchTask* parent); + ~AutoInstallJava() override = default; + + void executeTask() override; + bool canAbort() const override { return m_current_task ? m_current_task->canAbort() : false; } + bool abort() override; + + protected: + void setJavaPath(QString path); + void setJavaPathFromPartial(); + void downloadJava(Meta::Version::Ptr version, QString javaName); + void tryNextMajorJava(); + + private: + MinecraftInstancePtr m_instance; + Task::Ptr m_current_task; + + qsizetype m_majorJavaVersionIndex = 0; + const QString m_supported_arch; +}; diff --git a/launcher/minecraft/launch/VerifyJavaInstall.cpp b/launcher/minecraft/launch/VerifyJavaInstall.cpp index cdd1f7fd1f..1e74480897 100644 --- a/launcher/minecraft/launch/VerifyJavaInstall.cpp +++ b/launcher/minecraft/launch/VerifyJavaInstall.cpp @@ -34,7 +34,12 @@ */ #include "VerifyJavaInstall.h" +#include +#include "Application.h" +#include "MessageLevel.h" +#include "java/JavaInstall.h" +#include "java/JavaInstallList.h" #include "java/JavaVersion.h" #include "minecraft/MinecraftInstance.h" #include "minecraft/PackProfile.h" @@ -46,6 +51,15 @@ void VerifyJavaInstall::executeTask() auto settings = instance->settings(); auto storedVersion = settings->get("JavaVersion").toString(); auto ignoreCompatibility = settings->get("IgnoreJavaCompatibility").toBool(); + auto javaArchitecture = settings->get("JavaArchitecture").toString(); + auto maxMemAlloc = settings->get("MaxMemAlloc").toInt(); + + if (javaArchitecture == "32" && maxMemAlloc > 2048) { + emit logLine(tr("Max memory allocation exceeds the supported value.\n" + "The selected installation of Java is 32-bit and doesn't support more than 2048MiB of RAM.\n" + "The instance may not start due to this."), + MessageLevel::Error); + } auto compatibleMajors = packProfile->getProfile()->getCompatibleJavaMajors(); diff --git a/launcher/ui/dialogs/VersionSelectDialog.h b/launcher/ui/dialogs/VersionSelectDialog.h index 34c5e66f8b..94d70beb0a 100644 --- a/launcher/ui/dialogs/VersionSelectDialog.h +++ b/launcher/ui/dialogs/VersionSelectDialog.h @@ -26,10 +26,6 @@ class QDialogButtonBox; class VersionSelectWidget; class QPushButton; -namespace Ui { -class VersionSelectDialog; -} - class VersionProxyModel; class VersionSelectDialog : public QDialog { diff --git a/launcher/ui/java/InstallJavaDialog.cpp b/launcher/ui/java/InstallJavaDialog.cpp new file mode 100644 index 0000000000..1a4b4cc588 --- /dev/null +++ b/launcher/ui/java/InstallJavaDialog.cpp @@ -0,0 +1,248 @@ +// SPDX-License-Identifier: GPL-3.0-only +/* + * Prism Launcher - Minecraft Launcher + * Copyright (c) 2024 Trial97 + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, version 3. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +#include "InstallJavaDialog.h" + +#include +#include +#include +#include +#include + +#include "Application.h" +#include "BaseVersionList.h" +#include "FileSystem.h" +#include "java/download/ArchiveDownloadTask.h" +#include "java/download/ManifestDownloadTask.h" +#include "meta/Index.h" +#include "meta/VersionList.h" +#include "ui/dialogs/CustomMessageBox.h" +#include "ui/dialogs/ProgressDialog.h" +#include "ui/java/VersionList.h" +#include "ui/widgets/PageContainer.h" +#include "ui/widgets/VersionSelectWidget.h" + +class InstallJavaPage : public QWidget, public BasePage { + public: + Q_OBJECT + public: + explicit InstallJavaPage(const QString& id, const QString& iconName, const QString& name, QWidget* parent = nullptr) + : QWidget(parent), uid(id), iconName(iconName), name(name) + { + setObjectName(QStringLiteral("VersionSelectWidget")); + horizontalLayout = new QHBoxLayout(this); + horizontalLayout->setObjectName(QStringLiteral("horizontalLayout")); + horizontalLayout->setContentsMargins(0, 0, 0, 0); + + majorVersionSelect = new VersionSelectWidget(this); + majorVersionSelect->selectCurrent(); + majorVersionSelect->setEmptyString(tr("No java versions are currently available in the meta.")); + majorVersionSelect->setEmptyErrorString(tr("Couldn't load or download the java version lists!")); + horizontalLayout->addWidget(majorVersionSelect, 1); + + javaVersionSelect = new VersionSelectWidget(this); + javaVersionSelect->setEmptyString(tr("No java versions are currently available for your OS.")); + javaVersionSelect->setEmptyErrorString(tr("Couldn't load or download the java version lists!")); + horizontalLayout->addWidget(javaVersionSelect, 4); + connect(majorVersionSelect, &VersionSelectWidget::selectedVersionChanged, this, &InstallJavaPage::setSelectedVersion); + connect(majorVersionSelect, &VersionSelectWidget::selectedVersionChanged, this, &InstallJavaPage::selectionChanged); + connect(javaVersionSelect, &VersionSelectWidget::selectedVersionChanged, this, &InstallJavaPage::selectionChanged); + + QMetaObject::connectSlotsByName(this); + } + ~InstallJavaPage() + { + delete horizontalLayout; + delete majorVersionSelect; + delete javaVersionSelect; + } + + //! loads the list if needed. + void initialize(Meta::VersionList::Ptr vlist) + { + vlist->setProvidedRoles({ BaseVersionList::JavaMajorRole, BaseVersionList::RecommendedRole, BaseVersionList::VersionPointerRole }); + majorVersionSelect->initialize(vlist.get()); + } + + void setSelectedVersion(BaseVersion::Ptr version) + { + auto dcast = std::dynamic_pointer_cast(version); + if (!dcast) { + return; + } + javaVersionSelect->initialize(new Java::VersionList(dcast, this)); + javaVersionSelect->selectCurrent(); + } + + QString id() const override { return uid; } + QString displayName() const override { return name; } + QIcon icon() const override { return APPLICATION->getThemedIcon(iconName); } + + void openedImpl() override + { + if (loaded) + return; + + const auto versions = APPLICATION->metadataIndex()->get(uid); + if (!versions) + return; + + initialize(versions); + loaded = true; + } + + void setParentContainer(BasePageContainer* container) override + { + auto dialog = dynamic_cast(dynamic_cast(container)->parent()); + connect(javaVersionSelect->view(), &QAbstractItemView::doubleClicked, dialog, &QDialog::accept); + } + + BaseVersion::Ptr selectedVersion() const { return javaVersionSelect->selectedVersion(); } + void selectSearch() { javaVersionSelect->selectSearch(); } + void loadList() + { + majorVersionSelect->loadList(); + javaVersionSelect->loadList(); + } + signals: + void selectionChanged(); + + private: + const QString uid; + const QString iconName; + const QString name; + bool loaded = false; + + QHBoxLayout* horizontalLayout = nullptr; + VersionSelectWidget* majorVersionSelect = nullptr; + VersionSelectWidget* javaVersionSelect = nullptr; +}; + +static InstallJavaPage* pageCast(BasePage* page) +{ + auto result = dynamic_cast(page); + Q_ASSERT(result != nullptr); + return result; +} +namespace Java { + +InstallDialog::InstallDialog(const QString& uid, QWidget* parent) + : QDialog(parent), container(new PageContainer(this, QString(), this)), buttons(new QDialogButtonBox(this)) +{ + auto layout = new QVBoxLayout(this); + + container->setSizePolicy(QSizePolicy::Preferred, QSizePolicy::Expanding); + layout->addWidget(container); + + auto buttonLayout = new QHBoxLayout(this); + + auto refreshButton = new QPushButton(tr("&Refresh"), this); + connect(refreshButton, &QPushButton::clicked, this, [this] { pageCast(container->selectedPage())->loadList(); }); + buttonLayout->addWidget(refreshButton); + + buttons->setOrientation(Qt::Horizontal); + buttons->setStandardButtons(QDialogButtonBox::Cancel | QDialogButtonBox::Ok); + buttons->button(QDialogButtonBox::Ok)->setText(tr("Download")); + connect(buttons, &QDialogButtonBox::accepted, this, &QDialog::accept); + connect(buttons, &QDialogButtonBox::rejected, this, &QDialog::reject); + buttonLayout->addWidget(buttons); + + layout->addLayout(buttonLayout); + + setWindowTitle(dialogTitle()); + setWindowModality(Qt::WindowModal); + resize(840, 480); + + for (BasePage* page : container->getPages()) { + if (page->id() == uid) + container->selectPage(page->id()); + + connect(pageCast(page), &InstallJavaPage::selectionChanged, this, [this] { validate(); }); + } + connect(container, &PageContainer::selectedPageChanged, this, [this] { validate(); }); + pageCast(container->selectedPage())->selectSearch(); + validate(); +} + +QList InstallDialog::getPages() +{ + return { + // Mojang + new InstallJavaPage("net.minecraft.java", "", tr("Mojang")), + // Adoptium + new InstallJavaPage("net.adoptium.java", "", tr("Adoptium")), + // Azul + new InstallJavaPage("com.azul.java", "", tr("Azul")), + }; +} + +QString InstallDialog::dialogTitle() +{ + return tr("Install Java"); +} + +void InstallDialog::validate() +{ + buttons->button(QDialogButtonBox::Ok) + ->setEnabled(!!std::dynamic_pointer_cast(pageCast(container->selectedPage())->selectedVersion())); +} + +void InstallDialog::done(int result) +{ + if (result == Accepted) { + auto* page = pageCast(container->selectedPage()); + if (page->selectedVersion()) { + auto meta = std::dynamic_pointer_cast(page->selectedVersion()); + if (meta) { + Task::Ptr task; + auto final_path = FS::PathCombine(APPLICATION->javaPath(), meta->m_name); + auto deletePath = [final_path] { FS::deletePath(final_path); }; + switch (meta->downloadType) { + case Java::DownloadType::Manifest: + task = makeShared(meta->url, final_path, meta->checksumType, meta->checksumHash); + break; + case Java::DownloadType::Archive: + task = makeShared(meta->url, final_path, meta->checksumType, meta->checksumHash); + break; + case Java::DownloadType::Unknown: + QString error = QString(tr("Could not determine Java download type!")); + CustomMessageBox::selectable(this, tr("Error"), error, QMessageBox::Warning)->show(); + deletePath(); + } + connect(task.get(), &Task::failed, this, [this, &deletePath](QString reason) { + QString error = QString("Java download failed: %1").arg(reason); + CustomMessageBox::selectable(this, tr("Error"), error, QMessageBox::Warning)->show(); + deletePath(); + }); + connect(task.get(), &Task::aborted, this, deletePath); + ProgressDialog pg(this); + pg.setSkipButton(true, tr("Abort")); + pg.execWithTask(task.get()); + } else { + return; + } + } else { + return; + } + } + + QDialog::done(result); +} +} // namespace Java + +#include "InstallJavaDialog.moc" \ No newline at end of file diff --git a/launcher/ui/java/InstallJavaDialog.h b/launcher/ui/java/InstallJavaDialog.h new file mode 100644 index 0000000000..80d010c1a0 --- /dev/null +++ b/launcher/ui/java/InstallJavaDialog.h @@ -0,0 +1,46 @@ +// SPDX-License-Identifier: GPL-3.0-only +/* + * Prism Launcher - Minecraft Launcher + * Copyright (c) 2024 Trial97 + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, version 3. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +#pragma once + +#include +#include "ui/pages/BasePageProvider.h" + +class MinecraftInstance; +class PageContainer; +class PackProfile; +class QDialogButtonBox; + +namespace Java { +class InstallDialog final : public QDialog, private BasePageProvider { + Q_OBJECT + + public: + explicit InstallDialog(const QString& uid = QString(), QWidget* parent = nullptr); + + QList getPages() override; + QString dialogTitle() override; + + void validate(); + void done(int result) override; + + private: + PageContainer* container; + QDialogButtonBox* buttons; +}; +} // namespace Java diff --git a/launcher/ui/java/VersionList.cpp b/launcher/ui/java/VersionList.cpp new file mode 100644 index 0000000000..6f60a68673 --- /dev/null +++ b/launcher/ui/java/VersionList.cpp @@ -0,0 +1,127 @@ +// SPDX-License-Identifier: GPL-3.0-only +/* + * Prism Launcher - Minecraft Launcher + * Copyright (c) 2024 Trial97 + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, version 3. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +#include "VersionList.h" + +#include + +#include "BaseVersionList.h" +#include "SysInfo.h" +#include "java/JavaMetadata.h" +#include "meta/VersionList.h" + +namespace Java { + +VersionList::VersionList(Meta::Version::Ptr version, QObject* parent) : BaseVersionList(parent), m_version(version) +{ + if (version->isLoaded()) + sortVersions(); +} + +Task::Ptr VersionList::getLoadTask() +{ + m_version->load(Net::Mode::Online); + auto task = m_version->getCurrentTask(); + connect(task.get(), &Task::finished, this, &VersionList::sortVersions); + return task; +} + +const BaseVersion::Ptr VersionList::at(int i) const +{ + return m_vlist.at(i); +} + +bool VersionList::isLoaded() +{ + return m_version->isLoaded(); +} + +int VersionList::count() const +{ + return m_vlist.count(); +} + +QVariant VersionList::data(const QModelIndex& index, int role) const +{ + if (!index.isValid()) + return QVariant(); + + if (index.row() > count()) + return QVariant(); + + auto version = (m_vlist[index.row()]); + switch (role) { + case SortRole: + return -index.row(); + case VersionPointerRole: + return QVariant::fromValue(std::dynamic_pointer_cast(m_vlist[index.row()])); + case VersionIdRole: + return version->descriptor(); + case VersionRole: + return version->version.toString(); + case RecommendedRole: + return false; // do not recommend any version + case JavaNameRole: + return version->name(); + case JavaMajorRole: { + auto major = version->version.toString(); + if (major.startsWith("java")) { + major = "Java " + major.mid(4); + } + return major; + } + case TypeRole: + return version->packageType; + case Meta::VersionList::TimeRole: + return version->releaseTime; + default: + return QVariant(); + } +} + +BaseVersionList::RoleList VersionList::providesRoles() const +{ + return { VersionPointerRole, VersionIdRole, VersionRole, RecommendedRole, JavaNameRole, TypeRole, Meta::VersionList::TimeRole }; +} + +bool sortJavas(BaseVersion::Ptr left, BaseVersion::Ptr right) +{ + auto rleft = std::dynamic_pointer_cast(right); + auto rright = std::dynamic_pointer_cast(left); + return (*rleft) < (*rright); +} + +void VersionList::sortVersions() +{ + if (!m_version || !m_version->data()) + return; + QString versionStr = SysInfo::getSupportedJavaArchitecture(); + beginResetModel(); + auto runtimes = m_version->data()->runtimes; + m_vlist = {}; + if (!versionStr.isEmpty() && !runtimes.isEmpty()) { + std::copy_if(runtimes.begin(), runtimes.end(), std::back_inserter(m_vlist), + [versionStr](Java::MetadataPtr val) { return val->runtimeOS == versionStr; }); + std::sort(m_vlist.begin(), m_vlist.end(), sortJavas); + } else { + qWarning() << "No Java versions found for your operating system." << SysInfo::currentSystem() << " " << SysInfo::useQTForArch(); + } + endResetModel(); +} + +} // namespace Java diff --git a/launcher/ui/java/VersionList.h b/launcher/ui/java/VersionList.h new file mode 100644 index 0000000000..d334ed5648 --- /dev/null +++ b/launcher/ui/java/VersionList.h @@ -0,0 +1,50 @@ +// SPDX-License-Identifier: GPL-3.0-only +/* + * Prism Launcher - Minecraft Launcher + * Copyright (c) 2024 Trial97 + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, version 3. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +#pragma once + +#include "BaseVersionList.h" +#include "java/JavaMetadata.h" +#include "meta/Version.h" + +namespace Java { + +class VersionList : public BaseVersionList { + Q_OBJECT + + public: + explicit VersionList(Meta::Version::Ptr m_version, QObject* parent = 0); + + Task::Ptr getLoadTask() override; + bool isLoaded() override; + const BaseVersion::Ptr at(int i) const override; + int count() const override; + void sortVersions() override; + + QVariant data(const QModelIndex& index, int role) const override; + RoleList providesRoles() const override; + + protected slots: + void updateListData(QList) override {} + + protected: + Meta::Version::Ptr m_version; + QList m_vlist; +}; + +} // namespace Java diff --git a/launcher/ui/pages/global/JavaPage.cpp b/launcher/ui/pages/global/JavaPage.cpp index ac50319ec1..ad37fa7408 100644 --- a/launcher/ui/pages/global/JavaPage.cpp +++ b/launcher/ui/pages/global/JavaPage.cpp @@ -35,12 +35,18 @@ */ #include "JavaPage.h" +#include "BuildConfig.h" #include "JavaCommon.h" +#include "java/JavaInstall.h" +#include "ui/dialogs/CustomMessageBox.h" +#include "ui/java/InstallJavaDialog.h" #include "ui_JavaPage.h" +#include #include #include #include +#include #include #include "ui/dialogs/VersionSelectDialog.h" @@ -56,7 +62,22 @@ JavaPage::JavaPage(QWidget* parent) : QWidget(parent), ui(new Ui::JavaPage) { ui->setupUi(this); - ui->tabWidget->tabBar()->hide(); + + if (BuildConfig.JAVA_DOWNLOADER_ENABLED) { + ui->managedJavaList->initialize(new JavaInstallList(this, true)); + ui->managedJavaList->setResizeOn(2); + ui->managedJavaList->selectCurrent(); + ui->managedJavaList->setEmptyString(tr("No managed java versions are installed")); + ui->managedJavaList->setEmptyErrorString(tr("Couldn't load the managed java list!")); + connect(ui->autodetectJavaCheckBox, &QCheckBox::stateChanged, this, [this] { + ui->autodownloadCheckBox->setEnabled(ui->autodetectJavaCheckBox->isChecked()); + if (!ui->autodetectJavaCheckBox->isChecked()) + ui->autodownloadCheckBox->setChecked(false); + }); + } else { + ui->autodownloadCheckBox->setHidden(true); + ui->tabWidget->tabBar()->hide(); + } loadSettings(); updateThresholds(); @@ -94,6 +115,8 @@ void JavaPage::applySettings() s->set("JvmArgs", ui->jvmArgsTextBox->toPlainText().replace("\n", " ")); s->set("IgnoreJavaCompatibility", ui->skipCompatibilityCheckbox->isChecked()); s->set("IgnoreJavaWizard", ui->skipJavaWizardCheckbox->isChecked()); + s->set("AutomaticJavaSwitch", ui->autodetectJavaCheckBox->isChecked()); + s->set("AutomaticJavaDownload", ui->autodownloadCheckBox->isChecked()); JavaCommon::checkJVMArgs(s->get("JvmArgs").toString(), this->parentWidget()); } void JavaPage::loadSettings() @@ -116,6 +139,8 @@ void JavaPage::loadSettings() ui->jvmArgsTextBox->setPlainText(s->get("JvmArgs").toString()); ui->skipCompatibilityCheckbox->setChecked(s->get("IgnoreJavaCompatibility").toBool()); ui->skipJavaWizardCheckbox->setChecked(s->get("IgnoreJavaWizard").toBool()); + ui->autodetectJavaCheckBox->setChecked(s->get("AutomaticJavaSwitch").toBool()); + ui->autodownloadCheckBox->setChecked(s->get("AutomaticJavaSwitch").toBool() && s->get("AutomaticJavaDownload").toBool()); } void JavaPage::on_javaDetectBtn_clicked() @@ -134,6 +159,14 @@ void JavaPage::on_javaDetectBtn_clicked() if (vselect.result() == QDialog::Accepted && vselect.selectedVersion()) { java = std::dynamic_pointer_cast(vselect.selectedVersion()); ui->javaPathTextBox->setText(java->path); + if (!java->is_64bit && APPLICATION->settings()->get("MaxMemAlloc").toInt() > 2048) { + CustomMessageBox::selectable(this, tr("Confirm Selection"), + tr("You selected a 32-bit version of Java.\n" + "This installation does not support more than 2048MiB of RAM.\n" + "Please make sure that the maximum memory value is lower."), + QMessageBox::Warning, QMessageBox::Ok, QMessageBox::Ok) + ->exec(); + } } } @@ -166,6 +199,13 @@ void JavaPage::on_javaTestBtn_clicked() checker->run(); } +void JavaPage::on_downloadJavaButton_clicked() +{ + auto jdialog = new Java::InstallDialog({}, this); + jdialog->exec(); + ui->managedJavaList->loadList(); +} + void JavaPage::on_maxMemSpinBox_valueChanged([[maybe_unused]] int i) { updateThresholds(); @@ -210,3 +250,35 @@ void JavaPage::updateThresholds() ui->labelMaxMemIcon->setPixmap(pix); } } + +void JavaPage::on_removeJavaButton_clicked() +{ + auto version = ui->managedJavaList->selectedVersion(); + auto dcast = std::dynamic_pointer_cast(version); + if (!dcast) { + return; + } + QDir dir(APPLICATION->javaPath()); + + auto entries = dir.entryInfoList(QDir::Dirs | QDir::NoDotAndDotDot); + for (auto& entry : entries) { + if (dcast->path.startsWith(entry.canonicalFilePath())) { + auto response = CustomMessageBox::selectable(this, tr("Confirm Deletion"), + tr("You are about to remove \"%1\" java version.\n" + "Are you sure?") + .arg(entry.fileName()), + QMessageBox::Warning, QMessageBox::Yes | QMessageBox::No, QMessageBox::No) + ->exec(); + + if (response == QMessageBox::Yes) { + FS::deletePath(entry.canonicalFilePath()); + ui->managedJavaList->loadList(); + } + break; + } + } +} +void JavaPage::on_refreshJavaButton_clicked() +{ + ui->managedJavaList->loadList(); +} diff --git a/launcher/ui/pages/global/JavaPage.h b/launcher/ui/pages/global/JavaPage.h index 1a1bd96e14..0a1c4a6bef 100644 --- a/launcher/ui/pages/global/JavaPage.h +++ b/launcher/ui/pages/global/JavaPage.h @@ -38,7 +38,7 @@ #include #include #include -#include +#include #include "JavaCommon.h" #include "ui/pages/BasePage.h" @@ -72,6 +72,9 @@ class JavaPage : public QWidget, public BasePage { void on_javaDetectBtn_clicked(); void on_javaTestBtn_clicked(); void on_javaBrowseBtn_clicked(); + void on_downloadJavaButton_clicked(); + void on_removeJavaButton_clicked(); + void on_refreshJavaButton_clicked(); void on_maxMemSpinBox_valueChanged(int i); void checkerFinished(); diff --git a/launcher/ui/pages/global/JavaPage.ui b/launcher/ui/pages/global/JavaPage.ui index fd16572d38..c8f62e0757 100644 --- a/launcher/ui/pages/global/JavaPage.ui +++ b/launcher/ui/pages/global/JavaPage.ui @@ -6,8 +6,8 @@ 0 0 - 545 - 580 + 559 + 659 @@ -34,9 +34,9 @@ 0 - + - Tab 1 + General @@ -160,25 +160,6 @@ Java Runtime - - - - true - - - - 0 - 0 - - - - - 16777215 - 100 - - - - @@ -225,7 +206,7 @@ - + @@ -241,6 +222,42 @@ + + + + If enabled, the launcher will not prompt you to choose a Java version if one isn't found. + + + Skip Java &Wizard + + + + + + + true + + + + 0 + 0 + + + + + 16777215 + 100 + + + + + + + + Autodetect Java version + + + @@ -277,13 +294,13 @@ - - - - If enabled, the launcher will not prompt you to choose a Java version if one isn't found. + + + + false - Skip Java &Wizard + Auto-download Mojang Java @@ -305,16 +322,106 @@ + + + Management + + + + + + Downloaded Java Versions + + + + + + + 0 + 0 + + + + + + + + + + Download + + + + + + + Remove + + + + + + + Qt::Vertical + + + + 20 + 40 + + + + + + + + Refresh + + + + + + + + + + + + Qt::Vertical + + + + 20 + 40 + + + + + + + + + VersionSelectWidget + QWidget +
ui/widgets/VersionSelectWidget.h
+ 1 +
+
minMemSpinBox maxMemSpinBox permGenSpinBox - javaBrowseBtn javaPathTextBox + javaBrowseBtn + javaDetectBtn + javaTestBtn + skipCompatibilityCheckbox + skipJavaWizardCheckbox + jvmArgsTextBox tabWidget diff --git a/launcher/ui/pages/global/LauncherPage.cpp b/launcher/ui/pages/global/LauncherPage.cpp index 6b32e98496..887ae6c31d 100644 --- a/launcher/ui/pages/global/LauncherPage.cpp +++ b/launcher/ui/pages/global/LauncherPage.cpp @@ -173,6 +173,16 @@ void LauncherPage::on_downloadsDirBrowseBtn_clicked() } } +void LauncherPage::on_javaDirBrowseBtn_clicked() +{ + QString raw_dir = QFileDialog::getExistingDirectory(this, tr("Java Folder"), ui->javaDirTextBox->text()); + + if (!raw_dir.isEmpty() && QDir(raw_dir).exists()) { + QString cooked_dir = FS::NormalizePath(raw_dir); + ui->javaDirTextBox->setText(cooked_dir); + } +} + void LauncherPage::on_skinsDirBrowseBtn_clicked() { QString raw_dir = QFileDialog::getExistingDirectory(this, tr("Skins Folder"), ui->skinsDirTextBox->text()); @@ -222,6 +232,7 @@ void LauncherPage::applySettings() s->set("IconsDir", ui->iconsDirTextBox->text()); s->set("DownloadsDir", ui->downloadsDirTextBox->text()); s->set("SkinsDir", ui->skinsDirTextBox->text()); + s->set("JavaDir", ui->javaDirTextBox->text()); s->set("DownloadsDirWatchRecursive", ui->downloadsDirWatchRecursiveCheckBox->isChecked()); auto sortMode = (InstSortMode)ui->sortingModeGroup->checkedId(); @@ -286,6 +297,7 @@ void LauncherPage::loadSettings() ui->iconsDirTextBox->setText(s->get("IconsDir").toString()); ui->downloadsDirTextBox->setText(s->get("DownloadsDir").toString()); ui->skinsDirTextBox->setText(s->get("SkinsDir").toString()); + ui->javaDirTextBox->setText(s->get("JavaDir").toString()); ui->downloadsDirWatchRecursiveCheckBox->setChecked(s->get("DownloadsDirWatchRecursive").toBool()); QString sortMode = s->get("InstSortMode").toString(); diff --git a/launcher/ui/pages/global/LauncherPage.h b/launcher/ui/pages/global/LauncherPage.h index f9aefb1716..5db3e1681b 100644 --- a/launcher/ui/pages/global/LauncherPage.h +++ b/launcher/ui/pages/global/LauncherPage.h @@ -74,6 +74,7 @@ class LauncherPage : public QWidget, public BasePage { void on_modsDirBrowseBtn_clicked(); void on_iconsDirBrowseBtn_clicked(); void on_downloadsDirBrowseBtn_clicked(); + void on_javaDirBrowseBtn_clicked(); void on_skinsDirBrowseBtn_clicked(); void on_metadataDisableBtn_clicked(); diff --git a/launcher/ui/pages/global/LauncherPage.ui b/launcher/ui/pages/global/LauncherPage.ui index 1f49a831f9..aa7c1b0129 100644 --- a/launcher/ui/pages/global/LauncherPage.ui +++ b/launcher/ui/pages/global/LauncherPage.ui @@ -7,7 +7,7 @@ 0 0 511 - 691 + 726 @@ -94,7 +94,7 @@ Folders - + &Downloads: @@ -104,42 +104,59 @@ - - + + - I&nstances: - - - instDirTextBox + Browse - - - - - - - + - - + + - Browse + &Skins: + + + skinsDirTextBox - - + + + + &Icons: + + + iconsDirTextBox + + - - + + + + When enabled, in addition to the downloads folder, its sub folders will also be searched when looking for resources (e.g. when looking for blocked mods on CurseForge). + - Browse + Check downloads folder recursively + + + + + + + + + + &Java: + + + javaDirTextBox @@ -153,6 +170,22 @@ + + + + + + + + + + + + + Browse + + + @@ -167,40 +200,27 @@ - - + + - &Icons: + I&nstances: - iconsDirTextBox + instDirTextBox - + Browse - - - - &Skins: - - - skinsDirTextBox - - - - - - - When enabled, in addition to the downloads folder, its sub folders will also be searched when looking for resources (e.g. when looking for blocked mods on CurseForge). - + + - Check downloads folder recursively + Browse diff --git a/launcher/ui/pages/instance/InstanceSettingsPage.cpp b/launcher/ui/pages/instance/InstanceSettingsPage.cpp index 76add9402c..6e6b1db57b 100644 --- a/launcher/ui/pages/instance/InstanceSettingsPage.cpp +++ b/launcher/ui/pages/instance/InstanceSettingsPage.cpp @@ -36,6 +36,8 @@ */ #include "InstanceSettingsPage.h" +#include "ui/dialogs/CustomMessageBox.h" +#include "ui/java/InstallJavaDialog.h" #include "ui_InstanceSettingsPage.h" #include @@ -62,6 +64,8 @@ InstanceSettingsPage::InstanceSettingsPage(BaseInstance* inst, QWidget* parent) m_settings = inst->settings(); ui->setupUi(this); + ui->javaDownloadBtn->setHidden(!BuildConfig.JAVA_DOWNLOADER_ENABLED); + connect(ui->openGlobalJavaSettingsButton, &QCommandLinkButton::clicked, this, &InstanceSettingsPage::globalSettingsButtonClicked); connect(APPLICATION, &Application::globalSettingsAboutToOpen, this, &InstanceSettingsPage::applySettings); connect(APPLICATION, &Application::globalSettingsClosed, this, &InstanceSettingsPage::loadSettings); @@ -186,9 +190,6 @@ void InstanceSettingsPage::applySettings() m_settings->reset("JvmArgs"); } - // old generic 'override both' is removed. - m_settings->reset("OverrideJava"); - // Custom Commands bool custcmd = ui->customCommands->checked(); m_settings->set("OverrideCommands", custcmd); @@ -317,9 +318,8 @@ void InstanceSettingsPage::loadSettings() ui->labelPermgenNote->setVisible(permGenVisible); // Java Settings - bool overrideJava = m_settings->get("OverrideJava").toBool(); - bool overrideLocation = m_settings->get("OverrideJavaLocation").toBool() || overrideJava; - bool overrideArgs = m_settings->get("OverrideJavaArgs").toBool() || overrideJava; + bool overrideLocation = m_settings->get("OverrideJavaLocation").toBool(); + bool overrideArgs = m_settings->get("OverrideJavaArgs").toBool(); ui->javaSettingsGroupBox->setChecked(overrideLocation); ui->javaPathTextBox->setText(m_settings->get("JavaPath").toString()); @@ -388,6 +388,12 @@ void InstanceSettingsPage::loadSettings() ui->onlineFixes->setChecked(m_settings->get("OnlineFixes").toBool()); } +void InstanceSettingsPage::on_javaDownloadBtn_clicked() +{ + auto jdialog = new Java::InstallDialog({}, this); + jdialog->exec(); +} + void InstanceSettingsPage::on_javaDetectBtn_clicked() { if (JavaUtils::getJavaCheckPath().isEmpty()) { @@ -409,6 +415,15 @@ void InstanceSettingsPage::on_javaDetectBtn_clicked() ui->labelPermGen->setVisible(visible); ui->labelPermgenNote->setVisible(visible); m_settings->set("PermGenVisible", visible); + + if (!java->is_64bit && m_settings->get("MaxMemAlloc").toInt() > 2048) { + CustomMessageBox::selectable(this, tr("Confirm Selection"), + tr("You selected a 32-bit version of Java.\n" + "This installation does not support more than 2048MiB of RAM.\n" + "Please make sure that the maximum memory value is lower."), + QMessageBox::Warning, QMessageBox::Ok, QMessageBox::Ok) + ->exec(); + } } } diff --git a/launcher/ui/pages/instance/InstanceSettingsPage.h b/launcher/ui/pages/instance/InstanceSettingsPage.h index 8b78dcb7f1..bf0b364e83 100644 --- a/launcher/ui/pages/instance/InstanceSettingsPage.h +++ b/launcher/ui/pages/instance/InstanceSettingsPage.h @@ -69,6 +69,7 @@ class InstanceSettingsPage : public QWidget, public BasePage { void on_javaDetectBtn_clicked(); void on_javaTestBtn_clicked(); void on_javaBrowseBtn_clicked(); + void on_javaDownloadBtn_clicked(); void on_maxMemSpinBox_valueChanged(int i); void onUseNativeGLFWChanged(bool checked); diff --git a/launcher/ui/pages/instance/InstanceSettingsPage.ui b/launcher/ui/pages/instance/InstanceSettingsPage.ui index 9490860ae4..58894f50ec 100644 --- a/launcher/ui/pages/instance/InstanceSettingsPage.ui +++ b/launcher/ui/pages/instance/InstanceSettingsPage.ui @@ -84,6 +84,13 @@ + + + + Download Java + + + @@ -764,6 +771,12 @@ openGlobalJavaSettingsButton settingsTabs javaSettingsGroupBox + javaPathTextBox + javaBrowseBtn + javaDownloadBtn + javaDetectBtn + javaTestBtn + skipCompatibilityCheckbox memoryGroupBox minMemSpinBox maxMemSpinBox @@ -783,6 +796,18 @@ useNativeOpenALCheck showGameTime recordGameTime + miscellaneousSettingsBox + closeAfterLaunchCheck + quitAfterGameStopCheck + perfomanceGroupBox + enableFeralGamemodeCheck + enableMangoHud + useDiscreteGpuCheck + gameTimeGroupBox + serverJoinGroupBox + serverJoinAddress + instanceAccountGroupBox + instanceAccountSelector diff --git a/launcher/ui/setupwizard/JavaWizardPage.cpp b/launcher/ui/setupwizard/JavaWizardPage.cpp index abe4860da4..a47cebcaa8 100644 --- a/launcher/ui/setupwizard/JavaWizardPage.cpp +++ b/launcher/ui/setupwizard/JavaWizardPage.cpp @@ -12,12 +12,8 @@ #include -#include "FileSystem.h" #include "JavaCommon.h" -#include "java/JavaInstall.h" -#include "java/JavaUtils.h" -#include "ui/dialogs/CustomMessageBox.h" #include "ui/widgets/JavaSettingsWidget.h" #include "ui/widgets/VersionSelectWidget.h" @@ -57,6 +53,8 @@ bool JavaWizardPage::validatePage() { auto settings = APPLICATION->settings(); auto result = m_java_widget->validate(); + settings->set("AutomaticJavaSwitch", m_java_widget->autoDetectJava()); + settings->set("AutomaticJavaDownload", m_java_widget->autoDownloadJava()); switch (result) { default: case JavaSettingsWidget::ValidationStatus::Bad: { diff --git a/launcher/ui/widgets/JavaSettingsWidget.cpp b/launcher/ui/widgets/JavaSettingsWidget.cpp index bd6b6b1181..ec1ed0605c 100644 --- a/launcher/ui/widgets/JavaSettingsWidget.cpp +++ b/launcher/ui/widgets/JavaSettingsWidget.cpp @@ -4,6 +4,7 @@ #include #include #include +#include #include #include #include @@ -11,12 +12,16 @@ #include +#include "DesktopServices.h" #include "FileSystem.h" #include "JavaCommon.h" +#include "java/JavaChecker.h" #include "java/JavaInstall.h" +#include "java/JavaInstallList.h" #include "java/JavaUtils.h" #include "ui/dialogs/CustomMessageBox.h" +#include "ui/java/InstallJavaDialog.h" #include "ui/widgets/VersionSelectWidget.h" #include "Application.h" @@ -38,6 +43,9 @@ JavaSettingsWidget::JavaSettingsWidget(QWidget* parent) : QWidget(parent) connect(m_javaBrowseBtn, &QPushButton::clicked, this, &JavaSettingsWidget::on_javaBrowseBtn_clicked); connect(m_javaPathTextBox, &QLineEdit::textEdited, this, &JavaSettingsWidget::javaPathEdited); connect(m_javaStatusBtn, &QToolButton::clicked, this, &JavaSettingsWidget::on_javaStatusBtn_clicked); + if (BuildConfig.JAVA_DOWNLOADER_ENABLED) { + connect(m_javaDownloadBtn, &QPushButton::clicked, this, &JavaSettingsWidget::javaDownloadBtn_clicked); + } } void JavaSettingsWidget::setupUi() @@ -120,6 +128,38 @@ void JavaSettingsWidget::setupUi() m_verticalLayout->addWidget(m_memoryGroupBox); + m_horizontalBtnLayout = new QHBoxLayout(); + m_horizontalBtnLayout->setObjectName(QStringLiteral("horizontalBtnLayout")); + + if (BuildConfig.JAVA_DOWNLOADER_ENABLED) { + m_javaDownloadBtn = new QPushButton(tr("Download Java"), this); + m_horizontalBtnLayout->addWidget(m_javaDownloadBtn); + } + + m_verticalLayout->addLayout(m_horizontalBtnLayout); + + m_autoJavaGroupBox = new QGroupBox(this); + m_autoJavaGroupBox->setObjectName(QStringLiteral("autoJavaGroupBox")); + m_veriticalJavaLayout = new QVBoxLayout(m_autoJavaGroupBox); + m_veriticalJavaLayout->setObjectName(QStringLiteral("veriticalJavaLayout")); + + m_autodetectJavaCheckBox = new QCheckBox(m_autoJavaGroupBox); + m_autodetectJavaCheckBox->setObjectName("autodetectJavaCheckBox"); + m_veriticalJavaLayout->addWidget(m_autodetectJavaCheckBox); + + if (BuildConfig.JAVA_DOWNLOADER_ENABLED) { + m_autodownloadCheckBox = new QCheckBox(m_autoJavaGroupBox); + m_autodownloadCheckBox->setObjectName("autodownloadCheckBox"); + m_autodownloadCheckBox->setEnabled(false); + m_veriticalJavaLayout->addWidget(m_autodownloadCheckBox); + connect(m_autodetectJavaCheckBox, &QCheckBox::stateChanged, this, [this] { + m_autodownloadCheckBox->setEnabled(m_autodetectJavaCheckBox->isChecked()); + if (!m_autodetectJavaCheckBox->isChecked()) + m_autodownloadCheckBox->setChecked(false); + }); + } + m_verticalLayout->addWidget(m_autoJavaGroupBox); + retranslate(); } @@ -137,6 +177,23 @@ void JavaSettingsWidget::initialize() m_maxMemSpinBox->setValue(observedMaxMemory); m_permGenSpinBox->setValue(observedPermGenMemory); updateThresholds(); + + if (BuildConfig.JAVA_DOWNLOADER_ENABLED) { + auto button = + CustomMessageBox::selectable(this, tr("Automatic Java Download"), + tr("%1 can automatically download the correct Java version for each version of Minecraft..\n" + "Do you want to enable Java auto-download?\n") + .arg(BuildConfig.LAUNCHER_DISPLAYNAME), + QMessageBox::Question, QMessageBox::Yes | QMessageBox::No, QMessageBox::Yes) + ->exec(); + if (button == QMessageBox::Yes) { + m_autodetectJavaCheckBox->setChecked(true); + m_autodownloadCheckBox->setChecked(true); + } else { + m_autodetectJavaCheckBox->setChecked(s->get("AutomaticJavaSwitch").toBool()); + m_autodownloadCheckBox->setChecked(s->get("AutomaticJavaSwitch").toBool() && s->get("AutomaticJavaDownload").toBool()); + } + } } void JavaSettingsWidget::refresh() @@ -153,20 +210,52 @@ JavaSettingsWidget::ValidationStatus JavaSettingsWidget::validate() switch (javaStatus) { default: case JavaStatus::NotSet: + /* fallthrough */ case JavaStatus::DoesNotExist: + /* fallthrough */ case JavaStatus::DoesNotStart: + /* fallthrough */ case JavaStatus::ReturnedInvalidData: { - int button = CustomMessageBox::selectable(this, tr("No Java version selected"), - tr("You didn't select a Java version or selected something that doesn't work.\n" - "%1 will not be able to start Minecraft.\n" - "Do you wish to proceed without any Java?" - "\n\n" - "You can change the Java version in the settings later.\n") - .arg(BuildConfig.LAUNCHER_DISPLAYNAME), - QMessageBox::Warning, QMessageBox::Yes | QMessageBox::No, QMessageBox::NoButton) - ->exec(); - if (button == QMessageBox::No) { - return ValidationStatus::Bad; + if (!(BuildConfig.JAVA_DOWNLOADER_ENABLED && m_autodownloadCheckBox->isChecked())) { // the java will not be autodownloaded + int button = QMessageBox::No; + if (m_result.mojangPlatform == "32" && maxHeapSize() > 2048) { + button = CustomMessageBox::selectable( + this, tr("32-bit Java detected"), + tr("You selected a 32-bit installation of Java, but allocated more than 2048MiB as maximum memory.\n" + "%1 will not be able to start Minecraft.\n" + "Do you wish to proceed?" + "\n\n" + "You can change the Java version in the settings later.\n") + .arg(BuildConfig.LAUNCHER_DISPLAYNAME), + QMessageBox::Warning, QMessageBox::Yes | QMessageBox::No | QMessageBox::Help, QMessageBox::NoButton) + ->exec(); + + } else { + button = CustomMessageBox::selectable(this, tr("No Java version selected"), + tr("You either didn't select a Java version or selected one that does not work.\n" + "%1 will not be able to start Minecraft.\n" + "Do you wish to proceed without a functional version of Java?" + "\n\n" + "You can change the Java version in the settings later.\n") + .arg(BuildConfig.LAUNCHER_DISPLAYNAME), + QMessageBox::Warning, QMessageBox::Yes | QMessageBox::No | QMessageBox::Help, + QMessageBox::NoButton) + ->exec(); + } + switch (button) { + case QMessageBox::Yes: + return ValidationStatus::JavaBad; + case QMessageBox::Help: + DesktopServices::openUrl(QUrl(BuildConfig.HELP_URL.arg("java-wizard"))); + /* fallthrough */ + case QMessageBox::No: + /* fallthrough */ + default: + return ValidationStatus::Bad; + } + if (button == QMessageBox::No) { + return ValidationStatus::Bad; + } } return ValidationStatus::JavaBad; } break; @@ -250,21 +339,22 @@ void JavaSettingsWidget::javaVersionSelected(BaseVersion::Ptr version) void JavaSettingsWidget::on_javaBrowseBtn_clicked() { - QString filter; -#if defined Q_OS_WIN32 - filter = "Java (javaw.exe)"; -#else - filter = "Java (java)"; -#endif - QString raw_path = QFileDialog::getOpenFileName(this, tr("Find Java executable"), QString(), filter); + auto filter = QString("Java (%1)").arg(JavaUtils::javaExecutable); + auto raw_path = QFileDialog::getOpenFileName(this, tr("Find Java executable"), QString(), filter); if (raw_path.isEmpty()) { return; } - QString cooked_path = FS::NormalizePath(raw_path); + auto cooked_path = FS::NormalizePath(raw_path); m_javaPathTextBox->setText(cooked_path); checkJavaPath(cooked_path); } +void JavaSettingsWidget::javaDownloadBtn_clicked() +{ + auto jdialog = new Java::InstallDialog({}, this); + jdialog->exec(); +} + void JavaSettingsWidget::on_javaStatusBtn_clicked() { QString text; @@ -359,30 +449,25 @@ void JavaSettingsWidget::checkJavaPath(const QString& path) return; } setJavaStatus(JavaStatus::Pending); - m_checker.reset(new JavaChecker()); - m_checker->m_path = path; - m_checker->m_minMem = minHeapSize(); - m_checker->m_maxMem = maxHeapSize(); - if (m_permGenSpinBox->isVisible()) { - m_checker->m_permGen = m_permGenSpinBox->value(); - } + m_checker.reset( + new JavaChecker(path, "", minHeapSize(), maxHeapSize(), m_permGenSpinBox->isVisible() ? m_permGenSpinBox->value() : 0, 0, this)); connect(m_checker.get(), &JavaChecker::checkFinished, this, &JavaSettingsWidget::checkFinished); - m_checker->performCheck(); + m_checker->start(); } -void JavaSettingsWidget::checkFinished(JavaCheckResult result) +void JavaSettingsWidget::checkFinished(const JavaChecker::Result& result) { m_result = result; switch (result.validity) { - case JavaCheckResult::Validity::Valid: { + case JavaChecker::Result::Validity::Valid: { setJavaStatus(JavaStatus::Good); break; } - case JavaCheckResult::Validity::ReturnedInvalidData: { + case JavaChecker::Result::Validity::ReturnedInvalidData: { setJavaStatus(JavaStatus::ReturnedInvalidData); break; } - case JavaCheckResult::Validity::Errored: { + case JavaChecker::Result::Validity::Errored: { setJavaStatus(JavaStatus::DoesNotStart); break; } @@ -403,6 +488,11 @@ void JavaSettingsWidget::retranslate() m_minMemSpinBox->setToolTip(tr("The amount of memory Minecraft is started with.")); m_permGenSpinBox->setToolTip(tr("The amount of memory available to store loaded Java classes.")); m_javaBrowseBtn->setText(tr("Browse")); + if (BuildConfig.JAVA_DOWNLOADER_ENABLED) { + m_autodownloadCheckBox->setText(tr("Auto-download Mojang Java")); + } + m_autodetectJavaCheckBox->setText(tr("Autodetect Java version")); + m_autoJavaGroupBox->setTitle(tr("Autodetect Java")); } void JavaSettingsWidget::updateThresholds() @@ -418,6 +508,9 @@ void JavaSettingsWidget::updateThresholds() } else if (observedMaxMemory < observedMinMemory) { iconName = "status-yellow"; m_labelMaxMemIcon->setToolTip(tr("Your maximum memory allocation is smaller than the minimum value")); + } else if (observedMaxMemory > 2048 && m_result.is_64bit) { + iconName = "status-bad"; + m_labelMaxMemIcon->setToolTip(tr("You are exceeding the maximum allocation supported by 32-bit installations of Java.")); } else { iconName = "status-good"; m_labelMaxMemIcon->setToolTip(""); @@ -430,3 +523,13 @@ void JavaSettingsWidget::updateThresholds() m_labelMaxMemIcon->setPixmap(pix); } } + +bool JavaSettingsWidget::autoDownloadJava() const +{ + return m_autodownloadCheckBox && m_autodownloadCheckBox->isChecked(); +} + +bool JavaSettingsWidget::autoDetectJava() const +{ + return m_autodetectJavaCheckBox->isChecked(); +} diff --git a/launcher/ui/widgets/JavaSettingsWidget.h b/launcher/ui/widgets/JavaSettingsWidget.h index 18a480532d..622c473fe5 100644 --- a/launcher/ui/widgets/JavaSettingsWidget.h +++ b/launcher/ui/widgets/JavaSettingsWidget.h @@ -4,6 +4,7 @@ #include #include #include +#include #include class QLineEdit; @@ -25,7 +26,7 @@ class JavaSettingsWidget : public QWidget { public: explicit JavaSettingsWidget(QWidget* parent); - virtual ~JavaSettingsWidget() {}; + virtual ~JavaSettingsWidget() = default; enum class JavaStatus { NotSet, Pending, Good, DoesNotExist, DoesNotStart, ReturnedInvalidData } javaStatus = JavaStatus::NotSet; @@ -41,6 +42,8 @@ class JavaSettingsWidget : public QWidget { int minHeapSize() const; int maxHeapSize() const; QString javaPath() const; + bool autoDetectJava() const; + bool autoDownloadJava() const; void updateThresholds(); @@ -50,7 +53,8 @@ class JavaSettingsWidget : public QWidget { void javaVersionSelected(BaseVersion::Ptr version); void on_javaBrowseBtn_clicked(); void on_javaStatusBtn_clicked(); - void checkFinished(JavaCheckResult result); + void javaDownloadBtn_clicked(); + void checkFinished(const JavaChecker::Result& result); protected: /* methods */ void checkJavaPathOnEdit(const QString& path); @@ -76,15 +80,23 @@ class JavaSettingsWidget : public QWidget { QSpinBox* m_minMemSpinBox = nullptr; QLabel* m_labelPermGen = nullptr; QSpinBox* m_permGenSpinBox = nullptr; + + QHBoxLayout* m_horizontalBtnLayout = nullptr; + QPushButton* m_javaDownloadBtn = nullptr; QIcon goodIcon; QIcon yellowIcon; QIcon badIcon; + QGroupBox* m_autoJavaGroupBox = nullptr; + QVBoxLayout* m_veriticalJavaLayout = nullptr; + QCheckBox* m_autodetectJavaCheckBox = nullptr; + QCheckBox* m_autodownloadCheckBox = nullptr; + unsigned int observedMinMemory = 0; unsigned int observedMaxMemory = 0; unsigned int observedPermGenMemory = 0; QString queuedCheck; uint64_t m_availableMemory = 0ull; shared_qobject_ptr m_checker; - JavaCheckResult m_result; + JavaChecker::Result m_result; };