diff --git a/.github/workflows/backport.yml b/.github/workflows/backport.yml index d91d9507a9..4146cddf4f 100644 --- a/.github/workflows/backport.yml +++ b/.github/workflows/backport.yml @@ -25,7 +25,7 @@ jobs: with: ref: ${{ github.event.pull_request.head.sha }} - name: Create backport PRs - uses: korthout/backport-action@v3.0.2 + uses: korthout/backport-action@v3.1.0 with: # Config README: https://github.com/korthout/backport-action#backport-action pull_description: |- diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 249168fe9a..cbffca5a12 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -160,7 +160,7 @@ jobs: - name: Setup ccache if: (runner.os != 'Windows' || matrix.msystem == '') && inputs.build_type == 'Debug' - uses: hendrikmuhs/ccache-action@v1.2.13 + uses: hendrikmuhs/ccache-action@v1.2.14 with: key: ${{ matrix.os }}-qt${{ matrix.qt_ver }}-${{ matrix.architecture }} @@ -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/.github/workflows/update-flake.yml b/.github/workflows/update-flake.yml index 2226d07104..e1ab2e86e9 100644 --- a/.github/workflows/update-flake.yml +++ b/.github/workflows/update-flake.yml @@ -19,7 +19,7 @@ jobs: - uses: actions/checkout@v4 - uses: cachix/install-nix-action@ba0dd844c9180cbf77aa72a116d6fbc515d0e87b # v27 - - uses: DeterminateSystems/update-flake-lock@v22 + - uses: DeterminateSystems/update-flake-lock@v23 with: commit-msg: "chore(nix): update lockfile" pr-title: "chore(nix): update lockfile" 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/default.nix b/default.nix index c7d0c267d2..6466507b71 100644 --- a/default.nix +++ b/default.nix @@ -1,14 +1,9 @@ -( - import - ( - let - lock = builtins.fromJSON (builtins.readFile ./flake.lock); - in - fetchTarball { - url = "https://github.com/edolstra/flake-compat/archive/${lock.nodes.flake-compat.locked.rev}.tar.gz"; - sha256 = lock.nodes.flake-compat.locked.narHash; - } - ) - {src = ./.;} -) -.defaultNix +(import ( + let + lock = builtins.fromJSON (builtins.readFile ./flake.lock); + in + fetchTarball { + url = "https://github.com/edolstra/flake-compat/archive/${lock.nodes.flake-compat.locked.rev}.tar.gz"; + sha256 = lock.nodes.flake-compat.locked.narHash; + } +) { src = ./.; }).defaultNix diff --git a/flake.lock b/flake.lock index 38b0d74303..58ae840c20 100644 --- a/flake.lock +++ b/flake.lock @@ -16,47 +16,6 @@ "type": "github" } }, - "flake-parts": { - "inputs": { - "nixpkgs-lib": [ - "nixpkgs" - ] - }, - "locked": { - "lastModified": 1719994518, - "narHash": "sha256-pQMhCCHyQGRzdfAkdJ4cIWiw+JNuWsTX7f0ZYSyz0VY=", - "owner": "hercules-ci", - "repo": "flake-parts", - "rev": "9227223f6d922fee3c7b190b2cc238a99527bbb7", - "type": "github" - }, - "original": { - "owner": "hercules-ci", - "repo": "flake-parts", - "type": "github" - } - }, - "gitignore": { - "inputs": { - "nixpkgs": [ - "pre-commit-hooks", - "nixpkgs" - ] - }, - "locked": { - "lastModified": 1709087332, - "narHash": "sha256-HG2cCnktfHsKV0s4XW83gU3F57gaTljL9KNSuG6bnQs=", - "owner": "hercules-ci", - "repo": "gitignore.nix", - "rev": "637db329424fd7e46cf4185293b9cc8c88c95394", - "type": "github" - }, - "original": { - "owner": "hercules-ci", - "repo": "gitignore.nix", - "type": "github" - } - }, "libnbtplusplus": { "flake": false, "locked": { @@ -73,56 +32,43 @@ "type": "github" } }, - "nixpkgs": { + "nix-filter": { "locked": { - "lastModified": 1720768451, - "narHash": "sha256-EYekUHJE2gxeo2pM/zM9Wlqw1Uw2XTJXOSAO79ksc4Y=", - "owner": "NixOS", - "repo": "nixpkgs", - "rev": "7e7c39ea35c5cdd002cd4588b03a3fb9ece6fad9", + "lastModified": 1710156097, + "narHash": "sha256-1Wvk8UP7PXdf8bCCaEoMnOT1qe5/Duqgj+rL8sRQsSM=", + "owner": "numtide", + "repo": "nix-filter", + "rev": "3342559a24e85fc164b295c3444e8a139924675b", "type": "github" }, "original": { - "owner": "NixOS", - "ref": "nixos-unstable", - "repo": "nixpkgs", + "owner": "numtide", + "repo": "nix-filter", "type": "github" } }, - "pre-commit-hooks": { - "inputs": { - "flake-compat": [ - "flake-compat" - ], - "gitignore": "gitignore", - "nixpkgs": [ - "nixpkgs" - ], - "nixpkgs-stable": [ - "nixpkgs" - ] - }, + "nixpkgs": { "locked": { - "lastModified": 1720524665, - "narHash": "sha256-ni/87oHPZm6Gv0ECYxr1f6uxB0UKBWJ6HvS7lwLU6oY=", - "owner": "cachix", - "repo": "pre-commit-hooks.nix", - "rev": "8d6a17d0cdf411c55f12602624df6368ad86fac1", + "lastModified": 1724224976, + "narHash": "sha256-Z/ELQhrSd7bMzTO8r7NZgi9g5emh+aRKoCdaAv5fiO0=", + "owner": "NixOS", + "repo": "nixpkgs", + "rev": "c374d94f1536013ca8e92341b540eba4c22f9c62", "type": "github" }, "original": { - "owner": "cachix", - "repo": "pre-commit-hooks.nix", + "owner": "NixOS", + "ref": "nixos-unstable", + "repo": "nixpkgs", "type": "github" } }, "root": { "inputs": { "flake-compat": "flake-compat", - "flake-parts": "flake-parts", "libnbtplusplus": "libnbtplusplus", - "nixpkgs": "nixpkgs", - "pre-commit-hooks": "pre-commit-hooks" + "nix-filter": "nix-filter", + "nixpkgs": "nixpkgs" } } }, diff --git a/flake.nix b/flake.nix index 7cef5217a7..987fc0eda6 100644 --- a/flake.nix +++ b/flake.nix @@ -2,52 +2,121 @@ description = "A custom launcher for Minecraft that allows you to easily manage multiple installations of Minecraft at once (Fork of MultiMC)"; nixConfig = { - extra-substituters = ["https://cache.garnix.io"]; - extra-trusted-public-keys = ["cache.garnix.io:CTFPyKSLcx5RMJKfLo5EEPUObbA78b0YQ2DTCJXqr9g="]; + extra-substituters = [ "https://cache.garnix.io" ]; + extra-trusted-public-keys = [ "cache.garnix.io:CTFPyKSLcx5RMJKfLo5EEPUObbA78b0YQ2DTCJXqr9g=" ]; }; inputs = { nixpkgs.url = "github:NixOS/nixpkgs/nixos-unstable"; - flake-parts = { - url = "github:hercules-ci/flake-parts"; - inputs.nixpkgs-lib.follows = "nixpkgs"; - }; - pre-commit-hooks = { - url = "github:cachix/pre-commit-hooks.nix"; - inputs = { - nixpkgs.follows = "nixpkgs"; - nixpkgs-stable.follows = "nixpkgs"; - flake-compat.follows = "flake-compat"; - }; + + libnbtplusplus = { + url = "github:PrismLauncher/libnbtplusplus"; + flake = false; }; + + nix-filter.url = "github:numtide/nix-filter"; + + /* + Inputs below this are optional and can be removed + + ``` + { + inputs.prismlauncher = { + url = "github:PrismLauncher/PrismLauncher"; + inputs = { + flake-compat.follows = ""; + }; + }; + } + ``` + */ + flake-compat = { url = "github:edolstra/flake-compat"; flake = false; }; - libnbtplusplus = { - url = "github:PrismLauncher/libnbtplusplus"; - flake = false; - }; }; - outputs = { - flake-parts, - pre-commit-hooks, - ... - } @ inputs: - flake-parts.lib.mkFlake {inherit inputs;} { - imports = [ - pre-commit-hooks.flakeModule - - ./nix/dev.nix - ./nix/distribution.nix - ]; - - systems = [ - "x86_64-linux" - "aarch64-linux" - "x86_64-darwin" - "aarch64-darwin" - ]; + outputs = + { + self, + nixpkgs, + libnbtplusplus, + nix-filter, + ... + }: + let + inherit (nixpkgs) lib; + + # While we only officially support aarch and x86_64 on Linux and MacOS, + # we expose a reasonable amount of other systems for users who want to + # build for most exotic platforms + systems = lib.systems.flakeExposed; + + forAllSystems = lib.genAttrs systems; + nixpkgsFor = forAllSystems (system: nixpkgs.legacyPackages.${system}); + in + { + checks = forAllSystems ( + system: + let + checks' = nixpkgsFor.${system}.callPackage ./nix/checks.nix { inherit self; }; + in + lib.filterAttrs (_: lib.isDerivation) checks' + ); + + devShells = forAllSystems ( + system: + let + pkgs = nixpkgsFor.${system}; + in + { + default = pkgs.mkShell { + inputsFrom = [ self.packages.${system}.prismlauncher-unwrapped ]; + buildInputs = with pkgs; [ + ccache + ninja + ]; + }; + } + ); + + formatter = forAllSystems (system: nixpkgsFor.${system}.nixfmt-rfc-style); + + overlays.default = + final: prev: + let + version = builtins.substring 0 8 self.lastModifiedDate or "dirty"; + in + { + prismlauncher-unwrapped = prev.callPackage ./nix/unwrapped.nix { + inherit + libnbtplusplus + nix-filter + self + version + ; + }; + + prismlauncher = final.callPackage ./nix/wrapper.nix { }; + }; + + packages = forAllSystems ( + system: + let + pkgs = nixpkgsFor.${system}; + + # Build a scope from our overlay + prismPackages = lib.makeScope pkgs.newScope (final: self.overlays.default final pkgs); + + # Grab our packages from it and set the default + packages = { + inherit (prismPackages) prismlauncher-unwrapped prismlauncher; + default = prismPackages.prismlauncher; + }; + in + # Only output them if they're available on the current system + lib.filterAttrs (_: lib.meta.availableOn pkgs.stdenv.hostPlatform) packages + ); }; } diff --git a/garnix.yaml b/garnix.yaml index 6cf8f72146..a7c1b48a9f 100644 --- a/garnix.yaml +++ b/garnix.yaml @@ -1,7 +1,10 @@ builds: exclude: + # Currently broken on Garnix's end - "*.x86_64-darwin.*" include: - "checks.x86_64-linux.*" - - "devShells.*.*" - - "packages.*.*" + - "packages.x86_64-linux.*" + - "packages.aarch64-linux.*" + - "packages.x86_64-darwin.*" + - "packages.aarch64-darwin.*" diff --git a/launcher/Application.cpp b/launcher/Application.cpp index 6c0319984b..9cd0445e62 100644 --- a/launcher/Application.cpp +++ b/launcher/Application.cpp @@ -44,10 +44,10 @@ #include "BuildConfig.h" #include "DataMigrationTask.h" +#include "java/JavaInstallList.h" #include "net/PasteUpload.h" #include "pathmatcher/MultiMatcher.h" #include "pathmatcher/SimplePrefixMatcher.h" -#include "settings/INIFile.h" #include "tools/GenericProfiler.h" #include "ui/InstanceWindow.h" #include "ui/MainWindow.h" @@ -106,7 +106,7 @@ #include "icons/IconList.h" #include "net/HttpMetaCache.h" -#include "java/JavaUtils.h" +#include "java/JavaInstallList.h" #include "updater/ExternalUpdater.h" @@ -126,6 +126,7 @@ #include #include +#include "SysInfo.h" #ifdef Q_OS_LINUX #include @@ -151,6 +152,7 @@ #endif #if defined Q_OS_WIN32 +#include #include "WindowsConsole.h" #endif @@ -236,6 +238,7 @@ Application::Application(int& argc, char** argv) : QApplication(argc, argv) { { { "d", "dir" }, "Use a custom path as application root (use '.' for current directory)", "directory" }, { { "l", "launch" }, "Launch the specified instance (by instance ID)", "instance" }, { { "s", "server" }, "Join the specified server on launch (only valid in combination with --launch)", "address" }, + { { "w", "world" }, "Join the specified world on launch (only valid in combination with --launch)", "world" }, { { "a", "profile" }, "Use the account specified by its profile name (only valid in combination with --launch)", "profile" }, { "alive", "Write a small '" + liveCheckFile + "' file after the launcher starts" }, { { "I", "import" }, "Import instance or resource from specified local path or URL", "url" }, @@ -250,6 +253,7 @@ Application::Application(int& argc, char** argv) : QApplication(argc, argv) m_instanceIdToLaunch = parser.value("launch"); m_serverToJoin = parser.value("server"); + m_worldToJoin = parser.value("world"); m_profileToUse = parser.value("profile"); m_liveCheck = parser.isSet("alive"); @@ -265,7 +269,7 @@ Application::Application(int& argc, char** argv) : QApplication(argc, argv) } // error if --launch is missing with --server or --profile - if ((!m_serverToJoin.isEmpty() || !m_profileToUse.isEmpty()) && m_instanceIdToLaunch.isEmpty()) { + if (((!m_serverToJoin.isEmpty() || !m_worldToJoin.isEmpty()) || !m_profileToUse.isEmpty()) && m_instanceIdToLaunch.isEmpty()) { std::cerr << "--server and --profile can only be used in combination with --launch!" << std::endl; m_status = Application::Failed; return; @@ -385,6 +389,8 @@ Application::Application(int& argc, char** argv) : QApplication(argc, argv) if (!m_serverToJoin.isEmpty()) { launch.args["server"] = m_serverToJoin; + } else if (!m_worldToJoin.isEmpty()) { + launch.args["world"] = m_worldToJoin; } if (!m_profileToUse.isEmpty()) { launch.args["profile"] = m_profileToUse; @@ -523,6 +529,8 @@ Application::Application(int& argc, char** argv) : QApplication(argc, argv) } if (!m_serverToJoin.isEmpty()) { qDebug() << "Address of server to join :" << m_serverToJoin; + } else if (!m_worldToJoin.isEmpty()) { + qDebug() << "Name of the world to join :" << m_worldToJoin; } qDebug() << "<> Paths set."; } @@ -596,6 +604,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()); @@ -624,7 +633,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 @@ -638,6 +647,9 @@ Application::Application(int& argc, char** argv) : QApplication(argc, argv) m_settings->registerSetting("JvmArgs", ""); m_settings->registerSetting("IgnoreJavaCompatibility", false); m_settings->registerSetting("IgnoreJavaWizard", false); + auto defaultEnableAutoJava = m_settings->get("JavaPath").toString().isEmpty(); + m_settings->registerSetting("AutomaticJavaSwitch", defaultEnableAutoJava); + m_settings->registerSetting("AutomaticJavaDownload", defaultEnableAutoJava); // Legacy settings m_settings->registerSetting("OnlineFixes", false); @@ -867,6 +879,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."; } @@ -1157,14 +1170,17 @@ void Application::performMainStartupAction() if (!m_instanceIdToLaunch.isEmpty()) { auto inst = instances()->getInstanceById(m_instanceIdToLaunch); if (inst) { - MinecraftServerTargetPtr serverToJoin = nullptr; + MinecraftTarget::Ptr targetToJoin = nullptr; MinecraftAccountPtr accountToUse = nullptr; qDebug() << "<> Instance" << m_instanceIdToLaunch << "launching"; if (!m_serverToJoin.isEmpty()) { // FIXME: validate the server string - serverToJoin.reset(new MinecraftServerTarget(MinecraftServerTarget::parse(m_serverToJoin))); + targetToJoin.reset(new MinecraftTarget(MinecraftTarget::parse(m_serverToJoin, false))); qDebug() << " Launching with server" << m_serverToJoin; + } else if (!m_worldToJoin.isEmpty()) { + targetToJoin.reset(new MinecraftTarget(MinecraftTarget::parse(m_worldToJoin, true))); + qDebug() << " Launching with world" << m_worldToJoin; } if (!m_profileToUse.isEmpty()) { @@ -1175,7 +1191,7 @@ void Application::performMainStartupAction() qDebug() << " Launching with account" << m_profileToUse; } - launch(inst, true, false, serverToJoin, accountToUse); + launch(inst, true, false, targetToJoin, accountToUse); return; } } @@ -1265,6 +1281,7 @@ void Application::messageReceived(const QByteArray& message) } else if (command == "launch") { QString id = received.args["id"]; QString server = received.args["server"]; + QString world = received.args["world"]; QString profile = received.args["profile"]; InstancePtr instance; @@ -1279,11 +1296,12 @@ void Application::messageReceived(const QByteArray& message) return; } - MinecraftServerTargetPtr serverObject = nullptr; + MinecraftTarget::Ptr serverObject = nullptr; if (!server.isEmpty()) { - serverObject = std::make_shared(MinecraftServerTarget::parse(server)); + serverObject = std::make_shared(MinecraftTarget::parse(server, false)); + } else if (!world.isEmpty()) { + serverObject = std::make_shared(MinecraftTarget::parse(world, true)); } - MinecraftAccountPtr accountObject; if (!profile.isEmpty()) { accountObject = accounts()->getAccountByProfileName(profile); @@ -1332,11 +1350,7 @@ bool Application::openJsonEditor(const QString& filename) } } -bool Application::launch(InstancePtr instance, - bool online, - bool demo, - MinecraftServerTargetPtr serverToJoin, - MinecraftAccountPtr accountToUse) +bool Application::launch(InstancePtr instance, bool online, bool demo, MinecraftTarget::Ptr targetToJoin, MinecraftAccountPtr accountToUse) { if (m_updateRunning) { qDebug() << "Cannot launch instances while an update is running. Please try again when updates are completed."; @@ -1354,7 +1368,7 @@ bool Application::launch(InstancePtr instance, controller->setOnline(online); controller->setDemo(demo); controller->setProfiler(profilers().value(instance->settings()->get("Profiler").toString(), nullptr).get()); - controller->setServerToJoin(serverToJoin); + controller->setTargetToJoin(targetToJoin); controller->setAccountToUse(accountToUse); if (window) { controller->setParentWidget(window); @@ -1733,20 +1747,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, @@ -1853,3 +1853,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..7432c9683c 100644 --- a/launcher/Application.h +++ b/launcher/Application.h @@ -47,7 +47,7 @@ #include -#include "minecraft/launch/MinecraftServerTarget.h" +#include "minecraft/launch/MinecraftTarget.h" class LaunchController; class LocalPeer; @@ -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(); @@ -202,7 +203,7 @@ class Application : public QApplication { bool launch(InstancePtr instance, bool online = true, bool demo = false, - MinecraftServerTargetPtr serverToJoin = nullptr, + MinecraftTarget::Ptr targetToJoin = nullptr, MinecraftAccountPtr accountToUse = nullptr); bool kill(InstancePtr instance); void closeCurrentWindow(); @@ -290,6 +291,7 @@ class Application : public QApplication { QString m_detectedOpenALPath; QString m_instanceIdToLaunch; QString m_serverToJoin; + QString m_worldToJoin; QString m_profileToUse; bool m_liveCheck = false; QList m_urlsToImport; diff --git a/launcher/BaseInstance.h b/launcher/BaseInstance.h index 499ec7866e..8c80331bcb 100644 --- a/launcher/BaseInstance.h +++ b/launcher/BaseInstance.h @@ -56,7 +56,7 @@ #include "net/Mode.h" #include "RuntimeContext.h" -#include "minecraft/launch/MinecraftServerTarget.h" +#include "minecraft/launch/MinecraftTarget.h" class QDir; class Task; @@ -184,7 +184,7 @@ class BaseInstance : public QObject, public std::enable_shared_from_this createLaunchTask(AuthSessionPtr account, MinecraftServerTargetPtr serverToJoin) = 0; + virtual shared_qobject_ptr createLaunchTask(AuthSessionPtr account, MinecraftTarget::Ptr targetToJoin) = 0; /// returns the current launch task (if any) shared_qobject_ptr getLaunchTask(); @@ -256,7 +256,7 @@ class BaseInstance : public QObject, public std::enable_shared_from_thistypeString(); + 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 0208c5ec39..58de35a7d2 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 @@ -262,8 +264,8 @@ set(MINECRAFT_SOURCES minecraft/launch/ExtractNatives.h minecraft/launch/LauncherPartLaunch.cpp minecraft/launch/LauncherPartLaunch.h - minecraft/launch/MinecraftServerTarget.cpp - minecraft/launch/MinecraftServerTarget.h + minecraft/launch/MinecraftTarget.cpp + minecraft/launch/MinecraftTarget.h minecraft/launch/PrintInstanceInfo.cpp minecraft/launch/PrintInstanceInfo.h minecraft/launch/ReconstructAssets.cpp @@ -272,6 +274,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 @@ -417,8 +421,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 @@ -427,6 +429,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 @@ -746,6 +760,8 @@ SET(LAUNCHER_SOURCES DataMigrationTask.cpp ApplicationMessage.h ApplicationMessage.cpp + SysInfo.h + SysInfo.cpp # GUI - general utilities DesktopServices.h @@ -784,8 +800,6 @@ SET(LAUNCHER_SOURCES # GUI - windows ui/GuiUtil.h ui/GuiUtil.cpp - ui/ColorCache.h - ui/ColorCache.cpp ui/MainWindow.h ui/MainWindow.cpp ui/InstanceWindow.h diff --git a/launcher/FileSystem.cpp b/launcher/FileSystem.cpp index a39f440155..b5c2c95cfb 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/Filter.cpp b/launcher/Filter.cpp index fc1c423447..adeb2209e1 100644 --- a/launcher/Filter.cpp +++ b/launcher/Filter.cpp @@ -1,16 +1,12 @@ #include "Filter.h" -Filter::~Filter() {} - ContainsFilter::ContainsFilter(const QString& pattern) : pattern(pattern) {} -ContainsFilter::~ContainsFilter() {} bool ContainsFilter::accepts(const QString& value) { return value.contains(pattern); } ExactFilter::ExactFilter(const QString& pattern) : pattern(pattern) {} -ExactFilter::~ExactFilter() {} bool ExactFilter::accepts(const QString& value) { return value == pattern; @@ -27,10 +23,15 @@ RegexpFilter::RegexpFilter(const QString& regexp, bool invert) : invert(invert) pattern.setPattern(regexp); pattern.optimize(); } -RegexpFilter::~RegexpFilter() {} bool RegexpFilter::accepts(const QString& value) { auto match = pattern.match(value); bool matched = match.hasMatch(); return invert ? (!matched) : (matched); } + +ExactListFilter::ExactListFilter(const QStringList& pattern) : m_pattern(pattern) {} +bool ExactListFilter::accepts(const QString& value) +{ + return m_pattern.isEmpty() || m_pattern.contains(value); +} \ No newline at end of file diff --git a/launcher/Filter.h b/launcher/Filter.h index 089c844d4c..a8c9c14d85 100644 --- a/launcher/Filter.h +++ b/launcher/Filter.h @@ -5,14 +5,14 @@ class Filter { public: - virtual ~Filter(); + virtual ~Filter() = default; virtual bool accepts(const QString& value) = 0; }; class ContainsFilter : public Filter { public: ContainsFilter(const QString& pattern); - virtual ~ContainsFilter(); + virtual ~ContainsFilter() = default; bool accepts(const QString& value) override; private: @@ -22,7 +22,7 @@ class ContainsFilter : public Filter { class ExactFilter : public Filter { public: ExactFilter(const QString& pattern); - virtual ~ExactFilter(); + virtual ~ExactFilter() = default; bool accepts(const QString& value) override; private: @@ -32,7 +32,7 @@ class ExactFilter : public Filter { class ExactIfPresentFilter : public Filter { public: ExactIfPresentFilter(const QString& pattern); - ~ExactIfPresentFilter() override = default; + virtual ~ExactIfPresentFilter() override = default; bool accepts(const QString& value) override; private: @@ -42,10 +42,20 @@ class ExactIfPresentFilter : public Filter { class RegexpFilter : public Filter { public: RegexpFilter(const QString& regexp, bool invert); - virtual ~RegexpFilter(); + virtual ~RegexpFilter() = default; bool accepts(const QString& value) override; private: QRegularExpression pattern; bool invert = false; }; + +class ExactListFilter : public Filter { + public: + ExactListFilter(const QStringList& pattern = {}); + virtual ~ExactListFilter() = default; + bool accepts(const QString& value) override; + + private: + const QStringList& m_pattern; +}; diff --git a/launcher/InstancePageProvider.h b/launcher/InstancePageProvider.h index 52399e9125..174041f89c 100644 --- a/launcher/InstancePageProvider.h +++ b/launcher/InstancePageProvider.h @@ -22,7 +22,7 @@ class InstancePageProvider : protected QObject, public BasePageProvider { public: explicit InstancePageProvider(InstancePtr parent) { inst = parent; } - virtual ~InstancePageProvider() {}; + virtual ~InstancePageProvider() = default; virtual QList getPages() override { QList values; @@ -39,7 +39,7 @@ class InstancePageProvider : protected QObject, public BasePageProvider { values.append(new TexturePackPage(onesix.get(), onesix->texturePackList())); values.append(new ShaderPackPage(onesix.get(), onesix->shaderPackList())); values.append(new NotesPage(onesix.get())); - values.append(new WorldListPage(onesix.get(), onesix->worldList())); + values.append(new WorldListPage(onesix, onesix->worldList())); values.append(new ServersPage(onesix)); // values.append(new GameOptionsPage(onesix.get())); values.append(new ScreenshotsPage(FS::PathCombine(onesix->gameRoot(), "screenshots"))); 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/LaunchController.cpp b/launcher/LaunchController.cpp index 3866a76726..73800574f1 100644 --- a/launcher/LaunchController.cpp +++ b/launcher/LaunchController.cpp @@ -324,7 +324,7 @@ void LaunchController::launchInstance() return; } - m_launcher = m_instance->createLaunchTask(m_session, m_serverToJoin); + m_launcher = m_instance->createLaunchTask(m_session, m_targetToJoin); if (!m_launcher) { emitFailed(tr("Couldn't instantiate a launcher.")); return; diff --git a/launcher/LaunchController.h b/launcher/LaunchController.h index bc688f2bae..6e2a942581 100644 --- a/launcher/LaunchController.h +++ b/launcher/LaunchController.h @@ -39,7 +39,7 @@ #include #include "minecraft/auth/MinecraftAccount.h" -#include "minecraft/launch/MinecraftServerTarget.h" +#include "minecraft/launch/MinecraftTarget.h" class InstanceWindow; class LaunchController : public Task { @@ -48,7 +48,7 @@ class LaunchController : public Task { void executeTask() override; LaunchController(QObject* parent = nullptr); - virtual ~LaunchController() {}; + virtual ~LaunchController() = default; void setInstance(InstancePtr instance) { m_instance = instance; } @@ -62,7 +62,7 @@ class LaunchController : public Task { void setParentWidget(QWidget* widget) { m_parentWidget = widget; } - void setServerToJoin(MinecraftServerTargetPtr serverToJoin) { m_serverToJoin = std::move(serverToJoin); } + void setTargetToJoin(MinecraftTarget::Ptr targetToJoin) { m_targetToJoin = std::move(targetToJoin); } void setAccountToUse(MinecraftAccountPtr accountToUse) { m_accountToUse = std::move(accountToUse); } @@ -94,5 +94,5 @@ class LaunchController : public Task { MinecraftAccountPtr m_accountToUse = nullptr; AuthSessionPtr m_session; shared_qobject_ptr m_launcher; - MinecraftServerTargetPtr m_serverToJoin; + MinecraftTarget::Ptr m_targetToJoin; }; diff --git a/launcher/MMCZip.cpp b/launcher/MMCZip.cpp index cb9ee99409..dcf3d566f8 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 @@ -536,6 +536,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/MangoHud.cpp b/launcher/MangoHud.cpp index ab79f418bc..ba16ddc4ae 100644 --- a/launcher/MangoHud.cpp +++ b/launcher/MangoHud.cpp @@ -40,8 +40,8 @@ namespace MangoHud { QString getLibraryString() { - /* - * Check for vulkan layers in this order: + /** + * Guess MangoHud install location by searching for vulkan layers in this order: * * $VK_LAYER_PATH * $XDG_DATA_DIRS (/usr/local/share/:/usr/share/) @@ -49,8 +49,9 @@ QString getLibraryString() * /etc * $XDG_CONFIG_DIRS (/etc/xdg) * $XDG_CONFIG_HOME (~/.config) + * + * @returns Absolute path of libMangoHud.so if found and empty QString otherwise. */ - QStringList vkLayerList; { QString home = QDir::homePath(); @@ -85,7 +86,7 @@ QString getLibraryString() vkLayerList << FS::PathCombine(xdgConfigHome, "vulkan", "implicit_layer.d"); } - for (QString vkLayer : vkLayerList) { + for (const QString& vkLayer : vkLayerList) { // prefer to use architecture specific vulkan layers QString currentArch = QSysInfo::currentCpuArchitecture(); @@ -95,8 +96,8 @@ QString getLibraryString() QStringList manifestNames = { QString("MangoHud.%1.json").arg(currentArch), "MangoHud.json" }; - QString filePath = ""; - for (QString manifestName : manifestNames) { + QString filePath{}; + for (const QString& manifestName : manifestNames) { QString tryPath = FS::PathCombine(vkLayer, manifestName); if (QFile::exists(tryPath)) { filePath = tryPath; @@ -111,10 +112,23 @@ QString getLibraryString() auto conf = Json::requireDocument(filePath, vkLayer); auto confObject = Json::requireObject(conf, vkLayer); auto layer = Json::ensureObject(confObject, "layer"); - return Json::ensureString(layer, "library_path"); + QString libraryName = Json::ensureString(layer, "library_path"); + +#ifdef __GLIBC__ + // Check whether mangohud is usable on a glibc based system + if (!libraryName.isEmpty()) { + QString libraryPath = findLibrary(libraryName); + if (!libraryPath.isEmpty()) { + return libraryPath; + } + } +#else + // Without glibc return recorded shared library as-is. + return libraryName; +#endif } - return QString(); + return {}; } QString findLibrary(QString libName) diff --git a/launcher/NullInstance.h b/launcher/NullInstance.h index b6a9478a8a..3ee38e76c3 100644 --- a/launcher/NullInstance.h +++ b/launcher/NullInstance.h @@ -46,13 +46,13 @@ class NullInstance : public BaseInstance { { setVersionBroken(true); } - virtual ~NullInstance() {}; + virtual ~NullInstance() = default; void saveNow() override {} void loadSpecificSettings() override { setSpecificSettingsLoaded(true); } QString getStatusbarDescription() override { return tr("Unknown instance type"); }; QSet traits() const override { return {}; }; QString instanceConfigFolder() const override { return instanceRoot(); }; - shared_qobject_ptr createLaunchTask(AuthSessionPtr, MinecraftServerTargetPtr) override { return nullptr; } + shared_qobject_ptr createLaunchTask(AuthSessionPtr, MinecraftTarget::Ptr) override { return nullptr; } shared_qobject_ptr createUpdateTask([[maybe_unused]] Net::Mode mode) override { return nullptr; } QProcessEnvironment createEnvironment() override { return QProcessEnvironment(); } QProcessEnvironment createLaunchEnvironment() override { return QProcessEnvironment(); } @@ -64,7 +64,7 @@ class NullInstance : public BaseInstance { bool canEdit() const override { return false; } bool canLaunch() const override { return false; } void populateLaunchMenu(QMenu* menu) override {} - QStringList verboseDescription(AuthSessionPtr session, MinecraftServerTargetPtr serverToJoin) override + QStringList verboseDescription(AuthSessionPtr session, MinecraftTarget::Ptr targetToJoin) override { QStringList out; out << "Null instance - placeholder."; 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..552900d35c 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 Version"); 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..b77f17b289 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,9 +33,9 @@ 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; + [[nodiscard]] Task::Ptr getLoadTask() override; bool isLoaded() override; const BaseVersion::Ptr at(int i) const override; int count() const 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 350ccc30da..bc80263489 100644 --- a/launcher/java/JavaUtils.cpp +++ b/launcher/java/JavaUtils.cpp @@ -347,6 +347,7 @@ QList JavaUtils::FindJavaPaths() } candidates.append(getMinecraftJavaBundle()); + candidates.append(getPrismJavaBundle()); candidates = addJavasFromEnv(candidates); candidates.removeDuplicates(); return candidates; @@ -391,6 +392,7 @@ QList JavaUtils::FindJavaPaths() } javas.append(getMinecraftJavaBundle()); + javas.append(getPrismJavaBundle()); javas = addJavasFromEnv(javas); javas.removeDuplicates(); return javas; @@ -401,12 +403,17 @@ QList JavaUtils::FindJavaPaths() { QList javas; javas.append(this->GetDefaultJava()->path); - auto scanJavaDir = [&](const QString& dirPath) { + auto scanJavaDir = [&]( + const QString& dirPath, + const std::function& filter = [](const QFileInfo&) { return true; }) { QDir dir(dirPath); if (!dir.exists()) return; auto entries = dir.entryInfoList(QDir::Dirs | QDir::NoDotAndDotDot); for (auto& entry : entries) { + if (!filter(entry)) + continue; + QString prefix; prefix = entry.canonicalFilePath(); javas.append(FS::PathCombine(prefix, "jre/bin/java")); @@ -429,9 +436,13 @@ QList JavaUtils::FindJavaPaths() scanJavaDirs("/usr/lib64/jvm"); scanJavaDirs("/usr/lib32/jvm"); // Gentoo's locations for openjdk and openjdk-bin respectively - scanJavaDir("/usr/lib64"); - scanJavaDir("/usr/lib"); - scanJavaDir("/opt"); + auto gentooFilter = [](const QFileInfo& info) { + QString fileName = info.fileName(); + return fileName.startsWith("openjdk-") || fileName.startsWith("openj9-"); + }; + scanJavaDir("/usr/lib64", gentooFilter); + scanJavaDir("/usr/lib", gentooFilter); + scanJavaDir("/opt", gentooFilter); // javas stored in Prism Launcher's folder scanJavaDirs("java"); // manually installed JDKs in /opt @@ -454,6 +465,7 @@ QList JavaUtils::FindJavaPaths() scanJavaDirs(FS::PathCombine(home, ".gradle/jdks")); javas.append(getMinecraftJavaBundle()); + javas.append(getPrismJavaBundle()); javas = addJavasFromEnv(javas); javas.removeDuplicates(); return javas; @@ -467,6 +479,8 @@ QList JavaUtils::FindJavaPaths() javas.append(this->GetDefaultJava()->path); javas.append(getMinecraftJavaBundle()); + javas.append(getPrismJavaBundle()); + javas.removeDuplicates(); return addJavasFromEnv(javas); } #endif @@ -478,12 +492,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"); @@ -508,7 +520,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; } @@ -521,3 +533,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..ba1c96fafc --- /dev/null +++ b/launcher/java/download/ArchiveDownloadTask.cpp @@ -0,0 +1,141 @@ +// 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(); + return; + } + + 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..99ff62b67d 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" @@ -45,20 +46,23 @@ void CheckJava::executeTask() { auto instance = m_parent->instance(); auto settings = instance->settings(); - m_javaPath = FS::ResolveExecutable(settings->get("JavaPath").toString()); + + QString javaPathSetting = settings->get("JavaPath").toString(); + m_javaPath = FS::ResolveExecutable(javaPathSetting); + bool perInstance = settings->get("OverrideJava").toBool() || settings->get("OverrideJavaLocation").toBool(); auto realJavaPath = QStandardPaths::findExecutable(m_javaPath); if (realJavaPath.isEmpty()) { if (perInstance) { - emit logLine(QString("The java binary \"%1\" couldn't be found. Please fix the java path " + emit logLine(QString("The Java binary \"%1\" couldn't be found. Please fix the Java path " "override in the instance's settings or disable it.") - .arg(m_javaPath), + .arg(javaPathSetting), MessageLevel::Warning); } else { - emit logLine(QString("The java binary \"%1\" couldn't be found. Please set up java in " + emit logLine(QString("The Java binary \"%1\" couldn't be found. Please set up Java in " "the settings.") - .arg(m_javaPath), + .arg(javaPathSetting), MessageLevel::Warning); } emitFailed(QString("Java path is not valid.")); @@ -90,11 +94,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 +109,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 +120,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/launch/steps/LookupServerAddress.cpp b/launcher/launch/steps/LookupServerAddress.cpp index 9bdac203be..4b67b30926 100644 --- a/launcher/launch/steps/LookupServerAddress.cpp +++ b/launcher/launch/steps/LookupServerAddress.cpp @@ -30,7 +30,7 @@ void LookupServerAddress::setLookupAddress(const QString& lookupAddress) m_dnsLookup->setName(QString("_minecraft._tcp.%1").arg(lookupAddress)); } -void LookupServerAddress::setOutputAddressPtr(MinecraftServerTargetPtr output) +void LookupServerAddress::setOutputAddressPtr(MinecraftTarget::Ptr output) { m_output = std::move(output); } diff --git a/launcher/launch/steps/LookupServerAddress.h b/launcher/launch/steps/LookupServerAddress.h index acbd74309c..506314ee8e 100644 --- a/launcher/launch/steps/LookupServerAddress.h +++ b/launcher/launch/steps/LookupServerAddress.h @@ -19,20 +19,20 @@ #include #include -#include "minecraft/launch/MinecraftServerTarget.h" +#include "minecraft/launch/MinecraftTarget.h" class LookupServerAddress : public LaunchStep { Q_OBJECT public: explicit LookupServerAddress(LaunchTask* parent); - virtual ~LookupServerAddress() {}; + virtual ~LookupServerAddress() = default; virtual void executeTask(); virtual bool abort(); virtual bool canAbort() const { return true; } void setLookupAddress(const QString& lookupAddress); - void setOutputAddressPtr(MinecraftServerTargetPtr output); + void setOutputAddressPtr(MinecraftTarget::Ptr output); private slots: void on_dnsLookupFinished(); @@ -42,5 +42,5 @@ class LookupServerAddress : public LaunchStep { QDnsLookup* m_dnsLookup; QString m_lookupAddress; - MinecraftServerTargetPtr m_output; + MinecraftTarget::Ptr m_output; }; diff --git a/launcher/meta/BaseEntity.cpp b/launcher/meta/BaseEntity.cpp index 162af2be48..b0e754ada9 100644 --- a/launcher/meta/BaseEntity.cpp +++ b/launcher/meta/BaseEntity.cpp @@ -15,19 +15,26 @@ #include "BaseEntity.h" +#include "Exception.h" #include "FileSystem.h" #include "Json.h" +#include "modplatform/helpers/HashUtils.h" #include "net/ApiDownload.h" +#include "net/ChecksumValidator.h" #include "net/HttpMetaCache.h" +#include "net/Mode.h" #include "net/NetJob.h" #include "Application.h" #include "BuildConfig.h" +#include "tasks/Task.h" + +namespace Meta { class ParsingValidator : public Net::Validator { public: /* con/des */ - ParsingValidator(Meta::BaseEntity* entity) : m_entity(entity) {}; - virtual ~ParsingValidator() {}; + ParsingValidator(BaseEntity* entity) : m_entity(entity) {}; + virtual ~ParsingValidator() = default; public: /* methods */ bool init(QNetworkRequest&) override @@ -61,92 +68,131 @@ class ParsingValidator : public Net::Validator { private: /* data */ QByteArray m_data; - Meta::BaseEntity* m_entity; + BaseEntity* m_entity; }; -Meta::BaseEntity::~BaseEntity() {} - -QUrl Meta::BaseEntity::url() const +QUrl BaseEntity::url() const { auto s = APPLICATION->settings(); QString metaOverride = s->get("MetaURLOverride").toString(); if (metaOverride.isEmpty()) { return QUrl(BuildConfig.META_URL).resolved(localFilename()); - } else { - return QUrl(metaOverride).resolved(localFilename()); } + return QUrl(metaOverride).resolved(localFilename()); } -bool Meta::BaseEntity::loadLocalFile() +Task::Ptr BaseEntity::loadTask(Net::Mode mode) { - const QString fname = QDir("meta").absoluteFilePath(localFilename()); - if (!QFile::exists(fname)) { - return false; - } - // TODO: check if the file has the expected checksum - try { - auto doc = Json::requireDocument(fname, fname); - auto obj = Json::requireObject(doc, fname); - parse(obj); - return true; - } catch (const Exception& e) { - qDebug() << QString("Unable to parse file %1: %2").arg(fname, e.cause()); - // just make sure it's gone and we never consider it again. - return !FS::deletePath(fname); + if (m_task && m_task->isRunning()) { + return m_task; } + m_task.reset(new BaseEntityLoadTask(this, mode)); + return m_task; +} + +bool BaseEntity::isLoaded() const +{ + // consider it loaded only if the main hash is either empty and was remote loadded or the hashes match and was loaded + return m_sha256.isEmpty() ? m_load_status == LoadStatus::Remote : m_load_status != LoadStatus::NotLoaded && m_sha256 == m_file_sha256; +} + +void BaseEntity::setSha256(QString sha256) +{ + m_sha256 = sha256; +} + +BaseEntity::LoadStatus BaseEntity::status() const +{ + return m_load_status; } -void Meta::BaseEntity::load(Net::Mode loadType) +BaseEntityLoadTask::BaseEntityLoadTask(BaseEntity* parent, Net::Mode mode) : m_entity(parent), m_mode(mode) {} + +void BaseEntityLoadTask::executeTask() { - // load local file if nothing is loaded yet - if (!isLoaded()) { - if (loadLocalFile()) { - m_loadStatus = LoadStatus::Local; + const QString fname = QDir("meta").absoluteFilePath(m_entity->localFilename()); + auto hashMatches = false; + // the file exists on disk try to load it + if (QFile::exists(fname)) { + try { + QByteArray fileData; + // read local file if nothing is loaded yet + if (m_entity->m_load_status == BaseEntity::LoadStatus::NotLoaded || m_entity->m_file_sha256.isEmpty()) { + setStatus(tr("Loading local file")); + fileData = FS::read(fname); + m_entity->m_file_sha256 = Hashing::hash(fileData, Hashing::Algorithm::Sha256); + } + + // on online the hash needs to match + hashMatches = m_entity->m_sha256 == m_entity->m_file_sha256; + if (m_mode == Net::Mode::Online && !m_entity->m_sha256.isEmpty() && !hashMatches) { + throw Exception("mismatched checksum"); + } + + // load local file + if (m_entity->m_load_status == BaseEntity::LoadStatus::NotLoaded) { + auto doc = Json::requireDocument(fileData, fname); + auto obj = Json::requireObject(doc, fname); + m_entity->parse(obj); + m_entity->m_load_status = BaseEntity::LoadStatus::Local; + } + + } catch (const Exception& e) { + qDebug() << QString("Unable to parse file %1: %2").arg(fname, e.cause()); + // just make sure it's gone and we never consider it again. + FS::deletePath(fname); + m_entity->m_load_status = BaseEntity::LoadStatus::NotLoaded; } } // if we need remote update, run the update task - if (loadType == Net::Mode::Offline || !shouldStartRemoteUpdate()) { + auto wasLoadedOffline = m_entity->m_load_status != BaseEntity::LoadStatus::NotLoaded && m_mode == Net::Mode::Offline; + // if has is not present allways fetch from remote(e.g. the main index file), else only fetch if hash doesn't match + auto wasLoadedRemote = m_entity->m_sha256.isEmpty() ? m_entity->m_load_status == BaseEntity::LoadStatus::Remote : hashMatches; + if (wasLoadedOffline || wasLoadedRemote) { + emitSucceeded(); return; } - m_updateTask.reset(new NetJob(QObject::tr("Download of meta file %1").arg(localFilename()), APPLICATION->network())); - auto url = this->url(); - auto entry = APPLICATION->metacache()->resolveEntry("meta", localFilename()); + m_task.reset(new NetJob(QObject::tr("Download of meta file %1").arg(m_entity->localFilename()), APPLICATION->network())); + auto url = m_entity->url(); + auto entry = APPLICATION->metacache()->resolveEntry("meta", m_entity->localFilename()); entry->setStale(true); auto dl = Net::ApiDownload::makeCached(url, entry); /* * The validator parses the file and loads it into the object. * If that fails, the file is not written to storage. */ - dl->addValidator(new ParsingValidator(this)); - m_updateTask->addNetAction(dl); - m_updateStatus = UpdateStatus::InProgress; - QObject::connect(m_updateTask.get(), &NetJob::succeeded, [&]() { - m_loadStatus = LoadStatus::Remote; - m_updateStatus = UpdateStatus::Succeeded; - m_updateTask.reset(); - }); - QObject::connect(m_updateTask.get(), &NetJob::failed, [&]() { - m_updateStatus = UpdateStatus::Failed; - m_updateTask.reset(); + if (!m_entity->m_sha256.isEmpty()) + dl->addValidator(new Net::ChecksumValidator(QCryptographicHash::Algorithm::Sha256, m_entity->m_sha256)); + dl->addValidator(new ParsingValidator(m_entity)); + m_task->addNetAction(dl); + m_task->setAskRetry(false); + connect(m_task.get(), &Task::failed, this, &BaseEntityLoadTask::emitFailed); + connect(m_task.get(), &Task::succeeded, this, &BaseEntityLoadTask::emitSucceeded); + connect(m_task.get(), &Task::succeeded, this, [this]() { + m_entity->m_load_status = BaseEntity::LoadStatus::Remote; + m_entity->m_file_sha256 = m_entity->m_sha256; }); - m_updateTask->start(); -} -bool Meta::BaseEntity::isLoaded() const -{ - return m_loadStatus > LoadStatus::NotLoaded; + connect(m_task.get(), &Task::progress, this, &Task::setProgress); + connect(m_task.get(), &Task::stepProgress, this, &BaseEntityLoadTask::propagateStepProgress); + connect(m_task.get(), &Task::status, this, &Task::setStatus); + connect(m_task.get(), &Task::details, this, &Task::setDetails); + + m_task->start(); } -bool Meta::BaseEntity::shouldStartRemoteUpdate() const +bool BaseEntityLoadTask::canAbort() const { - // TODO: version-locks and offline mode? - return m_updateStatus != UpdateStatus::InProgress; + return m_task ? m_task->canAbort() : false; } -Task::Ptr Meta::BaseEntity::getCurrentTask() +bool BaseEntityLoadTask::abort() { - if (m_updateStatus == UpdateStatus::InProgress) { - return m_updateTask; + if (m_task) { + Task::abort(); + return m_task->abort(); } - return nullptr; + return Task::abort(); } + +} // namespace Meta diff --git a/launcher/meta/BaseEntity.h b/launcher/meta/BaseEntity.h index 1336a5217d..17aa0cb87b 100644 --- a/launcher/meta/BaseEntity.h +++ b/launcher/meta/BaseEntity.h @@ -17,38 +17,57 @@ #include #include -#include "QObjectPtr.h" #include "net/Mode.h" #include "net/NetJob.h" +#include "tasks/Task.h" namespace Meta { +class BaseEntityLoadTask; class BaseEntity { + friend BaseEntityLoadTask; + public: /* types */ using Ptr = std::shared_ptr; enum class LoadStatus { NotLoaded, Local, Remote }; - enum class UpdateStatus { NotDone, InProgress, Failed, Succeeded }; public: - virtual ~BaseEntity(); - - virtual void parse(const QJsonObject& obj) = 0; + virtual ~BaseEntity() = default; virtual QString localFilename() const = 0; virtual QUrl url() const; - bool isLoaded() const; - bool shouldStartRemoteUpdate() const; + LoadStatus status() const; + + /* for parsers */ + void setSha256(QString sha256); + + virtual void parse(const QJsonObject& obj) = 0; + [[nodiscard]] Task::Ptr loadTask(Net::Mode loadType = Net::Mode::Online); - void load(Net::Mode loadType); - Task::Ptr getCurrentTask(); + protected: + QString m_sha256; // the expected sha256 + QString m_file_sha256; // the file sha256 + + private: + LoadStatus m_load_status = LoadStatus::NotLoaded; + Task::Ptr m_task; +}; + +class BaseEntityLoadTask : public Task { + Q_OBJECT + + public: + explicit BaseEntityLoadTask(BaseEntity* parent, Net::Mode mode); + ~BaseEntityLoadTask() override = default; - protected: /* methods */ - bool loadLocalFile(); + virtual void executeTask() override; + virtual bool canAbort() const override; + virtual bool abort() override; private: - LoadStatus m_loadStatus = LoadStatus::NotLoaded; - UpdateStatus m_updateStatus = UpdateStatus::NotDone; - NetJob::Ptr m_updateTask; + BaseEntity* m_entity; + Net::Mode m_mode; + NetJob::Ptr m_task; }; } // namespace Meta diff --git a/launcher/meta/Index.cpp b/launcher/meta/Index.cpp index 657019f8a5..bd0745b6b2 100644 --- a/launcher/meta/Index.cpp +++ b/launcher/meta/Index.cpp @@ -16,7 +16,10 @@ #include "Index.h" #include "JsonFormat.h" +#include "QObjectPtr.h" #include "VersionList.h" +#include "meta/BaseEntity.h" +#include "tasks/SequentialTask.h" namespace Meta { Index::Index(QObject* parent) : QAbstractListModel(parent) {} @@ -51,14 +54,17 @@ QVariant Index::data(const QModelIndex& index, int role) const } return QVariant(); } + int Index::rowCount(const QModelIndex& parent) const { return parent.isValid() ? 0 : m_lists.size(); } + int Index::columnCount(const QModelIndex& parent) const { return parent.isValid() ? 0 : 1; } + QVariant Index::headerData(int section, Qt::Orientation orientation, int role) const { if (orientation == Qt::Horizontal && role == Qt::DisplayRole && section == 0) { @@ -79,6 +85,7 @@ VersionList::Ptr Index::get(const QString& uid) if (!out) { out = std::make_shared(uid); m_uids[uid] = out; + m_lists.append(out); } return out; } @@ -96,7 +103,7 @@ void Index::parse(const QJsonObject& obj) void Index::merge(const std::shared_ptr& other) { - const QVector lists = std::dynamic_pointer_cast(other)->m_lists; + const QVector lists = other->m_lists; // initial load, no need to merge if (m_lists.isEmpty()) { beginResetModel(); @@ -123,7 +130,33 @@ void Index::merge(const std::shared_ptr& other) void Index::connectVersionList(const int row, const VersionList::Ptr& list) { - connect(list.get(), &VersionList::nameChanged, this, - [this, row]() { emit dataChanged(index(row), index(row), QVector() << Qt::DisplayRole); }); + connect(list.get(), &VersionList::nameChanged, this, [this, row] { emit dataChanged(index(row), index(row), { Qt::DisplayRole }); }); +} + +Task::Ptr Index::loadVersion(const QString& uid, const QString& version, Net::Mode mode, bool force) +{ + if (mode == Net::Mode::Offline) { + return get(uid, version)->loadTask(mode); + } + + auto versionList = get(uid); + auto loadTask = makeShared( + this, tr("Load meta for %1:%2", "This is for the task name that loads the meta index.").arg(uid, version)); + if (status() != BaseEntity::LoadStatus::Remote || force) { + loadTask->addTask(this->loadTask(mode)); + } + loadTask->addTask(versionList->loadTask(mode)); + loadTask->addTask(versionList->getVersion(version)->loadTask(mode)); + return loadTask; +} + +Version::Ptr Index::getLoadedVersion(const QString& uid, const QString& version) +{ + QEventLoop ev; + auto task = loadVersion(uid, version); + QObject::connect(task.get(), &Task::finished, &ev, &QEventLoop::quit); + task->start(); + ev.exec(); + return get(uid, version); } } // namespace Meta diff --git a/launcher/meta/Index.h b/launcher/meta/Index.h index 2c650ce2f9..026a00c07e 100644 --- a/launcher/meta/Index.h +++ b/launcher/meta/Index.h @@ -16,10 +16,10 @@ #pragma once #include -#include #include "BaseEntity.h" #include "meta/VersionList.h" +#include "net/Mode.h" class Task; @@ -30,6 +30,7 @@ class Index : public QAbstractListModel, public BaseEntity { public: explicit Index(QObject* parent = nullptr); explicit Index(const QVector& lists, QObject* parent = nullptr); + virtual ~Index() = default; enum { UidRole = Qt::UserRole, NameRole, ListPtrRole }; @@ -47,8 +48,15 @@ class Index : public QAbstractListModel, public BaseEntity { QVector lists() const { return m_lists; } + Task::Ptr loadVersion(const QString& uid, const QString& version = {}, Net::Mode mode = Net::Mode::Online, bool force = false); + + // this blocks until the version is loaded + Version::Ptr getLoadedVersion(const QString& uid, const QString& version); + public: // for usage by parsers only void merge(const std::shared_ptr& other); + + protected: void parse(const QJsonObject& obj) override; private: diff --git a/launcher/meta/JsonFormat.cpp b/launcher/meta/JsonFormat.cpp index 6c993f7200..86af7277e4 100644 --- a/launcher/meta/JsonFormat.cpp +++ b/launcher/meta/JsonFormat.cpp @@ -41,6 +41,7 @@ static std::shared_ptr parseIndexInternal(const QJsonObject& obj) std::transform(objects.begin(), objects.end(), std::back_inserter(lists), [](const QJsonObject& obj) { VersionList::Ptr list = std::make_shared(requireString(obj, "uid")); list->setName(ensureString(obj, "name", QString())); + list->setSha256(ensureString(obj, "sha256", QString())); return list; }); return std::make_shared(lists); @@ -58,6 +59,9 @@ static Version::Ptr parseCommonVersion(const QString& uid, const QJsonObject& ob parseRequires(obj, &reqs, "requires"); parseRequires(obj, &conflicts, "conflicts"); version->setRequires(reqs, conflicts); + if (auto sha256 = ensureString(obj, "sha256", QString()); !sha256.isEmpty()) { + version->setSha256(sha256); + } return version; } diff --git a/launcher/meta/JsonFormat.h b/launcher/meta/JsonFormat.h index d474bcc395..7fbf808a75 100644 --- a/launcher/meta/JsonFormat.h +++ b/launcher/meta/JsonFormat.h @@ -16,11 +16,9 @@ #pragma once #include -#include #include #include "Exception.h" -#include "meta/BaseEntity.h" namespace Meta { class Index; diff --git a/launcher/meta/Version.cpp b/launcher/meta/Version.cpp index 655a20b93c..74e71e91c9 100644 --- a/launcher/meta/Version.cpp +++ b/launcher/meta/Version.cpp @@ -18,12 +18,9 @@ #include #include "JsonFormat.h" -#include "minecraft/PackProfile.h" Meta::Version::Version(const QString& uid, const QString& version) : BaseVersion(), m_uid(uid), m_version(version) {} -Meta::Version::~Version() {} - QString Meta::Version::descriptor() { return m_version; @@ -71,6 +68,9 @@ void Meta::Version::mergeFromList(const Meta::Version::Ptr& other) if (m_volatile != other->m_volatile) { setVolatile(other->m_volatile); } + if (!other->m_sha256.isEmpty()) { + m_sha256 = other->m_sha256; + } } void Meta::Version::merge(const Version::Ptr& other) diff --git a/launcher/meta/Version.h b/launcher/meta/Version.h index 24da12d6d1..46dc740da9 100644 --- a/launcher/meta/Version.h +++ b/launcher/meta/Version.h @@ -38,7 +38,7 @@ class Version : public QObject, public BaseVersion, public BaseEntity { using Ptr = std::shared_ptr; explicit Version(const QString& uid, const QString& version); - virtual ~Version(); + virtual ~Version() = default; QString descriptor() override; QString name() override; @@ -52,7 +52,7 @@ class Version : public QObject, public BaseVersion, public BaseEntity { const Meta::RequireSet& requiredSet() const { return m_requires; } VersionFilePtr data() const { return m_data; } bool isRecommended() const { return m_recommended; } - bool isLoaded() const { return m_data != nullptr; } + bool isLoaded() const { return m_data != nullptr && BaseEntity::isLoaded(); } void merge(const Version::Ptr& other); void mergeFromList(const Version::Ptr& other); diff --git a/launcher/meta/VersionList.cpp b/launcher/meta/VersionList.cpp index 7b7ae1fa32..8b98223d15 100644 --- a/launcher/meta/VersionList.cpp +++ b/launcher/meta/VersionList.cpp @@ -17,8 +17,13 @@ #include +#include "Application.h" +#include "Index.h" #include "JsonFormat.h" #include "Version.h" +#include "meta/BaseEntity.h" +#include "net/Mode.h" +#include "tasks/SequentialTask.h" namespace Meta { VersionList::VersionList(const QString& uid, QObject* parent) : BaseVersionList(parent), m_uid(uid) @@ -28,8 +33,11 @@ VersionList::VersionList(const QString& uid, QObject* parent) : BaseVersionList( Task::Ptr VersionList::getLoadTask() { - load(Net::Mode::Online); - return getCurrentTask(); + auto loadTask = + makeShared(this, tr("Load meta for %1", "This is for the task name that loads the meta index.").arg(m_uid)); + loadTask->addTask(APPLICATION->metadataIndex()->loadTask(Net::Mode::Online)); + loadTask->addTask(this->loadTask(Net::Mode::Online)); + return loadTask; } bool VersionList::isLoaded() @@ -92,6 +100,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 +116,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(); @@ -131,6 +150,8 @@ Version::Ptr VersionList::getVersion(const QString& version) if (!out) { out = std::make_shared(m_uid, version); m_lookup[version] = out; + setupAddedVersion(m_versions.size(), out); + m_versions.append(out); } return out; } @@ -191,6 +212,9 @@ void VersionList::mergeFromIndex(const VersionList::Ptr& other) if (m_name != other->m_name) { setName(other->m_name); } + if (!other->m_sha256.isEmpty()) { + m_sha256 = other->m_sha256; + } } void VersionList::merge(const VersionList::Ptr& other) @@ -198,23 +222,27 @@ void VersionList::merge(const VersionList::Ptr& other) if (m_name != other->m_name) { setName(other->m_name); } + if (!other->m_sha256.isEmpty()) { + m_sha256 = other->m_sha256; + } // TODO: do not reset the whole model. maybe? beginResetModel(); - m_versions.clear(); if (other->m_versions.isEmpty()) { qWarning() << "Empty list loaded ..."; } - for (const Version::Ptr& version : other->m_versions) { + for (auto version : other->m_versions) { // we already have the version. merge the contents if (m_lookup.contains(version->version())) { - m_lookup.value(version->version())->mergeFromList(version); + auto existing = m_lookup.value(version->version()); + existing->mergeFromList(version); + version = existing; } else { - m_lookup.insert(version->uid(), version); + m_lookup.insert(version->version(), version); + // connect it. + setupAddedVersion(m_versions.size(), version); + m_versions.append(version); } - // connect it. - setupAddedVersion(m_versions.size(), version); - m_versions.append(version); m_recommended = getBetterVersion(m_recommended, version); } endResetModel(); @@ -222,14 +250,15 @@ void VersionList::merge(const VersionList::Ptr& other) void VersionList::setupAddedVersion(const int row, const Version::Ptr& version) { - // FIXME: do not disconnect from everythin, disconnect only the lambdas here - version->disconnect(); + disconnect(version.get(), &Version::requiresChanged, this, nullptr); + disconnect(version.get(), &Version::timeChanged, this, nullptr); + disconnect(version.get(), &Version::typeChanged, this, nullptr); + connect(version.get(), &Version::requiresChanged, this, [this, row]() { emit dataChanged(index(row), index(row), QVector() << RequiresRole); }); connect(version.get(), &Version::timeChanged, this, - [this, row]() { emit dataChanged(index(row), index(row), QVector() << TimeRole << SortRole); }); - connect(version.get(), &Version::typeChanged, this, - [this, row]() { emit dataChanged(index(row), index(row), QVector() << TypeRole); }); + [this, row]() { emit dataChanged(index(row), index(row), { TimeRole, SortRole }); }); + connect(version.get(), &Version::typeChanged, this, [this, row]() { emit dataChanged(index(row), index(row), { TypeRole }); }); } BaseVersion::Ptr VersionList::getRecommended() const @@ -237,4 +266,14 @@ BaseVersion::Ptr VersionList::getRecommended() const return m_recommended; } +void VersionList::waitToLoad() +{ + if (isLoaded()) + return; + QEventLoop ev; + auto task = getLoadTask(); + QObject::connect(task.get(), &Task::finished, &ev, &QEventLoop::quit); + task->start(); + ev.exec(); +} } // namespace Meta diff --git a/launcher/meta/VersionList.h b/launcher/meta/VersionList.h index 2c5624701b..94aaae22c1 100644 --- a/launcher/meta/VersionList.h +++ b/launcher/meta/VersionList.h @@ -30,13 +30,14 @@ class VersionList : public BaseVersionList, public BaseEntity { Q_PROPERTY(QString name READ name NOTIFY nameChanged) public: explicit VersionList(const QString& uid, QObject* parent = nullptr); + virtual ~VersionList() = default; using Ptr = std::shared_ptr; enum Roles { UidRole = Qt::UserRole + 100, TimeRole, RequiresRole, VersionPtrRole }; - Task::Ptr getLoadTask() override; bool isLoaded() override; + [[nodiscard]] Task::Ptr getLoadTask() override; const BaseVersion::Ptr at(int i) const override; int count() const override; void sortVersions() override; @@ -47,6 +48,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; } @@ -58,6 +61,9 @@ class VersionList : public BaseVersionList, public BaseEntity { QVector versions() const { return m_versions; } + // this blocks until the version list is loaded + void waitToLoad(); + public: // for usage only by parsers void setName(const QString& name); void setVersions(const QVector& versions); @@ -79,6 +85,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/AssetsUtils.cpp b/launcher/minecraft/AssetsUtils.cpp index 6bbe0bb2c3..4406d9b344 100644 --- a/launcher/minecraft/AssetsUtils.cpp +++ b/launcher/minecraft/AssetsUtils.cpp @@ -283,8 +283,7 @@ Net::NetRequest::Ptr AssetObject::getDownloadAction() if ((!objectFile.isFile()) || (objectFile.size() != size)) { auto objectDL = Net::ApiDownload::makeFile(getUrl(), objectFile.filePath()); if (hash.size()) { - auto rawHash = QByteArray::fromHex(hash.toLatin1()); - objectDL->addValidator(new Net::ChecksumValidator(QCryptographicHash::Sha1, rawHash)); + objectDL->addValidator(new Net::ChecksumValidator(QCryptographicHash::Sha1, hash)); } objectDL->setProgress(objectDL->getProgress(), size); return objectDL; diff --git a/launcher/minecraft/Component.cpp b/launcher/minecraft/Component.cpp index ad2e4023cf..32a1deb684 100644 --- a/launcher/minecraft/Component.cpp +++ b/launcher/minecraft/Component.cpp @@ -56,18 +56,6 @@ Component::Component(PackProfile* parent, const QString& uid) m_uid = uid; } -Component::Component(PackProfile* parent, std::shared_ptr version) -{ - assert(parent); - m_parent = parent; - - m_metaVersion = version; - m_uid = version->uid(); - m_version = m_cachedVersion = version->version(); - m_cachedName = version->name(); - m_loaded = version->isLoaded(); -} - Component::Component(PackProfile* parent, const QString& uid, std::shared_ptr file) { assert(parent); @@ -102,9 +90,6 @@ void Component::applyTo(LaunchProfile* profile) std::shared_ptr Component::getVersionFile() const { if (m_metaVersion) { - if (!m_metaVersion->isLoaded()) { - m_metaVersion->load(Net::Mode::Online); - } return m_metaVersion->data(); } else { return m_file; @@ -131,29 +116,35 @@ int Component::getOrder() } return 0; } + void Component::setOrder(int order) { m_orderOverride = true; m_order = order; } + QString Component::getID() { return m_uid; } + QString Component::getName() { if (!m_cachedName.isEmpty()) return m_cachedName; return m_uid; } + QString Component::getVersion() { return m_cachedVersion; } + QString Component::getFilename() { return m_parent->patchFilePathForUid(m_uid); } + QDateTime Component::getReleaseDateTime() { if (m_metaVersion) { @@ -198,17 +189,14 @@ bool Component::isCustom() bool Component::isCustomizable() { - if (m_metaVersion) { - if (getVersionFile()) { - return true; - } - } - return false; + return m_metaVersion && getVersionFile(); } + bool Component::isRemovable() { return !m_important; } + bool Component::isRevertible() { if (isCustom()) { @@ -218,18 +206,18 @@ bool Component::isRevertible() } return false; } + bool Component::isMoveable() { // HACK, FIXME: this was too dumb and wouldn't follow dependency constraints anyway. For now hardcoded to 'true'. return true; } + bool Component::isVersionChangeable() { auto list = getVersionList(); if (list) { - if (!list->isLoaded()) { - list->load(Net::Mode::Online); - } + list->waitToLoad(); return list->count() != 0; } return false; diff --git a/launcher/minecraft/Component.h b/launcher/minecraft/Component.h index fdb61c45eb..8aa6b4743e 100644 --- a/launcher/minecraft/Component.h +++ b/launcher/minecraft/Component.h @@ -22,7 +22,6 @@ class Component : public QObject, public ProblemProvider { Component(PackProfile* parent, const QString& uid); // DEPRECATED: remove these constructors? - Component(PackProfile* parent, std::shared_ptr version); Component(PackProfile* parent, const QString& uid, std::shared_ptr file); virtual ~Component() {} diff --git a/launcher/minecraft/ComponentUpdateTask.cpp b/launcher/minecraft/ComponentUpdateTask.cpp index bb838043a9..4d205af6c8 100644 --- a/launcher/minecraft/ComponentUpdateTask.cpp +++ b/launcher/minecraft/ComponentUpdateTask.cpp @@ -13,6 +13,7 @@ #include "net/Mode.h" #include "Application.h" +#include "tasks/Task.h" /* * This is responsible for loading the components of a component list AND resolving dependency issues between them @@ -93,9 +94,9 @@ static LoadResult loadComponent(ComponentPtr component, Task::Ptr& loadTask, Net component->m_loaded = true; result = LoadResult::LoadedLocal; } else { - metaVersion->load(netmode); - loadTask = metaVersion->getCurrentTask(); - if (loadTask) + loadTask = APPLICATION->metadataIndex()->loadVersion(component->m_uid, component->m_version, netmode); + loadTask->start(); + if (netmode == Net::Mode::Online) result = LoadResult::RequiresRemote; else if (metaVersion->isLoaded()) result = LoadResult::LoadedLocal; @@ -133,21 +134,6 @@ static LoadResult loadPackProfile(ComponentPtr component, Task::Ptr& loadTask, N } */ -static LoadResult loadIndex(Task::Ptr& loadTask, Net::Mode netmode) -{ - // FIXME: DECIDE. do we want to run the update task anyway? - if (APPLICATION->metadataIndex()->isLoaded()) { - qDebug() << "Index is already loaded"; - return LoadResult::LoadedLocal; - } - APPLICATION->metadataIndex()->load(netmode); - loadTask = APPLICATION->metadataIndex()->getCurrentTask(); - if (loadTask) { - return LoadResult::RequiresRemote; - } - // FIXME: this is assuming the load succeeded... did it really? - return LoadResult::LoadedLocal; -} } // namespace void ComponentUpdateTask::loadComponents() @@ -156,23 +142,7 @@ void ComponentUpdateTask::loadComponents() size_t taskIndex = 0; size_t componentIndex = 0; d->remoteLoadSuccessful = true; - // load the main index (it is needed to determine if components can revert) - { - // FIXME: tear out as a method? or lambda? - Task::Ptr indexLoadTask; - auto singleResult = loadIndex(indexLoadTask, d->netmode); - result = composeLoadResult(result, singleResult); - if (indexLoadTask) { - qDebug() << "Remote loading is being run for metadata index"; - RemoteLoadStatus status; - status.type = RemoteLoadStatus::Type::Index; - d->remoteLoadStatusList.append(status); - connect(indexLoadTask.get(), &Task::succeeded, [=]() { remoteLoadSucceeded(taskIndex); }); - connect(indexLoadTask.get(), &Task::failed, [=](const QString& error) { remoteLoadFailed(taskIndex, error); }); - connect(indexLoadTask.get(), &Task::aborted, [=]() { remoteLoadFailed(taskIndex, tr("Aborted")); }); - taskIndex++; - } - } + // load all the components OR their lists... for (auto component : d->m_list->d->components) { Task::Ptr loadTask; @@ -206,12 +176,13 @@ void ComponentUpdateTask::loadComponents() result = composeLoadResult(result, singleResult); if (loadTask) { qDebug() << "Remote loading is being run for" << component->getName(); - connect(loadTask.get(), &Task::succeeded, [=]() { remoteLoadSucceeded(taskIndex); }); - connect(loadTask.get(), &Task::failed, [=](const QString& error) { remoteLoadFailed(taskIndex, error); }); - connect(loadTask.get(), &Task::aborted, [=]() { remoteLoadFailed(taskIndex, tr("Aborted")); }); + connect(loadTask.get(), &Task::succeeded, this, [this, taskIndex]() { remoteLoadSucceeded(taskIndex); }); + connect(loadTask.get(), &Task::failed, this, [this, taskIndex](const QString& error) { remoteLoadFailed(taskIndex, error); }); + connect(loadTask.get(), &Task::aborted, this, [this, taskIndex]() { remoteLoadFailed(taskIndex, tr("Aborted")); }); RemoteLoadStatus status; status.type = loadType; status.PackProfileIndex = componentIndex; + status.task = loadTask; d->remoteLoadStatusList.append(status); taskIndex++; } @@ -518,7 +489,14 @@ void ComponentUpdateTask::resolveDependencies(bool checkOnly) void ComponentUpdateTask::remoteLoadSucceeded(size_t taskIndex) { + if (static_cast(d->remoteLoadStatusList.size()) < taskIndex) { + qWarning() << "Got task index outside of results" << taskIndex; + return; + } auto& taskSlot = d->remoteLoadStatusList[taskIndex]; + disconnect(taskSlot.task.get(), &Task::succeeded, this, nullptr); + disconnect(taskSlot.task.get(), &Task::failed, this, nullptr); + disconnect(taskSlot.task.get(), &Task::aborted, this, nullptr); if (taskSlot.finished) { qWarning() << "Got multiple results from remote load task" << taskIndex; return; @@ -538,7 +516,14 @@ void ComponentUpdateTask::remoteLoadSucceeded(size_t taskIndex) void ComponentUpdateTask::remoteLoadFailed(size_t taskIndex, const QString& msg) { + if (static_cast(d->remoteLoadStatusList.size()) < taskIndex) { + qWarning() << "Got task index outside of results" << taskIndex; + return; + } auto& taskSlot = d->remoteLoadStatusList[taskIndex]; + disconnect(taskSlot.task.get(), &Task::succeeded, this, nullptr); + disconnect(taskSlot.task.get(), &Task::failed, this, nullptr); + disconnect(taskSlot.task.get(), &Task::aborted, this, nullptr); if (taskSlot.finished) { qWarning() << "Got multiple results from remote load task" << taskIndex; return; diff --git a/launcher/minecraft/ComponentUpdateTask_p.h b/launcher/minecraft/ComponentUpdateTask_p.h index 00e8f2fbe4..b825537009 100644 --- a/launcher/minecraft/ComponentUpdateTask_p.h +++ b/launcher/minecraft/ComponentUpdateTask_p.h @@ -4,6 +4,7 @@ #include #include #include "net/Mode.h" +#include "tasks/Task.h" class PackProfile; @@ -13,6 +14,7 @@ struct RemoteLoadStatus { bool finished = false; bool succeeded = false; QString error; + Task::Ptr task; }; struct ComponentUpdateTaskData { 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/Library.cpp b/launcher/minecraft/Library.cpp index a20d6c5152..4f04f0eb93 100644 --- a/launcher/minecraft/Library.cpp +++ b/launcher/minecraft/Library.cpp @@ -147,9 +147,8 @@ QList Library::getDownloads(const RuntimeContext& runtimeC options |= Net::Download::Option::MakeEternal; if (sha1.size()) { - auto rawSha1 = QByteArray::fromHex(sha1.toLatin1()); auto dl = Net::ApiDownload::makeCached(url, entry, options); - dl->addValidator(new Net::ChecksumValidator(QCryptographicHash::Sha1, rawSha1)); + dl->addValidator(new Net::ChecksumValidator(QCryptographicHash::Sha1, sha1)); qDebug() << "Checksummed Download for:" << rawName().serialize() << "storage:" << storage << "url:" << url; out.append(dl); } else { diff --git a/launcher/minecraft/MinecraftInstance.cpp b/launcher/minecraft/MinecraftInstance.cpp index d119104fe7..d861056bf1 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,21 @@ 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); + m_settings->registerSetting("AutomaticJava", false); 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); @@ -196,8 +194,9 @@ void MinecraftInstance::loadSpecificSettings() } // Join server on launch, this does not have a global override - m_settings->registerSetting("JoinServerOnLaunch", false); + m_settings->registerSetting({ "JoinServerOnLaunch", "JoinOnLaunch" }, false); m_settings->registerSetting("JoinServerOnLaunchAddress", ""); + m_settings->registerSetting("JoinWorldOnLaunch", ""); // Use account for instance, this does not have a global override m_settings->registerSetting("UseAccountForInstance", false); @@ -523,8 +522,7 @@ QStringList MinecraftInstance::javaArguments() if (javaVersion.isModular() && shouldApplyOnlineFixes()) // allow reflective access to java.net - required by the skin fix - args << "--add-opens" - << "java.base/java.net=ALL-UNNAMED"; + args << "--add-opens" << "java.base/java.net=ALL-UNNAMED"; return args; } @@ -608,7 +606,7 @@ QProcessEnvironment MinecraftInstance::createLaunchEnvironment() // dlsym variant is only needed for OpenGL and not included in the vulkan layer appendLib("libMangoHud_dlsym.so"); appendLib("libMangoHud_opengl.so"); - appendLib(mangoHudLib.fileName()); + preloadList << mangoHudLibString; } env.insert("LD_PRELOAD", preloadList.join(QLatin1String(":"))); @@ -656,7 +654,7 @@ static QString replaceTokensIn(QString text, QMap with) return result; } -QStringList MinecraftInstance::processMinecraftArgs(AuthSessionPtr session, MinecraftServerTargetPtr serverToJoin) const +QStringList MinecraftInstance::processMinecraftArgs(AuthSessionPtr session, MinecraftTarget::Ptr targetToJoin) const { auto profile = m_components->getProfile(); QString args_pattern = profile->getMinecraftArguments(); @@ -664,12 +662,16 @@ QStringList MinecraftInstance::processMinecraftArgs(AuthSessionPtr session, Mine args_pattern += " --tweakClass " + tweaker; } - if (serverToJoin && !serverToJoin->address.isEmpty()) { - if (profile->hasTrait("feature:is_quick_play_multiplayer")) { - args_pattern += " --quickPlayMultiplayer " + serverToJoin->address + ':' + QString::number(serverToJoin->port); - } else { - args_pattern += " --server " + serverToJoin->address; - args_pattern += " --port " + QString::number(serverToJoin->port); + if (targetToJoin) { + if (!targetToJoin->address.isEmpty()) { + if (profile->hasTrait("feature:is_quick_play_multiplayer")) { + args_pattern += " --quickPlayMultiplayer " + targetToJoin->address + ':' + QString::number(targetToJoin->port); + } else { + args_pattern += " --server " + targetToJoin->address; + args_pattern += " --port " + QString::number(targetToJoin->port); + } + } else if (!targetToJoin->world.isEmpty() && profile->hasTrait("feature:is_quick_play_singleplayer")) { + args_pattern += " --quickPlaySingleplayer " + targetToJoin->world; } } @@ -713,7 +715,7 @@ QStringList MinecraftInstance::processMinecraftArgs(AuthSessionPtr session, Mine return parts; } -QString MinecraftInstance::createLaunchScript(AuthSessionPtr session, MinecraftServerTargetPtr serverToJoin) +QString MinecraftInstance::createLaunchScript(AuthSessionPtr session, MinecraftTarget::Ptr targetToJoin) { QString launchScript; @@ -732,9 +734,13 @@ QString MinecraftInstance::createLaunchScript(AuthSessionPtr session, MinecraftS launchScript += "appletClass " + appletClass + "\n"; } - if (serverToJoin && !serverToJoin->address.isEmpty()) { - launchScript += "serverAddress " + serverToJoin->address + "\n"; - launchScript += "serverPort " + QString::number(serverToJoin->port) + "\n"; + if (targetToJoin) { + if (!targetToJoin->address.isEmpty()) { + launchScript += "serverAddress " + targetToJoin->address + "\n"; + launchScript += "serverPort " + QString::number(targetToJoin->port) + "\n"; + } else if (!targetToJoin->world.isEmpty()) { + launchScript += "worldName " + targetToJoin->world + "\n"; + } } // generic minecraft params @@ -787,16 +793,15 @@ QString MinecraftInstance::createLaunchScript(AuthSessionPtr session, MinecraftS return launchScript; } -QStringList MinecraftInstance::verboseDescription(AuthSessionPtr session, MinecraftServerTargetPtr serverToJoin) +QStringList MinecraftInstance::verboseDescription(AuthSessionPtr session, MinecraftTarget::Ptr targetToJoin) { QStringList out; - out << "Main Class:" - << " " + getMainClass() << ""; - out << "Native path:" - << " " + getNativePath() << ""; + out << "Main Class:" << " " + getMainClass() << ""; + out << "Native path:" << " " + getNativePath() << ""; auto profile = m_components->getProfile(); + // traits auto alltraits = traits(); if (alltraits.size()) { out << "Traits:"; @@ -806,6 +811,7 @@ QStringList MinecraftInstance::verboseDescription(AuthSessionPtr session, Minecr out << ""; } + // native libraries auto settings = this->settings(); bool nativeOpenAL = settings->get("UseNativeOpenAL").toBool(); bool nativeGLFW = settings->get("UseNativeGLFW").toBool(); @@ -841,6 +847,7 @@ QStringList MinecraftInstance::verboseDescription(AuthSessionPtr session, Minecr out << ""; } + // mods and core mods auto printModList = [&](const QString& label, ModFolderModel& model) { if (model.size()) { out << QString("%1:").arg(label); @@ -869,6 +876,7 @@ QStringList MinecraftInstance::verboseDescription(AuthSessionPtr session, Minecr printModList("Mods", *(loaderModList().get())); printModList("Core Mods", *(coreModList().get())); + // jar mods auto& jarMods = profile->getJarMods(); if (jarMods.size()) { out << "Jar Mods:"; @@ -884,11 +892,13 @@ QStringList MinecraftInstance::verboseDescription(AuthSessionPtr session, Minecr out << ""; } - auto params = processMinecraftArgs(nullptr, serverToJoin); + // minecraft arguments + auto params = processMinecraftArgs(nullptr, targetToJoin); out << "Params:"; out << " " + params.join(' '); out << ""; + // window size QString windowParams; if (settings->get("LaunchMaximized").toBool()) { out << "Window size: max (if available)"; @@ -1034,7 +1044,7 @@ Task::Ptr MinecraftInstance::createUpdateTask(Net::Mode mode) return nullptr; } -shared_qobject_ptr MinecraftInstance::createLaunchTask(AuthSessionPtr session, MinecraftServerTargetPtr serverToJoin) +shared_qobject_ptr MinecraftInstance::createLaunchTask(AuthSessionPtr session, MinecraftTarget::Ptr targetToJoin) { updateRuntimeContext(); // FIXME: get rid of shared_from_this ... @@ -1048,26 +1058,28 @@ 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)); } - if (!serverToJoin && settings()->get("JoinServerOnLaunch").toBool()) { + if (!targetToJoin && settings()->get("JoinOnLaunch").toBool()) { QString fullAddress = settings()->get("JoinServerOnLaunchAddress").toString(); - serverToJoin.reset(new MinecraftServerTarget(MinecraftServerTarget::parse(fullAddress))); + if (!fullAddress.isEmpty()) { + targetToJoin.reset(new MinecraftTarget(MinecraftTarget::parse(fullAddress, false))); + } else { + QString world = settings()->get("JoinWorldOnLaunch").toString(); + if (!world.isEmpty()) { + targetToJoin.reset(new MinecraftTarget(MinecraftTarget::parse(world, true))); + } + } } - if (serverToJoin && serverToJoin->port == 25565) { + if (targetToJoin && targetToJoin->port == 25565) { // Resolve server address to join on launch auto step = makeShared(pptr); - step->setLookupAddress(serverToJoin->address); - step->setOutputAddressPtr(serverToJoin); + step->setLookupAddress(targetToJoin->address); + step->setOutputAddressPtr(targetToJoin); process->appendStep(step); } @@ -1088,6 +1100,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)); @@ -1100,7 +1118,7 @@ shared_qobject_ptr MinecraftInstance::createLaunchTask(AuthSessionPt // print some instance info here... { - process->appendStep(makeShared(pptr, session, serverToJoin)); + process->appendStep(makeShared(pptr, session, targetToJoin)); } // extract native jars if needed @@ -1123,7 +1141,7 @@ shared_qobject_ptr MinecraftInstance::createLaunchTask(AuthSessionPt auto step = makeShared(pptr); step->setWorkingDirectory(gameRoot()); step->setAuthSession(session); - step->setServerToJoin(serverToJoin); + step->setTargetToJoin(targetToJoin); process->appendStep(step); } diff --git a/launcher/minecraft/MinecraftInstance.h b/launcher/minecraft/MinecraftInstance.h index 7af0df3893..ad2cda186a 100644 --- a/launcher/minecraft/MinecraftInstance.h +++ b/launcher/minecraft/MinecraftInstance.h @@ -39,7 +39,7 @@ #include #include #include "BaseInstance.h" -#include "minecraft/launch/MinecraftServerTarget.h" +#include "minecraft/launch/MinecraftTarget.h" #include "minecraft/mod/Mod.h" class ModFolderModel; @@ -56,7 +56,7 @@ class MinecraftInstance : public BaseInstance { Q_OBJECT public: MinecraftInstance(SettingsObjectPtr globalSettings, SettingsObjectPtr settings, const QString& rootDir); - virtual ~MinecraftInstance() {}; + virtual ~MinecraftInstance() = default; virtual void saveNow() override; void loadSpecificSettings() override; @@ -121,11 +121,11 @@ class MinecraftInstance : public BaseInstance { ////// Launch stuff ////// Task::Ptr createUpdateTask(Net::Mode mode) override; - shared_qobject_ptr createLaunchTask(AuthSessionPtr account, MinecraftServerTargetPtr serverToJoin) override; + shared_qobject_ptr createLaunchTask(AuthSessionPtr account, MinecraftTarget::Ptr targetToJoin) override; QStringList extraArguments() override; - QStringList verboseDescription(AuthSessionPtr session, MinecraftServerTargetPtr serverToJoin) override; + QStringList verboseDescription(AuthSessionPtr session, MinecraftTarget::Ptr targetToJoin) override; QList getJarMods() const; - QString createLaunchScript(AuthSessionPtr session, MinecraftServerTargetPtr serverToJoin); + QString createLaunchScript(AuthSessionPtr session, MinecraftTarget::Ptr targetToJoin); /// get arguments passed to java QStringList javaArguments(); QString getLauncher(); @@ -155,7 +155,7 @@ class MinecraftInstance : public BaseInstance { virtual QString getMainClass() const; // FIXME: remove - virtual QStringList processMinecraftArgs(AuthSessionPtr account, MinecraftServerTargetPtr serverToJoin) const; + virtual QStringList processMinecraftArgs(AuthSessionPtr account, MinecraftTarget::Ptr targetToJoin) const; virtual JavaVersion getJavaVersion(); diff --git a/launcher/minecraft/MinecraftUpdate.cpp b/launcher/minecraft/MinecraftUpdate.cpp index c009317a6b..b63430aa87 100644 --- a/launcher/minecraft/MinecraftUpdate.cpp +++ b/launcher/minecraft/MinecraftUpdate.cpp @@ -16,32 +16,22 @@ #include "MinecraftUpdate.h" #include "MinecraftInstance.h" -#include -#include -#include -#include - -#include -#include "BaseInstance.h" -#include "minecraft/Library.h" #include "minecraft/PackProfile.h" +#include "tasks/SequentialTask.h" #include "update/AssetUpdateTask.h" #include "update/FMLLibrariesTask.h" #include "update/FoldersTask.h" #include "update/LibrariesTask.h" -#include -#include - -MinecraftUpdate::MinecraftUpdate(MinecraftInstance* inst, QObject* parent) : Task(parent), m_inst(inst) {} +MinecraftUpdate::MinecraftUpdate(MinecraftInstance* inst, QObject* parent) : SequentialTask(parent), m_inst(inst) {} void MinecraftUpdate::executeTask() { - m_tasks.clear(); + m_queue.clear(); // create folders { - m_tasks.append(makeShared(m_inst)); + addTask(makeShared(m_inst)); } // add metadata update task if necessary @@ -50,121 +40,24 @@ void MinecraftUpdate::executeTask() components->reload(Net::Mode::Online); auto task = components->getCurrentTask(); if (task) { - m_tasks.append(task); + addTask(task); } } // libraries download { - m_tasks.append(makeShared(m_inst)); + addTask(makeShared(m_inst)); } // FML libraries download and copy into the instance { - m_tasks.append(makeShared(m_inst)); + addTask(makeShared(m_inst)); } // assets update { - m_tasks.append(makeShared(m_inst)); - } - - if (!m_preFailure.isEmpty()) { - emitFailed(m_preFailure); - return; - } - next(); -} - -void MinecraftUpdate::next() -{ - if (m_abort) { - emitFailed(tr("Aborted by user.")); - return; - } - if (m_failed_out_of_order) { - emitFailed(m_fail_reason); - return; - } - m_currentTask++; - if (m_currentTask > 0) { - auto task = m_tasks[m_currentTask - 1]; - disconnect(task.get(), &Task::succeeded, this, &MinecraftUpdate::subtaskSucceeded); - disconnect(task.get(), &Task::failed, this, &MinecraftUpdate::subtaskFailed); - disconnect(task.get(), &Task::aborted, this, &Task::abort); - disconnect(task.get(), &Task::progress, this, &MinecraftUpdate::progress); - disconnect(task.get(), &Task::stepProgress, this, &MinecraftUpdate::propagateStepProgress); - disconnect(task.get(), &Task::status, this, &MinecraftUpdate::setStatus); - disconnect(task.get(), &Task::details, this, &MinecraftUpdate::setDetails); - } - if (m_currentTask == m_tasks.size()) { - emitSucceeded(); - return; - } - auto task = m_tasks[m_currentTask]; - // if the task is already finished by the time we look at it, skip it - if (task->isFinished()) { - qCritical() << "MinecraftUpdate: Skipping finished subtask" << m_currentTask << ":" << task.get(); - next(); - } - connect(task.get(), &Task::succeeded, this, &MinecraftUpdate::subtaskSucceeded); - connect(task.get(), &Task::failed, this, &MinecraftUpdate::subtaskFailed); - connect(task.get(), &Task::aborted, this, &Task::abort); - connect(task.get(), &Task::progress, this, &MinecraftUpdate::progress); - connect(task.get(), &Task::stepProgress, this, &MinecraftUpdate::propagateStepProgress); - connect(task.get(), &Task::status, this, &MinecraftUpdate::setStatus); - connect(task.get(), &Task::details, this, &MinecraftUpdate::setDetails); - // if the task is already running, do not start it again - if (!task->isRunning()) { - task->start(); - } -} - -void MinecraftUpdate::subtaskSucceeded() -{ - if (isFinished()) { - qCritical() << "MinecraftUpdate: Subtask" << sender() << "succeeded, but work was already done!"; - return; + addTask(makeShared(m_inst)); } - auto senderTask = QObject::sender(); - auto currentTask = m_tasks[m_currentTask].get(); - if (senderTask != currentTask) { - qDebug() << "MinecraftUpdate: Subtask" << sender() << "succeeded out of order."; - return; - } - next(); -} -void MinecraftUpdate::subtaskFailed(QString error) -{ - if (isFinished()) { - qCritical() << "MinecraftUpdate: Subtask" << sender() << "failed, but work was already done!"; - return; - } - auto senderTask = QObject::sender(); - auto currentTask = m_tasks[m_currentTask].get(); - if (senderTask != currentTask) { - qDebug() << "MinecraftUpdate: Subtask" << sender() << "failed out of order."; - m_failed_out_of_order = true; - m_fail_reason = error; - return; - } - emitFailed(error); -} - -bool MinecraftUpdate::abort() -{ - if (!m_abort) { - m_abort = true; - auto task = m_tasks[m_currentTask]; - if (task->canAbort()) { - return task->abort(); - } - } - return true; -} - -bool MinecraftUpdate::canAbort() const -{ - return true; + SequentialTask::executeTask(); } diff --git a/launcher/minecraft/MinecraftUpdate.h b/launcher/minecraft/MinecraftUpdate.h index 591c6afcb5..456a135185 100644 --- a/launcher/minecraft/MinecraftUpdate.h +++ b/launcher/minecraft/MinecraftUpdate.h @@ -15,43 +15,19 @@ #pragma once -#include -#include -#include +#include "tasks/SequentialTask.h" -#include -#include "minecraft/VersionFilterData.h" -#include "net/NetJob.h" -#include "tasks/Task.h" - -class MinecraftVersion; class MinecraftInstance; -// FIXME: This looks very similar to a SequentialTask. Maybe we can reduce code duplications? :^) - -class MinecraftUpdate : public Task { +// this needs to be a task because components->reload does stuff that may block +class MinecraftUpdate : public SequentialTask { Q_OBJECT public: explicit MinecraftUpdate(MinecraftInstance* inst, QObject* parent = 0); - virtual ~MinecraftUpdate() {}; + virtual ~MinecraftUpdate() = default; void executeTask() override; - bool canAbort() const override; - - private slots: - bool abort() override; - void subtaskSucceeded(); - void subtaskFailed(QString error); - - private: - void next(); private: MinecraftInstance* m_inst = nullptr; - QList m_tasks; - QString m_preFailure; - int m_currentTask = -1; - bool m_abort = false; - bool m_failed_out_of_order = false; - QString m_fail_reason; }; 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..40f49aaa4f 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 recommended 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..3f58a28a6d --- /dev/null +++ b/launcher/minecraft/launch/AutoInstallJava.cpp @@ -0,0 +1,242 @@ +// 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); + if (!m_current_task->isRunning()) { + m_current_task->start(); + } + emit progressReportingRequest(); +} + +void AutoInstallJava::setJavaPath(QString path) +{ + auto settings = m_instance->settings(); + settings->set("OverrideJavaLocation", true); + settings->set("JavaPath", path); + settings->set("AutomaticJava", true); + 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)); + + if (javaMajor->isLoaded()) { + downloadJava(javaMajor, wantedJavaName); + } else { + m_current_task = APPLICATION->metadataIndex()->loadVersion("net.minecraft.java", javaMajor->version(), Net::Mode::Online); + 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); + if (!m_current_task->isRunning()) { + m_current_task->start(); + } + } +} +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/LauncherPartLaunch.cpp b/launcher/minecraft/launch/LauncherPartLaunch.cpp index b35d559243..2b932ae47f 100644 --- a/launcher/minecraft/launch/LauncherPartLaunch.cpp +++ b/launcher/minecraft/launch/LauncherPartLaunch.cpp @@ -90,7 +90,7 @@ void LauncherPartLaunch::executeTask() } } - m_launchScript = minecraftInstance->createLaunchScript(m_session, m_serverToJoin); + m_launchScript = minecraftInstance->createLaunchScript(m_session, m_targetToJoin); QStringList args = minecraftInstance->javaArguments(); QString allArgs = args.join(", "); emit logLine("Java Arguments:\n[" + m_parent->censorPrivateInfo(allArgs) + "]\n\n", MessageLevel::Launcher); diff --git a/launcher/minecraft/launch/LauncherPartLaunch.h b/launcher/minecraft/launch/LauncherPartLaunch.h index fcd4daec65..ea125aa9ea 100644 --- a/launcher/minecraft/launch/LauncherPartLaunch.h +++ b/launcher/minecraft/launch/LauncherPartLaunch.h @@ -19,13 +19,13 @@ #include #include -#include "MinecraftServerTarget.h" +#include "MinecraftTarget.h" class LauncherPartLaunch : public LaunchStep { Q_OBJECT public: explicit LauncherPartLaunch(LaunchTask* parent); - virtual ~LauncherPartLaunch() {}; + virtual ~LauncherPartLaunch() = default; virtual void executeTask(); virtual bool abort(); @@ -34,7 +34,7 @@ class LauncherPartLaunch : public LaunchStep { void setWorkingDirectory(const QString& wd); void setAuthSession(AuthSessionPtr session) { m_session = session; } - void setServerToJoin(MinecraftServerTargetPtr serverToJoin) { m_serverToJoin = std::move(serverToJoin); } + void setTargetToJoin(MinecraftTarget::Ptr targetToJoin) { m_targetToJoin = std::move(targetToJoin); } private slots: void on_state(LoggedProcess::State state); @@ -44,7 +44,7 @@ class LauncherPartLaunch : public LaunchStep { QString m_command; AuthSessionPtr m_session; QString m_launchScript; - MinecraftServerTargetPtr m_serverToJoin; + MinecraftTarget::Ptr m_targetToJoin; bool mayProceed = false; }; diff --git a/launcher/minecraft/launch/MinecraftServerTarget.cpp b/launcher/minecraft/launch/MinecraftTarget.cpp similarity index 86% rename from launcher/minecraft/launch/MinecraftServerTarget.cpp rename to launcher/minecraft/launch/MinecraftTarget.cpp index e201efab14..ba9f875119 100644 --- a/launcher/minecraft/launch/MinecraftServerTarget.cpp +++ b/launcher/minecraft/launch/MinecraftTarget.cpp @@ -13,13 +13,18 @@ * limitations under the License. */ -#include "MinecraftServerTarget.h" +#include "MinecraftTarget.h" #include // FIXME: the way this is written, it can't ever do any sort of validation and can accept total junk -MinecraftServerTarget MinecraftServerTarget::parse(const QString& fullAddress) +MinecraftTarget MinecraftTarget::parse(const QString& fullAddress, bool useWorld) { + if (useWorld) { + MinecraftTarget target; + target.world = fullAddress; + return target; + } QStringList split = fullAddress.split(":"); // The logic below replicates the exact logic minecraft uses for parsing server addresses. @@ -56,5 +61,5 @@ MinecraftServerTarget MinecraftServerTarget::parse(const QString& fullAddress) } } - return MinecraftServerTarget{ realAddress, realPort }; + return MinecraftTarget{ realAddress, realPort }; } diff --git a/launcher/minecraft/launch/MinecraftServerTarget.h b/launcher/minecraft/launch/MinecraftTarget.h similarity index 80% rename from launcher/minecraft/launch/MinecraftServerTarget.h rename to launcher/minecraft/launch/MinecraftTarget.h index 2edd8a30d4..7f8b268d9b 100644 --- a/launcher/minecraft/launch/MinecraftServerTarget.h +++ b/launcher/minecraft/launch/MinecraftTarget.h @@ -19,11 +19,11 @@ #include -struct MinecraftServerTarget { +struct MinecraftTarget { QString address; quint16 port; - static MinecraftServerTarget parse(const QString& fullAddress); + QString world; + static MinecraftTarget parse(const QString& fullAddress, bool useWorld); + using Ptr = std::shared_ptr; }; - -using MinecraftServerTargetPtr = std::shared_ptr; diff --git a/launcher/minecraft/launch/PrintInstanceInfo.cpp b/launcher/minecraft/launch/PrintInstanceInfo.cpp index e3a45b030f..e44d09839d 100644 --- a/launcher/minecraft/launch/PrintInstanceInfo.cpp +++ b/launcher/minecraft/launch/PrintInstanceInfo.cpp @@ -129,6 +129,6 @@ void PrintInstanceInfo::executeTask() #endif logLines(log, MessageLevel::Launcher); - logLines(instance->verboseDescription(m_session, m_serverToJoin), MessageLevel::Launcher); + logLines(instance->verboseDescription(m_session, m_targetToJoin), MessageLevel::Launcher); emitSucceeded(); } diff --git a/launcher/minecraft/launch/PrintInstanceInfo.h b/launcher/minecraft/launch/PrintInstanceInfo.h index 93c5f0fd64..4138c0cd2c 100644 --- a/launcher/minecraft/launch/PrintInstanceInfo.h +++ b/launcher/minecraft/launch/PrintInstanceInfo.h @@ -16,22 +16,21 @@ #pragma once #include -#include #include "minecraft/auth/AuthSession.h" -#include "minecraft/launch/MinecraftServerTarget.h" +#include "minecraft/launch/MinecraftTarget.h" // FIXME: temporary wrapper for existing task. class PrintInstanceInfo : public LaunchStep { Q_OBJECT public: - explicit PrintInstanceInfo(LaunchTask* parent, AuthSessionPtr session, MinecraftServerTargetPtr serverToJoin) - : LaunchStep(parent), m_session(session), m_serverToJoin(serverToJoin) {}; - virtual ~PrintInstanceInfo() {}; + explicit PrintInstanceInfo(LaunchTask* parent, AuthSessionPtr session, MinecraftTarget::Ptr targetToJoin) + : LaunchStep(parent), m_session(session), m_targetToJoin(targetToJoin) {}; + virtual ~PrintInstanceInfo() = default; virtual void executeTask(); virtual bool canAbort() const { return false; } private: AuthSessionPtr m_session; - MinecraftServerTargetPtr m_serverToJoin; + MinecraftTarget::Ptr m_targetToJoin; }; 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/minecraft/update/AssetUpdateTask.cpp b/launcher/minecraft/update/AssetUpdateTask.cpp index 8af0149966..8add02d841 100644 --- a/launcher/minecraft/update/AssetUpdateTask.cpp +++ b/launcher/minecraft/update/AssetUpdateTask.cpp @@ -32,8 +32,7 @@ void AssetUpdateTask::executeTask() auto hexSha1 = assets->sha1.toLatin1(); qDebug() << "Asset index SHA1:" << hexSha1; auto dl = Net::ApiDownload::makeCached(indexUrl, entry); - auto rawSha1 = QByteArray::fromHex(assets->sha1.toLatin1()); - dl->addValidator(new Net::ChecksumValidator(QCryptographicHash::Sha1, rawSha1)); + dl->addValidator(new Net::ChecksumValidator(QCryptographicHash::Sha1, assets->sha1)); job->addNetAction(dl); downloadJob.reset(job); diff --git a/launcher/modplatform/atlauncher/ATLPackInstallTask.cpp b/launcher/modplatform/atlauncher/ATLPackInstallTask.cpp index a7721673e2..abe7d01773 100644 --- a/launcher/modplatform/atlauncher/ATLPackInstallTask.cpp +++ b/launcher/modplatform/atlauncher/ATLPackInstallTask.cpp @@ -343,9 +343,7 @@ QString PackInstallTask::getVersionForLoader(QString uid) return Q_NULLPTR; } - if (!vlist->isLoaded()) { - vlist->load(Net::Mode::Online); - } + vlist->waitToLoad(); if (m_version.loader.recommended || m_version.loader.latest) { for (int i = 0; i < vlist->versions().size(); i++) { @@ -638,8 +636,7 @@ void PackInstallTask::installConfigs() auto dl = Net::ApiDownload::makeCached(url, entry); if (!m_version.configs.sha1.isEmpty()) { - auto rawSha1 = QByteArray::fromHex(m_version.configs.sha1.toLatin1()); - dl->addValidator(new Net::ChecksumValidator(QCryptographicHash::Sha1, rawSha1)); + dl->addValidator(new Net::ChecksumValidator(QCryptographicHash::Sha1, m_version.configs.sha1)); } jobPtr->addNetAction(dl); archivePath = entry->getFullPath(); @@ -758,8 +755,7 @@ void PackInstallTask::downloadMods() auto dl = Net::ApiDownload::makeCached(url, entry); if (!mod.md5.isEmpty()) { - auto rawMd5 = QByteArray::fromHex(mod.md5.toLatin1()); - dl->addValidator(new Net::ChecksumValidator(QCryptographicHash::Md5, rawMd5)); + dl->addValidator(new Net::ChecksumValidator(QCryptographicHash::Md5, mod.md5)); } jobPtr->addNetAction(dl); } else if (mod.type == ModType::Decomp) { @@ -769,8 +765,7 @@ void PackInstallTask::downloadMods() auto dl = Net::ApiDownload::makeCached(url, entry); if (!mod.md5.isEmpty()) { - auto rawMd5 = QByteArray::fromHex(mod.md5.toLatin1()); - dl->addValidator(new Net::ChecksumValidator(QCryptographicHash::Md5, rawMd5)); + dl->addValidator(new Net::ChecksumValidator(QCryptographicHash::Md5, mod.md5)); } jobPtr->addNetAction(dl); } else { @@ -783,8 +778,7 @@ void PackInstallTask::downloadMods() auto dl = Net::ApiDownload::makeCached(url, entry); if (!mod.md5.isEmpty()) { - auto rawMd5 = QByteArray::fromHex(mod.md5.toLatin1()); - dl->addValidator(new Net::ChecksumValidator(QCryptographicHash::Md5, rawMd5)); + dl->addValidator(new Net::ChecksumValidator(QCryptographicHash::Md5, mod.md5)); } jobPtr->addNetAction(dl); @@ -1075,36 +1069,7 @@ void PackInstallTask::install() static Meta::Version::Ptr getComponentVersion(const QString& uid, const QString& version) { - auto vlist = APPLICATION->metadataIndex()->get(uid); - if (!vlist) - return {}; - - if (!vlist->isLoaded()) { - QEventLoop loadVersionLoop; - auto task = vlist->getLoadTask(); - QObject::connect(task.get(), &Task::finished, &loadVersionLoop, &QEventLoop::quit); - if (!task->isRunning()) - task->start(); - - loadVersionLoop.exec(); - } - - auto ver = vlist->getVersion(version); - if (!ver) - return {}; - - if (!ver->isLoaded()) { - QEventLoop loadVersionLoop; - ver->load(Net::Mode::Online); - auto task = ver->getCurrentTask(); - QObject::connect(task.get(), &Task::finished, &loadVersionLoop, &QEventLoop::quit); - if (!task->isRunning()) - task->start(); - - loadVersionLoop.exec(); - } - - return ver; + return APPLICATION->metadataIndex()->getLoadedVersion(uid, version); } } // namespace ATLauncher diff --git a/launcher/modplatform/flame/FlamePackExportTask.cpp b/launcher/modplatform/flame/FlamePackExportTask.cpp index 11bc3553be..d661f1f058 100644 --- a/launcher/modplatform/flame/FlamePackExportTask.cpp +++ b/launcher/modplatform/flame/FlamePackExportTask.cpp @@ -246,7 +246,7 @@ void FlamePackExportTask::makeApiRequest() pendingHashes.clear(); getProjectsInfo(); }); - connect(task.get(), &NetJob::failed, this, &FlamePackExportTask::getProjectsInfo); + connect(task.get(), &Task::failed, this, &FlamePackExportTask::getProjectsInfo); task->start(); } diff --git a/launcher/modplatform/modrinth/ModrinthInstanceCreationTask.cpp b/launcher/modplatform/modrinth/ModrinthInstanceCreationTask.cpp index c0806a638f..5178647903 100644 --- a/launcher/modplatform/modrinth/ModrinthInstanceCreationTask.cpp +++ b/launcher/modplatform/modrinth/ModrinthInstanceCreationTask.cpp @@ -346,23 +346,8 @@ bool ModrinthCreationTask::parseManifest(const QString& index_path, } QJsonObject hashes = Json::requireObject(modInfo, "hashes"); - QString hash; - QCryptographicHash::Algorithm hashAlgorithm; - hash = Json::ensureString(hashes, "sha1"); - hashAlgorithm = QCryptographicHash::Sha1; - if (hash.isEmpty()) { - hash = Json::ensureString(hashes, "sha512"); - hashAlgorithm = QCryptographicHash::Sha512; - if (hash.isEmpty()) { - hash = Json::ensureString(hashes, "sha256"); - hashAlgorithm = QCryptographicHash::Sha256; - if (hash.isEmpty()) { - throw JSONValidationError("No hash found for: " + file.path); - } - } - } - file.hash = QByteArray::fromHex(hash.toLatin1()); - file.hashAlgorithm = hashAlgorithm; + file.hash = QByteArray::fromHex(Json::requireString(hashes, "sha512").toLatin1()); + file.hashAlgorithm = QCryptographicHash::Sha512; // Do not use requireUrl, which uses StrictMode, instead use QUrl's default TolerantMode // (as Modrinth seems to incorrectly handle spaces) diff --git a/launcher/modplatform/modrinth/ModrinthPackExportTask.cpp b/launcher/modplatform/modrinth/ModrinthPackExportTask.cpp index ef0a3df16c..b7c2757e5e 100644 --- a/launcher/modplatform/modrinth/ModrinthPackExportTask.cpp +++ b/launcher/modplatform/modrinth/ModrinthPackExportTask.cpp @@ -18,6 +18,7 @@ #include "ModrinthPackExportTask.h" +#include #include #include #include @@ -28,6 +29,7 @@ #include "minecraft/mod/MetadataHandler.h" #include "minecraft/mod/ModFolderModel.h" #include "modplatform/helpers/HashUtils.h" +#include "tasks/Task.h" const QStringList ModrinthPackExportTask::PREFIXES({ "mods/", "coremods/", "resourcepacks/", "texturepacks/", "shaderpacks/" }); const QStringList ModrinthPackExportTask::FILE_EXTENSIONS({ "jar", "litemod", "zip" }); @@ -154,8 +156,8 @@ void ModrinthPackExportTask::makeApiRequest() setStatus(tr("Finding versions for hashes...")); auto response = std::make_shared(); task = api.currentVersions(pendingHashes.values(), "sha512", response); - connect(task.get(), &NetJob::succeeded, [this, response]() { parseApiResponse(response); }); - connect(task.get(), &NetJob::failed, this, &ModrinthPackExportTask::emitFailed); + connect(task.get(), &Task::succeeded, [this, response]() { parseApiResponse(response); }); + connect(task.get(), &Task::failed, this, &ModrinthPackExportTask::emitFailed); task->start(); } } diff --git a/launcher/modplatform/technic/SolderPackInstallTask.cpp b/launcher/modplatform/technic/SolderPackInstallTask.cpp index ed8b0a8a4a..ffda05ee93 100644 --- a/launcher/modplatform/technic/SolderPackInstallTask.cpp +++ b/launcher/modplatform/technic/SolderPackInstallTask.cpp @@ -114,8 +114,7 @@ void Technic::SolderPackInstallTask::fileListSucceeded() auto dl = Net::ApiDownload::makeFile(mod.url, path); if (!mod.md5.isEmpty()) { - auto rawMd5 = QByteArray::fromHex(mod.md5.toLatin1()); - dl->addValidator(new Net::ChecksumValidator(QCryptographicHash::Md5, rawMd5)); + dl->addValidator(new Net::ChecksumValidator(QCryptographicHash::Md5, mod.md5)); } m_filesNetJob->addNetAction(dl); diff --git a/launcher/net/ChecksumValidator.h b/launcher/net/ChecksumValidator.h index 6df5954537..7663d5d12b 100644 --- a/launcher/net/ChecksumValidator.h +++ b/launcher/net/ChecksumValidator.h @@ -43,6 +43,9 @@ namespace Net { class ChecksumValidator : public Validator { public: + ChecksumValidator(QCryptographicHash::Algorithm algorithm, QString expectedHex) + : Net::ChecksumValidator(algorithm, QByteArray::fromHex(expectedHex.toLatin1())) + {} ChecksumValidator(QCryptographicHash::Algorithm algorithm, QByteArray expected = QByteArray()) : m_checksum(algorithm), m_expected(expected) {}; virtual ~ChecksumValidator() = default; diff --git a/launcher/resources/multimc/multimc.qrc b/launcher/resources/multimc/multimc.qrc index eeba321866..25edd09e02 100644 --- a/launcher/resources/multimc/multimc.qrc +++ b/launcher/resources/multimc/multimc.qrc @@ -353,5 +353,11 @@ scalable/instances/neoforged.svg 128x128/instances/forge.png 128x128/instances/liteloader.png + + + scalable/adoptium.svg + scalable/azul.svg + scalable/mojang.svg + diff --git a/launcher/resources/multimc/scalable/adoptium.svg b/launcher/resources/multimc/scalable/adoptium.svg new file mode 100644 index 0000000000..d48f8b7d90 --- /dev/null +++ b/launcher/resources/multimc/scalable/adoptium.svg @@ -0,0 +1,180 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + image/svg+xml + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/launcher/resources/multimc/scalable/azul.svg b/launcher/resources/multimc/scalable/azul.svg new file mode 100644 index 0000000000..1c4356eb77 --- /dev/null +++ b/launcher/resources/multimc/scalable/azul.svg @@ -0,0 +1,17 @@ + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/launcher/resources/multimc/scalable/mojang.svg b/launcher/resources/multimc/scalable/mojang.svg new file mode 100644 index 0000000000..0c1f48d3db --- /dev/null +++ b/launcher/resources/multimc/scalable/mojang.svg @@ -0,0 +1,55 @@ + + Created with Fabric.js 3.6.3 diff --git a/launcher/translations/TranslationsModel.cpp b/launcher/translations/TranslationsModel.cpp index 016428d1c9..39719b1252 100644 --- a/launcher/translations/TranslationsModel.cpp +++ b/launcher/translations/TranslationsModel.cpp @@ -592,8 +592,7 @@ void TranslationsModel::downloadTranslation(QString key) entry->setStale(true); auto dl = Net::Download::makeCached(QUrl(BuildConfig.TRANSLATIONS_BASE_URL + lang->file_name), entry); - auto rawHash = QByteArray::fromHex(lang->file_sha1.toLatin1()); - dl->addValidator(new Net::ChecksumValidator(QCryptographicHash::Sha1, rawHash)); + dl->addValidator(new Net::ChecksumValidator(QCryptographicHash::Sha1, lang->file_sha1)); dl->setProgress(dl->getProgress(), lang->file_size); d->m_dl_job.reset(new NetJob("Translation for " + key, APPLICATION->network())); diff --git a/launcher/ui/ColorCache.cpp b/launcher/ui/ColorCache.cpp deleted file mode 100644 index f941a60935..0000000000 --- a/launcher/ui/ColorCache.cpp +++ /dev/null @@ -1,32 +0,0 @@ -#include "ColorCache.h" - -/** - * Blend the color with the front color, adapting to the back color - */ -QColor ColorCache::blend(QColor color) -{ - if (Rainbow::luma(m_front) > Rainbow::luma(m_back)) { - // for dark color schemes, produce a fitting color first - color = Rainbow::tint(m_front, color, 0.5); - } - // adapt contrast - return Rainbow::mix(m_front, color, m_bias); -} - -/** - * Blend the color with the back color - */ -QColor ColorCache::blendBackground(QColor color) -{ - // adapt contrast - return Rainbow::mix(m_back, color, m_bias); -} - -void ColorCache::recolorAll() -{ - auto iter = m_colors.begin(); - while (iter != m_colors.end()) { - iter->front = blend(iter->original); - iter->back = blendBackground(iter->original); - } -} diff --git a/launcher/ui/ColorCache.h b/launcher/ui/ColorCache.h deleted file mode 100644 index 1cf292c133..0000000000 --- a/launcher/ui/ColorCache.h +++ /dev/null @@ -1,106 +0,0 @@ -#pragma once -#include -#include -#include -#include - -class ColorCache { - public: - ColorCache(QColor front, QColor back, qreal bias) - { - m_front = front; - m_back = back; - m_bias = bias; - }; - - void addColor(int key, QColor color) { m_colors[key] = { color, blend(color), blendBackground(color) }; } - - void setForeground(QColor front) - { - if (m_front != front) { - m_front = front; - recolorAll(); - } - } - - void setBackground(QColor back) - { - if (m_back != back) { - m_back = back; - recolorAll(); - } - } - - QColor getFront(int key) - { - auto iter = m_colors.find(key); - if (iter == m_colors.end()) { - return QColor(); - } - return (*iter).front; - } - - QColor getBack(int key) - { - auto iter = m_colors.find(key); - if (iter == m_colors.end()) { - return QColor(); - } - return (*iter).back; - } - - /** - * Blend the color with the front color, adapting to the back color - */ - QColor blend(QColor color); - - /** - * Blend the color with the back color - */ - QColor blendBackground(QColor color); - - protected: - void recolorAll(); - - protected: - struct ColorEntry { - QColor original; - QColor front; - QColor back; - }; - - protected: - qreal m_bias; - QColor m_front; - QColor m_back; - QMap m_colors; -}; - -class LogColorCache : public ColorCache { - public: - LogColorCache(QColor front, QColor back) : ColorCache(front, back, 1.0) - { - addColor((int)MessageLevel::Launcher, QColor("purple")); - addColor((int)MessageLevel::Debug, QColor("green")); - addColor((int)MessageLevel::Warning, QColor("orange")); - addColor((int)MessageLevel::Error, QColor("red")); - addColor((int)MessageLevel::Fatal, QColor("red")); - addColor((int)MessageLevel::Message, front); - } - - QColor getFront(MessageLevel::Enum level) - { - if (!m_colors.contains((int)level)) { - return ColorCache::getFront((int)MessageLevel::Message); - } - return ColorCache::getFront((int)level); - } - - QColor getBack(MessageLevel::Enum level) - { - if (level == MessageLevel::Fatal) { - return QColor(Qt::black); - } - return QColor(Qt::transparent); - } -}; diff --git a/launcher/ui/dialogs/VersionSelectDialog.cpp b/launcher/ui/dialogs/VersionSelectDialog.cpp index c61d10578b..876d7470e7 100644 --- a/launcher/ui/dialogs/VersionSelectDialog.cpp +++ b/launcher/ui/dialogs/VersionSelectDialog.cpp @@ -121,7 +121,7 @@ void VersionSelectDialog::setResizeOn(int column) int VersionSelectDialog::exec() { QDialog::open(); - m_versionWidget->initialize(m_vlist); + m_versionWidget->initialize(m_vlist, true); m_versionWidget->selectSearch(); if (resizeOnColumn != -1) { m_versionWidget->setResizeOn(resizeOnColumn); diff --git a/launcher/ui/dialogs/VersionSelectDialog.h b/launcher/ui/dialogs/VersionSelectDialog.h index 34c5e66f8b..ed1de607b9 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 { @@ -37,7 +33,7 @@ class VersionSelectDialog : public QDialog { public: explicit VersionSelectDialog(BaseVersionList* vlist, QString title, QWidget* parent = 0, bool cancelable = true); - virtual ~VersionSelectDialog() {}; + virtual ~VersionSelectDialog() = default; int exec() override; diff --git a/launcher/ui/java/InstallJavaDialog.cpp b/launcher/ui/java/InstallJavaDialog.cpp new file mode 100644 index 0000000000..4fb9fc2d29 --- /dev/null +++ b/launcher/ui/java/InstallJavaDialog.cpp @@ -0,0 +1,338 @@ +// 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 + +#include "Application.h" +#include "BaseVersionList.h" +#include "FileSystem.h" +#include "Filter.h" +#include "java/download/ArchiveDownloadTask.h" +#include "java/download/ManifestDownloadTask.h" +#include "meta/Index.h" +#include "meta/VersionList.h" +#include "minecraft/MinecraftInstance.h" +#include "minecraft/PackProfile.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(); + } + + public slots: + void setRecommendedMajors(const QStringList& majors) + { + m_recommended_majors = majors; + recommendedFilterChanged(); + } + void setRecomend(bool recomend) + { + m_recommend = recomend; + recommendedFilterChanged(); + } + void recommendedFilterChanged() + { + if (m_recommend) { + majorVersionSelect->setFilter(BaseVersionList::ModelRoles::JavaMajorRole, new ExactListFilter(m_recommended_majors)); + } else { + majorVersionSelect->setFilter(BaseVersionList::ModelRoles::JavaMajorRole, new ExactListFilter()); + } + } + + 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; + + QStringList m_recommended_majors; + bool m_recommend; +}; + +static InstallJavaPage* pageCast(BasePage* page) +{ + auto result = dynamic_cast(page); + Q_ASSERT(result != nullptr); + return result; +} +namespace Java { +QStringList getRecommendedJavaVersionsFromVersionList(Meta::VersionList::Ptr list) +{ + QStringList recommendedJavas; + for (auto ver : list->versions()) { + auto major = ver->version(); + if (major.startsWith("java")) { + major = "Java " + major.mid(4); + } + recommendedJavas.append(major); + } + return recommendedJavas; +} + +InstallDialog::InstallDialog(const QString& uid, BaseInstance* instance, 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 refreshLayout = new QHBoxLayout(this); + + auto refreshButton = new QPushButton(tr("&Refresh"), this); + connect(refreshButton, &QPushButton::clicked, this, [this] { pageCast(container->selectedPage())->loadList(); }); + refreshLayout->addWidget(refreshButton); + + auto recommendedCheckBox = new QCheckBox("Recommended", this); + recommendedCheckBox->setCheckState(Qt::CheckState::Checked); + connect(recommendedCheckBox, &QCheckBox::stateChanged, this, [this](int state) { + for (BasePage* page : container->getPages()) { + pageCast(page)->setRecomend(state == Qt::Checked); + } + }); + + refreshLayout->addWidget(recommendedCheckBox); + buttonLayout->addLayout(refreshLayout); + + 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); + + QStringList recommendedJavas; + if (auto mcInst = dynamic_cast(instance); mcInst) { + auto mc = mcInst->getPackProfile()->getComponent("net.minecraft"); + if (mc) { + auto file = mc->getVersionFile(); // no need for load as it should already be loaded + if (file) { + for (auto major : file->compatibleJavaMajors) { + recommendedJavas.append(QString("Java %1").arg(major)); + } + } + } + } else { + const auto versions = APPLICATION->metadataIndex()->get("net.minecraft.java"); + if (versions) { + if (versions->isLoaded()) { + recommendedJavas = getRecommendedJavaVersionsFromVersionList(versions); + } else { + auto newTask = versions->getLoadTask(); + if (newTask) { + connect(newTask.get(), &Task::succeeded, this, [this, versions] { + auto recommendedJavas = getRecommendedJavaVersionsFromVersionList(versions); + for (BasePage* page : container->getPages()) { + pageCast(page)->setRecommendedMajors(recommendedJavas); + } + }); + if (!newTask->isRunning()) + newTask->start(); + } else { + recommendedJavas = getRecommendedJavaVersionsFromVersionList(versions); + } + } + } + } + for (BasePage* page : container->getPages()) { + if (page->id() == uid) + container->selectPage(page->id()); + + auto cast = pageCast(page); + cast->setRecomend(true); + connect(cast, &InstallJavaPage::selectionChanged, this, [this, cast] { validate(cast); }); + if (!recommendedJavas.isEmpty()) { + cast->setRecommendedMajors(recommendedJavas); + } + } + connect(container, &PageContainer::selectedPageChanged, this, [this](BasePage* previous, BasePage* selected) { validate(selected); }); + pageCast(container->selectedPage())->selectSearch(); + validate(container->selectedPage()); +} + +QList InstallDialog::getPages() +{ + return { + // Mojang + new InstallJavaPage("net.minecraft.java", "mojang", tr("Mojang")), + // Adoptium + new InstallJavaPage("net.adoptium.java", "adoptium", tr("Adoptium")), + // Azul + new InstallJavaPage("com.azul.java", "azul", tr("Azul Zulu")), + }; +} + +QString InstallDialog::dialogTitle() +{ + return tr("Install Java"); +} + +void InstallDialog::validate(BasePage* selected) +{ + buttons->button(QDialogButtonBox::Ok)->setEnabled(!!std::dynamic_pointer_cast(pageCast(selected)->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..7d0edbfdd3 --- /dev/null +++ b/launcher/ui/java/InstallJavaDialog.h @@ -0,0 +1,47 @@ +// 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 "BaseInstance.h" +#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(), BaseInstance* instance = nullptr, QWidget* parent = nullptr); + + QList getPages() override; + QString dialogTitle() override; + + void validate(BasePage* selected); + 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..f2c0cb3b9f --- /dev/null +++ b/launcher/ui/java/VersionList.cpp @@ -0,0 +1,126 @@ +// 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() +{ + auto task = m_version->loadTask(Net::Mode::Online); + 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/BasePageProvider.h b/launcher/ui/pages/BasePageProvider.h index 422891e6ba..ef3c1cd08a 100644 --- a/launcher/ui/pages/BasePageProvider.h +++ b/launcher/ui/pages/BasePageProvider.h @@ -16,7 +16,6 @@ #pragma once #include -#include #include "ui/pages/BasePage.h" class BasePageProvider { diff --git a/launcher/ui/pages/global/JavaPage.cpp b/launcher/ui/pages/global/JavaPage.cpp index ac50319ec1..6699b00c0a 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({}, nullptr, 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 the Java installation named \"%1\".\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..e6bbeb15a0 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,45 @@ + + + + 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 + + + + + + + + Automatically selects the Java version that is compatible with the current Minecraft instance, based on the major version required. + + + Autodetect Java version + + + @@ -277,13 +297,16 @@ - - + + + + false + - If enabled, the launcher will not prompt you to choose a Java version if one isn't found. + Automatically downloads and selects the Java version recommended by Mojang. - Skip Java &Wizard + Auto-download Mojang Java @@ -305,16 +328,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 6a240389a2..8bbed96436 100644 --- a/launcher/ui/pages/global/LauncherPage.cpp +++ b/launcher/ui/pages/global/LauncherPage.cpp @@ -4,6 +4,7 @@ * Copyright (c) 2022 Jamie Mansfield * Copyright (c) 2022 dada513 * Copyright (C) 2022 Tayou + * Copyright (C) 2024 TheKodeToad * * 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 @@ -50,6 +51,7 @@ #include "DesktopServices.h" #include "settings/SettingsObject.h" #include "ui/themes/ITheme.h" +#include "ui/themes/ThemeManager.h" #include "updater/ExternalUpdater.h" #include @@ -66,9 +68,6 @@ enum InstSortMode { LauncherPage::LauncherPage(QWidget* parent) : QWidget(parent), ui(new Ui::LauncherPage) { ui->setupUi(this); - auto origForeground = ui->fontPreview->palette().color(ui->fontPreview->foregroundRole()); - auto origBackground = ui->fontPreview->palette().color(ui->fontPreview->backgroundRole()); - m_colors.reset(new LogColorCache(origForeground, origBackground)); ui->sortingModeGroup->setId(ui->sortByNameBtn, Sort_Name); ui->sortingModeGroup->setId(ui->sortLastLaunchedBtn, Sort_LastLaunch); @@ -80,8 +79,9 @@ LauncherPage::LauncherPage(QWidget* parent) : QWidget(parent), ui(new Ui::Launch ui->updateSettingsBox->setHidden(!APPLICATION->updater()); - connect(ui->fontSizeBox, SIGNAL(valueChanged(int)), SLOT(refreshFontPreview())); - connect(ui->consoleFont, SIGNAL(currentFontChanged(QFont)), SLOT(refreshFontPreview())); + connect(ui->fontSizeBox, QOverload::of(&QSpinBox::valueChanged), this, &LauncherPage::refreshFontPreview); + connect(ui->consoleFont, &QFontComboBox::currentFontChanged, this, &LauncherPage::refreshFontPreview); + connect(ui->themeCustomizationWidget, &ThemeCustomizationWidget::currentWidgetThemeChanged, this, &LauncherPage::refreshFontPreview); connect(ui->themeCustomizationWidget, &ThemeCustomizationWidget::currentCatChanged, APPLICATION, &Application::currentCatChanged); } @@ -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()); @@ -223,6 +233,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(); @@ -289,6 +300,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(); @@ -311,37 +323,47 @@ void LauncherPage::loadSettings() void LauncherPage::refreshFontPreview() { + const LogColors& colors = APPLICATION->themeManager()->getLogColors(); + int fontSize = ui->fontSizeBox->value(); QString fontFamily = ui->consoleFont->currentFont().family(); ui->fontPreview->clear(); defaultFormat->setFont(QFont(fontFamily, fontSize)); - { - QTextCharFormat format(*defaultFormat); - format.setForeground(m_colors->getFront(MessageLevel::Error)); - // append a paragraph/line - auto workCursor = ui->fontPreview->textCursor(); - workCursor.movePosition(QTextCursor::End); - workCursor.insertText(tr("[Something/ERROR] A spooky error!"), format); - workCursor.insertBlock(); - } - { - QTextCharFormat format(*defaultFormat); - format.setForeground(m_colors->getFront(MessageLevel::Message)); - // append a paragraph/line - auto workCursor = ui->fontPreview->textCursor(); - workCursor.movePosition(QTextCursor::End); - workCursor.insertText(tr("[Test/INFO] A harmless message..."), format); - workCursor.insertBlock(); - } - { + + auto print = [this, colors](const QString& message, MessageLevel::Enum level) { QTextCharFormat format(*defaultFormat); - format.setForeground(m_colors->getFront(MessageLevel::Warning)); + + QColor bg = colors.background.value(level); + QColor fg = colors.foreground.value(level); + + if (bg.isValid()) + format.setBackground(bg); + + if (fg.isValid()) + format.setForeground(fg); + // append a paragraph/line auto workCursor = ui->fontPreview->textCursor(); workCursor.movePosition(QTextCursor::End); - workCursor.insertText(tr("[Something/WARN] A not so spooky warning."), format); + workCursor.insertText(message, format); workCursor.insertBlock(); - } + }; + + print(QString("%1 version: %2 (%3)\n") + .arg(BuildConfig.LAUNCHER_DISPLAYNAME, BuildConfig.printableVersionString(), BuildConfig.BUILD_PLATFORM), + MessageLevel::Launcher); + + QDate today = QDate::currentDate(); + + if (today.month() == 10 && today.day() == 31) + print(tr("[Test/ERROR] OOoooOOOoooo! A spooky error!"), MessageLevel::Error); + else + print(tr("[Test/ERROR] A spooky error!"), MessageLevel::Error); + + print(tr("[Test/INFO] A harmless message..."), MessageLevel::Info); + print(tr("[Test/WARN] A not so spooky warning."), MessageLevel::Warning); + print(tr("[Test/DEBUG] A secret debugging message..."), MessageLevel::Debug); + print(tr("[Test/FATAL] A terrifying fatal error!"), MessageLevel::Fatal); } void LauncherPage::retranslate() diff --git a/launcher/ui/pages/global/LauncherPage.h b/launcher/ui/pages/global/LauncherPage.h index f9aefb1716..02f371b04d 100644 --- a/launcher/ui/pages/global/LauncherPage.h +++ b/launcher/ui/pages/global/LauncherPage.h @@ -41,7 +41,6 @@ #include #include #include "java/JavaChecker.h" -#include "ui/ColorCache.h" #include "ui/pages/BasePage.h" class QTextCharFormat; @@ -74,6 +73,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(); @@ -93,7 +93,5 @@ class LauncherPage : public QWidget, public BasePage { // default format for the font preview... QTextCharFormat* defaultFormat; - std::unique_ptr m_colors; - std::shared_ptr m_languageModel; }; diff --git a/launcher/ui/pages/global/LauncherPage.ui b/launcher/ui/pages/global/LauncherPage.ui index 72039488bc..3cba468ff7 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..19bc9fdf98 100644 --- a/launcher/ui/pages/instance/InstanceSettingsPage.cpp +++ b/launcher/ui/pages/instance/InstanceSettingsPage.cpp @@ -36,6 +36,11 @@ */ #include "InstanceSettingsPage.h" +#include "minecraft/MinecraftInstance.h" +#include "minecraft/WorldList.h" +#include "settings/Setting.h" +#include "ui/dialogs/CustomMessageBox.h" +#include "ui/java/InstallJavaDialog.h" #include "ui_InstanceSettingsPage.h" #include @@ -62,6 +67,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); @@ -71,6 +78,22 @@ InstanceSettingsPage::InstanceSettingsPage(BaseInstance* inst, QWidget* parent) connect(ui->useNativeGLFWCheck, &QAbstractButton::toggled, this, &InstanceSettingsPage::onUseNativeGLFWChanged); connect(ui->useNativeOpenALCheck, &QAbstractButton::toggled, this, &InstanceSettingsPage::onUseNativeOpenALChanged); + auto mInst = dynamic_cast(inst); + m_world_quickplay_supported = mInst && mInst->traits().contains("feature:is_quick_play_singleplayer"); + if (m_world_quickplay_supported) { + auto worlds = mInst->worldList(); + worlds->update(); + for (const auto& world : worlds->allWorlds()) { + ui->worldsCb->addItem(world.folderName()); + } + } else { + ui->worldsCb->hide(); + ui->worldJoinButton->hide(); + ui->serverJoinAddressButton->setChecked(true); + ui->serverJoinAddress->setEnabled(true); + ui->serverJoinAddressButton->setStyleSheet("QRadioButton::indicator { width: 0px; height: 0px; }"); + } + loadSettings(); updateThresholds(); @@ -186,9 +209,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); @@ -256,9 +276,16 @@ void InstanceSettingsPage::applySettings() bool joinServerOnLaunch = ui->serverJoinGroupBox->isChecked(); m_settings->set("JoinServerOnLaunch", joinServerOnLaunch); if (joinServerOnLaunch) { - m_settings->set("JoinServerOnLaunchAddress", ui->serverJoinAddress->text()); + if (ui->serverJoinAddressButton->isChecked() || !m_world_quickplay_supported) { + m_settings->set("JoinServerOnLaunchAddress", ui->serverJoinAddress->text()); + m_settings->reset("JoinWorldOnLaunch"); + } else { + m_settings->set("JoinWorldOnLaunch", ui->worldsCb->currentText()); + m_settings->reset("JoinServerOnLaunchAddress"); + } } else { m_settings->reset("JoinServerOnLaunchAddress"); + m_settings->reset("JoinWorldOnLaunch"); } // Use an account for this instance @@ -317,10 +344,11 @@ 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(); + connect(m_settings->getSetting("OverrideJavaLocation").get(), &Setting::SettingChanged, ui->javaSettingsGroupBox, + [this] { ui->javaSettingsGroupBox->setChecked(m_settings->get("OverrideJavaLocation").toBool()); }); ui->javaSettingsGroupBox->setChecked(overrideLocation); ui->javaPathTextBox->setText(m_settings->get("JavaPath").toString()); ui->skipCompatibilityCheckbox->setChecked(m_settings->get("IgnoreJavaCompatibility").toBool()); @@ -379,7 +407,25 @@ void InstanceSettingsPage::loadSettings() ui->recordGameTime->setChecked(m_settings->get("RecordGameTime").toBool()); ui->serverJoinGroupBox->setChecked(m_settings->get("JoinServerOnLaunch").toBool()); - ui->serverJoinAddress->setText(m_settings->get("JoinServerOnLaunchAddress").toString()); + + if (auto server = m_settings->get("JoinServerOnLaunchAddress").toString(); !server.isEmpty()) { + ui->serverJoinAddress->setText(server); + ui->serverJoinAddressButton->setChecked(true); + ui->worldJoinButton->setChecked(false); + ui->serverJoinAddress->setEnabled(true); + ui->worldsCb->setEnabled(false); + } else if (auto world = m_settings->get("JoinWorldOnLaunch").toString(); !world.isEmpty() && m_world_quickplay_supported) { + ui->worldsCb->setCurrentText(world); + ui->serverJoinAddressButton->setChecked(false); + ui->worldJoinButton->setChecked(true); + ui->serverJoinAddress->setEnabled(false); + ui->worldsCb->setEnabled(true); + } else { + ui->serverJoinAddressButton->setChecked(true); + ui->worldJoinButton->setChecked(false); + ui->serverJoinAddress->setEnabled(true); + ui->worldsCb->setEnabled(false); + } ui->instanceAccountGroupBox->setChecked(m_settings->get("UseAccountForInstance").toBool()); updateAccountsMenu(); @@ -388,6 +434,12 @@ void InstanceSettingsPage::loadSettings() ui->onlineFixes->setChecked(m_settings->get("OnlineFixes").toBool()); } +void InstanceSettingsPage::on_javaDownloadBtn_clicked() +{ + auto jdialog = new Java::InstallDialog({}, m_instance, this); + jdialog->exec(); +} + void InstanceSettingsPage::on_javaDetectBtn_clicked() { if (JavaUtils::getJavaCheckPath().isEmpty()) { @@ -409,6 +461,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(); + } } } @@ -534,3 +595,13 @@ void InstanceSettingsPage::updateThresholds() ui->labelMaxMemIcon->setPixmap(pix); } } + +void InstanceSettingsPage::on_serverJoinAddressButton_toggled(bool checked) +{ + ui->serverJoinAddress->setEnabled(checked); +} + +void InstanceSettingsPage::on_worldJoinButton_toggled(bool checked) +{ + ui->worldsCb->setEnabled(checked); +} diff --git a/launcher/ui/pages/instance/InstanceSettingsPage.h b/launcher/ui/pages/instance/InstanceSettingsPage.h index 8b78dcb7f1..6499f9e8f4 100644 --- a/launcher/ui/pages/instance/InstanceSettingsPage.h +++ b/launcher/ui/pages/instance/InstanceSettingsPage.h @@ -69,7 +69,10 @@ 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 on_serverJoinAddressButton_toggled(bool checked); + void on_worldJoinButton_toggled(bool checked); void onUseNativeGLFWChanged(bool checked); void onUseNativeOpenALChanged(bool checked); @@ -90,4 +93,5 @@ class InstanceSettingsPage : public QWidget, public BasePage { BaseInstance* m_instance; SettingsObjectPtr m_settings; unique_qobject_ptr checker; + bool m_world_quickplay_supported; }; diff --git a/launcher/ui/pages/instance/InstanceSettingsPage.ui b/launcher/ui/pages/instance/InstanceSettingsPage.ui index 9490860ae4..4905eae87e 100644 --- a/launcher/ui/pages/instance/InstanceSettingsPage.ui +++ b/launcher/ui/pages/instance/InstanceSettingsPage.ui @@ -84,6 +84,13 @@ + + + + Download Java + + + @@ -660,7 +667,7 @@ - Set a server to join on launch + Set a target to join on launch true @@ -668,26 +675,26 @@ false - - - - - - - - 0 - 0 - - - - Server address: - - - - - - - + + + + + Server address: + + + + + + + + + + Singleplayer world + + + + + @@ -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/pages/instance/LogPage.cpp b/launcher/ui/pages/instance/LogPage.cpp index 8e1e53762b..0c22d1de66 100644 --- a/launcher/ui/pages/instance/LogPage.cpp +++ b/launcher/ui/pages/instance/LogPage.cpp @@ -3,7 +3,7 @@ * Prism Launcher - Minecraft Launcher * Copyright (c) 2022 Jamie Mansfield * Copyright (C) 2022 Sefa Eyeoglu - * Copyright (C) 2022 TheKodeToad + * Copyright (C) 2024 TheKodeToad * * 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 @@ -47,8 +47,8 @@ #include "launch/LaunchTask.h" #include "settings/Setting.h" -#include "ui/ColorCache.h" #include "ui/GuiUtil.h" +#include "ui/themes/ThemeManager.h" #include @@ -57,26 +57,36 @@ class LogFormatProxyModel : public QIdentityProxyModel { LogFormatProxyModel(QObject* parent = nullptr) : QIdentityProxyModel(parent) {} QVariant data(const QModelIndex& index, int role) const override { + const LogColors& colors = APPLICATION->themeManager()->getLogColors(); + switch (role) { case Qt::FontRole: return m_font; case Qt::ForegroundRole: { - MessageLevel::Enum level = (MessageLevel::Enum)QIdentityProxyModel::data(index, LogModel::LevelRole).toInt(); - return m_colors->getFront(level); + auto level = static_cast(QIdentityProxyModel::data(index, LogModel::LevelRole).toInt()); + QColor result = colors.foreground.value(level); + + if (result.isValid()) + return result; + + break; } case Qt::BackgroundRole: { - MessageLevel::Enum level = (MessageLevel::Enum)QIdentityProxyModel::data(index, LogModel::LevelRole).toInt(); - return m_colors->getBack(level); + auto level = static_cast(QIdentityProxyModel::data(index, LogModel::LevelRole).toInt()); + QColor result = colors.background.value(level); + + if (result.isValid()) + return result; + + break; } - default: - return QIdentityProxyModel::data(index, role); } + + return QIdentityProxyModel::data(index, role); } void setFont(QFont font) { m_font = font; } - void setColors(LogColorCache* colors) { m_colors.reset(colors); } - QModelIndex find(const QModelIndex& start, const QString& value, bool reverse) const { QModelIndex parentIndex = parent(start); @@ -125,7 +135,6 @@ class LogFormatProxyModel : public QIdentityProxyModel { private: QFont m_font; - std::unique_ptr m_colors; }; LogPage::LogPage(InstancePtr instance, QWidget* parent) : QWidget(parent), ui(new Ui::LogPage), m_instance(instance) @@ -134,12 +143,6 @@ LogPage::LogPage(InstancePtr instance, QWidget* parent) : QWidget(parent), ui(ne ui->tabWidget->tabBar()->hide(); m_proxy = new LogFormatProxyModel(this); - // set up text colors in the log proxy and adapt them to the current theme foreground and background - { - auto origForeground = ui->text->palette().color(ui->text->foregroundRole()); - auto origBackground = ui->text->palette().color(ui->text->backgroundRole()); - m_proxy->setColors(new LogColorCache(origForeground, origBackground)); - } // set up fonts in the log proxy { diff --git a/launcher/ui/pages/instance/ModFolderPage.cpp b/launcher/ui/pages/instance/ModFolderPage.cpp index d0be5301aa..f2feb8c7f4 100644 --- a/launcher/ui/pages/instance/ModFolderPage.cpp +++ b/launcher/ui/pages/instance/ModFolderPage.cpp @@ -68,6 +68,7 @@ #include "Version.h" #include "tasks/ConcurrentTask.h" +#include "tasks/Task.h" #include "ui/dialogs/ProgressDialog.h" ModFolderPage::ModFolderPage(BaseInstance* inst, std::shared_ptr mods, QWidget* parent) @@ -322,7 +323,27 @@ void ModFolderPage::updateMods(bool includeDeps) CoreModFolderPage::CoreModFolderPage(BaseInstance* inst, std::shared_ptr mods, QWidget* parent) : ModFolderPage(inst, mods, parent) -{} +{ + auto mcInst = dynamic_cast(m_instance); + if (mcInst) { + auto version = mcInst->getPackProfile(); + if (version && version->getComponent("net.minecraftforge") && version->getComponent("net.minecraft")) { + auto minecraftCmp = version->getComponent("net.minecraft"); + if (!minecraftCmp->m_loaded) { + version->reload(Net::Mode::Offline); + auto update = version->getCurrentTask(); + if (update) { + connect(update.get(), &Task::finished, this, [this] { + if (m_container) { + m_container->refreshContainer(); + } + }); + update->start(); + } + } + } + } +} bool CoreModFolderPage::shouldDisplay() const { @@ -332,15 +353,10 @@ bool CoreModFolderPage::shouldDisplay() const return true; auto version = inst->getPackProfile(); - - if (!version) - return true; - if (!version->getComponent("net.minecraftforge")) + if (!version || !version->getComponent("net.minecraftforge") || !version->getComponent("net.minecraft")) return false; - if (!version->getComponent("net.minecraft")) - return false; - if (version->getComponent("net.minecraft")->getReleaseDateTime() < g_VersionFilterData.legacyCutoffDate) - return true; + auto minecraftCmp = version->getComponent("net.minecraft"); + return minecraftCmp->m_loaded && minecraftCmp->getReleaseDateTime() < g_VersionFilterData.legacyCutoffDate; } return false; } diff --git a/launcher/ui/pages/instance/ServersPage.cpp b/launcher/ui/pages/instance/ServersPage.cpp index f842b4b93b..d8035e73ee 100644 --- a/launcher/ui/pages/instance/ServersPage.cpp +++ b/launcher/ui/pages/instance/ServersPage.cpp @@ -168,7 +168,7 @@ class ServersModel : public QAbstractListModel { m_saveTimer.setInterval(5000); connect(&m_saveTimer, &QTimer::timeout, this, &ServersModel::save_internal); } - virtual ~ServersModel() {}; + virtual ~ServersModel() = default; void observe() { @@ -731,7 +731,7 @@ void ServersPage::on_actionMove_Down_triggered() void ServersPage::on_actionJoin_triggered() { const auto& address = m_model->at(currentServer)->m_address; - APPLICATION->launch(m_inst, true, false, std::make_shared(MinecraftServerTarget::parse(address))); + APPLICATION->launch(m_inst, true, false, std::make_shared(MinecraftTarget::parse(address, false))); } #include "ServersPage.moc" diff --git a/launcher/ui/pages/instance/VersionPage.cpp b/launcher/ui/pages/instance/VersionPage.cpp index 807bc5d583..c55d32efb9 100644 --- a/launcher/ui/pages/instance/VersionPage.cpp +++ b/launcher/ui/pages/instance/VersionPage.cpp @@ -393,6 +393,11 @@ void VersionPage::on_actionChange_version_triggered() bool important = false; if (uid == "net.minecraft") { important = true; + if (APPLICATION->settings()->get("AutomaticJavaSwitch").toBool() && m_inst->settings()->get("AutomaticJava").toBool() && + m_inst->settings()->get("OverrideJavaLocation").toBool()) { + m_inst->settings()->set("OverrideJavaLocation", false); + m_inst->settings()->set("JavaPath", ""); + } } m_profile->setComponentVersion(uid, vselect.selectedVersion()->descriptor(), important); m_profile->resolve(Net::Mode::Online); diff --git a/launcher/ui/pages/instance/VersionPage.h b/launcher/ui/pages/instance/VersionPage.h index 951643743b..602d092062 100644 --- a/launcher/ui/pages/instance/VersionPage.h +++ b/launcher/ui/pages/instance/VersionPage.h @@ -41,6 +41,7 @@ #pragma once #include +#include #include "minecraft/MinecraftInstance.h" #include "minecraft/PackProfile.h" diff --git a/launcher/ui/pages/instance/WorldListPage.cpp b/launcher/ui/pages/instance/WorldListPage.cpp index 4f30e4bb79..4ed5f1f734 100644 --- a/launcher/ui/pages/instance/WorldListPage.cpp +++ b/launcher/ui/pages/instance/WorldListPage.cpp @@ -82,7 +82,7 @@ class WorldListProxyModel : public QSortFilterProxyModel { } }; -WorldListPage::WorldListPage(BaseInstance* inst, std::shared_ptr worlds, QWidget* parent) +WorldListPage::WorldListPage(InstancePtr inst, std::shared_ptr worlds, QWidget* parent) : QMainWindow(parent), m_inst(inst), ui(new Ui::WorldListPage), m_worlds(worlds) { ui->setupUi(this); @@ -113,6 +113,11 @@ void WorldListPage::openedImpl() { m_worlds->startWatching(); + auto mInst = std::dynamic_pointer_cast(m_inst); + if (!mInst || !mInst->traits().contains("feature:is_quick_play_singleplayer")) { + ui->toolBar->removeAction(ui->actionJoin); + } + auto const setting_name = QString("WideBarVisibility_%1").arg(id()); if (!APPLICATION->settings()->contains(setting_name)) m_wide_bar_setting = APPLICATION->settings()->registerSetting(setting_name); @@ -339,6 +344,14 @@ void WorldListPage::worldChanged([[maybe_unused]] const QModelIndex& current, [[ ui->actionDatapacks->setEnabled(enable); bool hasIcon = !index.data(WorldList::IconFileRole).isNull(); ui->actionReset_Icon->setEnabled(enable && hasIcon); + + auto mInst = std::dynamic_pointer_cast(m_inst); + auto supportsJoin = mInst && mInst->traits().contains("feature:is_quick_play_singleplayer"); + ui->actionJoin->setEnabled(enable && supportsJoin); + + if (!supportsJoin) { + ui->toolBar->removeAction(ui->actionJoin); + } } void WorldListPage::on_actionAdd_triggered() @@ -418,4 +431,15 @@ void WorldListPage::on_actionRefresh_triggered() m_worlds->update(); } +void WorldListPage::on_actionJoin_triggered() +{ + QModelIndex index = getSelectedWorld(); + if (!index.isValid()) { + return; + } + auto worldVariant = m_worlds->data(index, WorldList::ObjectRole); + auto world = (World*)worldVariant.value(); + APPLICATION->launch(m_inst, true, false, std::make_shared(MinecraftTarget::parse(world->folderName(), true))); +} + #include "WorldListPage.moc" diff --git a/launcher/ui/pages/instance/WorldListPage.h b/launcher/ui/pages/instance/WorldListPage.h index 4f83002f4e..84d9cd0750 100644 --- a/launcher/ui/pages/instance/WorldListPage.h +++ b/launcher/ui/pages/instance/WorldListPage.h @@ -53,7 +53,7 @@ class WorldListPage : public QMainWindow, public BasePage { Q_OBJECT public: - explicit WorldListPage(BaseInstance* inst, std::shared_ptr worlds, QWidget* parent = 0); + explicit WorldListPage(InstancePtr inst, std::shared_ptr worlds, QWidget* parent = 0); virtual ~WorldListPage(); virtual QString displayName() const override { return tr("Worlds"); } @@ -72,7 +72,7 @@ class WorldListPage : public QMainWindow, public BasePage { QMenu* createPopupMenu() override; protected: - BaseInstance* m_inst; + InstancePtr m_inst; private: QModelIndex getSelectedWorld(); @@ -101,6 +101,7 @@ class WorldListPage : public QMainWindow, public BasePage { void on_actionReset_Icon_triggered(); void worldChanged(const QModelIndex& current, const QModelIndex& previous); void mceditState(LoggedProcess::State state); + void on_actionJoin_triggered(); void ShowContextMenu(const QPoint& pos); }; diff --git a/launcher/ui/pages/instance/WorldListPage.ui b/launcher/ui/pages/instance/WorldListPage.ui index d74dd07968..04344b4533 100644 --- a/launcher/ui/pages/instance/WorldListPage.ui +++ b/launcher/ui/pages/instance/WorldListPage.ui @@ -81,6 +81,7 @@ + @@ -97,6 +98,11 @@ Add + + + Join + + Rename diff --git a/launcher/ui/pages/modplatform/ResourcePage.cpp b/launcher/ui/pages/modplatform/ResourcePage.cpp index 4d01fb1f08..bed1184658 100644 --- a/launcher/ui/pages/modplatform/ResourcePage.cpp +++ b/launcher/ui/pages/modplatform/ResourcePage.cpp @@ -321,14 +321,9 @@ void ResourcePage::onSelectionChanged(QModelIndex curr, [[maybe_unused]] QModelI updateUi(); } -void ResourcePage::onVersionSelectionChanged(QString versionData) +void ResourcePage::onVersionSelectionChanged(int index) { - if (versionData.isNull() || versionData.isEmpty()) { - m_selected_version_index = -1; - return; - } - - m_selected_version_index = m_ui->versionSelectionBox->currentData().toInt(); + m_selected_version_index = index; updateSelectionButton(); } diff --git a/launcher/ui/pages/modplatform/ResourcePage.h b/launcher/ui/pages/modplatform/ResourcePage.h index d5214dd341..b625240eb7 100644 --- a/launcher/ui/pages/modplatform/ResourcePage.h +++ b/launcher/ui/pages/modplatform/ResourcePage.h @@ -89,7 +89,7 @@ class ResourcePage : public QWidget, public BasePage { virtual void triggerSearch() = 0; void onSelectionChanged(QModelIndex first, QModelIndex second); - void onVersionSelectionChanged(QString data); + void onVersionSelectionChanged(int index); void onResourceSelected(); // NOTE: Can't use [[nodiscard]] here because of https://bugreports.qt.io/browse/QTBUG-58628 on Qt 5.12 diff --git a/launcher/ui/pages/modplatform/TexturePackModel.cpp b/launcher/ui/pages/modplatform/TexturePackModel.cpp index fa63695140..cb4cafd41a 100644 --- a/launcher/ui/pages/modplatform/TexturePackModel.cpp +++ b/launcher/ui/pages/modplatform/TexturePackModel.cpp @@ -17,9 +17,9 @@ TexturePackResourceModel::TexturePackResourceModel(BaseInstance const& inst, Res { if (!m_version_list->isLoaded()) { qDebug() << "Loading version list..."; - auto task = m_version_list->getLoadTask(); - if (!task->isRunning()) - task->start(); + m_task = m_version_list->getLoadTask(); + if (!m_task->isRunning()) + m_task->start(); } } @@ -35,7 +35,8 @@ void waitOnVersionListLoad(Meta::VersionList::Ptr version_list) auto task = version_list->getLoadTask(); QObject::connect(task.get(), &Task::finished, &load_version_list_loop, &QEventLoop::quit); - + if (!task->isRunning()) + task->start(); load_version_list_loop.exec(); if (time_limit_for_list_load.isActive()) time_limit_for_list_load.stop(); diff --git a/launcher/ui/pages/modplatform/TexturePackModel.h b/launcher/ui/pages/modplatform/TexturePackModel.h index bb2db5cfc6..607a03be37 100644 --- a/launcher/ui/pages/modplatform/TexturePackModel.h +++ b/launcher/ui/pages/modplatform/TexturePackModel.h @@ -22,6 +22,7 @@ class TexturePackResourceModel : public ResourcePackResourceModel { protected: Meta::VersionList::Ptr m_version_list; + Task::Ptr m_task; }; } // namespace ResourceDownload diff --git a/launcher/ui/pages/modplatform/flame/FlamePage.cpp b/launcher/ui/pages/modplatform/flame/FlamePage.cpp index 2c10dd085f..decb5de3b8 100644 --- a/launcher/ui/pages/modplatform/flame/FlamePage.cpp +++ b/launcher/ui/pages/modplatform/flame/FlamePage.cpp @@ -84,7 +84,7 @@ FlamePage::FlamePage(NewInstanceDialog* dialog, QWidget* parent) connect(ui->sortByBox, SIGNAL(currentIndexChanged(int)), this, SLOT(triggerSearch())); connect(ui->packView->selectionModel(), &QItemSelectionModel::currentChanged, this, &FlamePage::onSelectionChanged); - connect(ui->versionSelectionBox, &QComboBox::currentTextChanged, this, &FlamePage::onVersionSelectionChanged); + connect(ui->versionSelectionBox, QOverload::of(&QComboBox::currentIndexChanged), this, &FlamePage::onVersionSelectionChanged); ui->packView->setItemDelegate(new ProjectItemDelegate(this)); ui->packDescription->setMetaEntry("FlamePacks"); @@ -240,17 +240,17 @@ void FlamePage::suggestCurrent() [this, editedLogoName](QString logo) { dialog->setSuggestedIconFromFile(logo, editedLogoName); }); } -void FlamePage::onVersionSelectionChanged(QString version) +void FlamePage::onVersionSelectionChanged(int index) { bool is_blocked = false; ui->versionSelectionBox->currentData().toInt(&is_blocked); - if (version.isNull() || version.isEmpty() || is_blocked) { + if (index == -1 || is_blocked) { m_selected_version_index = -1; return; } - m_selected_version_index = ui->versionSelectionBox->currentIndex(); + m_selected_version_index = index; Q_ASSERT(current.versions.at(m_selected_version_index).downloadUrl == ui->versionSelectionBox->currentData().toString()); diff --git a/launcher/ui/pages/modplatform/flame/FlamePage.h b/launcher/ui/pages/modplatform/flame/FlamePage.h index d35858fbc8..7590e1a954 100644 --- a/launcher/ui/pages/modplatform/flame/FlamePage.h +++ b/launcher/ui/pages/modplatform/flame/FlamePage.h @@ -78,7 +78,7 @@ class FlamePage : public QWidget, public BasePage { private slots: void triggerSearch(); void onSelectionChanged(QModelIndex first, QModelIndex second); - void onVersionSelectionChanged(QString data); + void onVersionSelectionChanged(int index); private: Ui::FlamePage* ui = nullptr; diff --git a/launcher/ui/pages/modplatform/flame/FlameResourcePages.cpp b/launcher/ui/pages/modplatform/flame/FlameResourcePages.cpp index d82c76c3a8..62c22902e5 100644 --- a/launcher/ui/pages/modplatform/flame/FlameResourcePages.cpp +++ b/launcher/ui/pages/modplatform/flame/FlameResourcePages.cpp @@ -60,7 +60,7 @@ FlameModPage::FlameModPage(ModDownloadDialog* dialog, BaseInstance& instance) : // so it's best not to connect them in the parent's contructor... connect(m_ui->sortByBox, SIGNAL(currentIndexChanged(int)), this, SLOT(triggerSearch())); connect(m_ui->packView->selectionModel(), &QItemSelectionModel::currentChanged, this, &FlameModPage::onSelectionChanged); - connect(m_ui->versionSelectionBox, &QComboBox::currentTextChanged, this, &FlameModPage::onVersionSelectionChanged); + connect(m_ui->versionSelectionBox, QOverload::of(&QComboBox::currentIndexChanged), this, &FlameModPage::onVersionSelectionChanged); connect(m_ui->resourceSelectionButton, &QPushButton::clicked, this, &FlameModPage::onResourceSelected); m_ui->packDescription->setMetaEntry(metaEntryBase()); @@ -94,7 +94,8 @@ FlameResourcePackPage::FlameResourcePackPage(ResourcePackDownloadDialog* dialog, // so it's best not to connect them in the parent's contructor... connect(m_ui->sortByBox, SIGNAL(currentIndexChanged(int)), this, SLOT(triggerSearch())); connect(m_ui->packView->selectionModel(), &QItemSelectionModel::currentChanged, this, &FlameResourcePackPage::onSelectionChanged); - connect(m_ui->versionSelectionBox, &QComboBox::currentTextChanged, this, &FlameResourcePackPage::onVersionSelectionChanged); + connect(m_ui->versionSelectionBox, QOverload::of(&QComboBox::currentIndexChanged), this, + &FlameResourcePackPage::onVersionSelectionChanged); connect(m_ui->resourceSelectionButton, &QPushButton::clicked, this, &FlameResourcePackPage::onResourceSelected); m_ui->packDescription->setMetaEntry(metaEntryBase()); @@ -128,7 +129,8 @@ FlameTexturePackPage::FlameTexturePackPage(TexturePackDownloadDialog* dialog, Ba // so it's best not to connect them in the parent's contructor... connect(m_ui->sortByBox, SIGNAL(currentIndexChanged(int)), this, SLOT(triggerSearch())); connect(m_ui->packView->selectionModel(), &QItemSelectionModel::currentChanged, this, &FlameTexturePackPage::onSelectionChanged); - connect(m_ui->versionSelectionBox, &QComboBox::currentTextChanged, this, &FlameTexturePackPage::onVersionSelectionChanged); + connect(m_ui->versionSelectionBox, QOverload::of(&QComboBox::currentIndexChanged), this, + &FlameTexturePackPage::onVersionSelectionChanged); connect(m_ui->resourceSelectionButton, &QPushButton::clicked, this, &FlameTexturePackPage::onResourceSelected); m_ui->packDescription->setMetaEntry(metaEntryBase()); @@ -162,7 +164,8 @@ FlameShaderPackPage::FlameShaderPackPage(ShaderPackDownloadDialog* dialog, BaseI // so it's best not to connect them in the parent's constructor... connect(m_ui->sortByBox, SIGNAL(currentIndexChanged(int)), this, SLOT(triggerSearch())); connect(m_ui->packView->selectionModel(), &QItemSelectionModel::currentChanged, this, &FlameShaderPackPage::onSelectionChanged); - connect(m_ui->versionSelectionBox, &QComboBox::currentTextChanged, this, &FlameShaderPackPage::onVersionSelectionChanged); + connect(m_ui->versionSelectionBox, QOverload::of(&QComboBox::currentIndexChanged), this, + &FlameShaderPackPage::onVersionSelectionChanged); connect(m_ui->resourceSelectionButton, &QPushButton::clicked, this, &FlameShaderPackPage::onResourceSelected); m_ui->packDescription->setMetaEntry(metaEntryBase()); diff --git a/launcher/ui/pages/modplatform/modrinth/ModrinthPage.cpp b/launcher/ui/pages/modplatform/modrinth/ModrinthPage.cpp index cb811e428a..03461d85a2 100644 --- a/launcher/ui/pages/modplatform/modrinth/ModrinthPage.cpp +++ b/launcher/ui/pages/modplatform/modrinth/ModrinthPage.cpp @@ -85,7 +85,7 @@ ModrinthPage::ModrinthPage(NewInstanceDialog* dialog, QWidget* parent) connect(ui->sortByBox, SIGNAL(currentIndexChanged(int)), this, SLOT(triggerSearch())); connect(ui->packView->selectionModel(), &QItemSelectionModel::currentChanged, this, &ModrinthPage::onSelectionChanged); - connect(ui->versionSelectionBox, &QComboBox::currentTextChanged, this, &ModrinthPage::onVersionSelectionChanged); + connect(ui->versionSelectionBox, QOverload::of(&QComboBox::currentIndexChanged), this, &ModrinthPage::onVersionSelectionChanged); ui->packView->setItemDelegate(new ProjectItemDelegate(this)); ui->packDescription->setMetaEntry(metaEntryBase()); @@ -342,9 +342,9 @@ void ModrinthPage::triggerSearch() m_fetch_progress.watch(m_model->activeSearchJob().get()); } -void ModrinthPage::onVersionSelectionChanged(QString version) +void ModrinthPage::onVersionSelectionChanged(int index) { - if (version.isNull() || version.isEmpty()) { + if (index == -1) { selectedVersion = ""; return; } diff --git a/launcher/ui/pages/modplatform/modrinth/ModrinthPage.h b/launcher/ui/pages/modplatform/modrinth/ModrinthPage.h index 4240dcafb2..dadaeb0a02 100644 --- a/launcher/ui/pages/modplatform/modrinth/ModrinthPage.h +++ b/launcher/ui/pages/modplatform/modrinth/ModrinthPage.h @@ -80,7 +80,7 @@ class ModrinthPage : public QWidget, public BasePage { private slots: void onSelectionChanged(QModelIndex first, QModelIndex second); - void onVersionSelectionChanged(QString data); + void onVersionSelectionChanged(int index); void triggerSearch(); private: diff --git a/launcher/ui/pages/modplatform/modrinth/ModrinthResourcePages.cpp b/launcher/ui/pages/modplatform/modrinth/ModrinthResourcePages.cpp index 26fe46a545..85dcde471d 100644 --- a/launcher/ui/pages/modplatform/modrinth/ModrinthResourcePages.cpp +++ b/launcher/ui/pages/modplatform/modrinth/ModrinthResourcePages.cpp @@ -58,7 +58,8 @@ ModrinthModPage::ModrinthModPage(ModDownloadDialog* dialog, BaseInstance& instan // so it's best not to connect them in the parent's constructor... connect(m_ui->sortByBox, SIGNAL(currentIndexChanged(int)), this, SLOT(triggerSearch())); connect(m_ui->packView->selectionModel(), &QItemSelectionModel::currentChanged, this, &ModrinthModPage::onSelectionChanged); - connect(m_ui->versionSelectionBox, &QComboBox::currentTextChanged, this, &ModrinthModPage::onVersionSelectionChanged); + connect(m_ui->versionSelectionBox, QOverload::of(&QComboBox::currentIndexChanged), this, + &ModrinthModPage::onVersionSelectionChanged); connect(m_ui->resourceSelectionButton, &QPushButton::clicked, this, &ModrinthModPage::onResourceSelected); m_ui->packDescription->setMetaEntry(metaEntryBase()); @@ -76,7 +77,8 @@ ModrinthResourcePackPage::ModrinthResourcePackPage(ResourcePackDownloadDialog* d // so it's best not to connect them in the parent's constructor... connect(m_ui->sortByBox, SIGNAL(currentIndexChanged(int)), this, SLOT(triggerSearch())); connect(m_ui->packView->selectionModel(), &QItemSelectionModel::currentChanged, this, &ModrinthResourcePackPage::onSelectionChanged); - connect(m_ui->versionSelectionBox, &QComboBox::currentTextChanged, this, &ModrinthResourcePackPage::onVersionSelectionChanged); + connect(m_ui->versionSelectionBox, QOverload::of(&QComboBox::currentIndexChanged), this, + &ModrinthResourcePackPage::onVersionSelectionChanged); connect(m_ui->resourceSelectionButton, &QPushButton::clicked, this, &ModrinthResourcePackPage::onResourceSelected); m_ui->packDescription->setMetaEntry(metaEntryBase()); @@ -94,7 +96,8 @@ ModrinthTexturePackPage::ModrinthTexturePackPage(TexturePackDownloadDialog* dial // so it's best not to connect them in the parent's constructor... connect(m_ui->sortByBox, SIGNAL(currentIndexChanged(int)), this, SLOT(triggerSearch())); connect(m_ui->packView->selectionModel(), &QItemSelectionModel::currentChanged, this, &ModrinthTexturePackPage::onSelectionChanged); - connect(m_ui->versionSelectionBox, &QComboBox::currentTextChanged, this, &ModrinthTexturePackPage::onVersionSelectionChanged); + connect(m_ui->versionSelectionBox, QOverload::of(&QComboBox::currentIndexChanged), this, + &ModrinthTexturePackPage::onVersionSelectionChanged); connect(m_ui->resourceSelectionButton, &QPushButton::clicked, this, &ModrinthTexturePackPage::onResourceSelected); m_ui->packDescription->setMetaEntry(metaEntryBase()); @@ -112,7 +115,8 @@ ModrinthShaderPackPage::ModrinthShaderPackPage(ShaderPackDownloadDialog* dialog, // so it's best not to connect them in the parent's constructor... connect(m_ui->sortByBox, SIGNAL(currentIndexChanged(int)), this, SLOT(triggerSearch())); connect(m_ui->packView->selectionModel(), &QItemSelectionModel::currentChanged, this, &ModrinthShaderPackPage::onSelectionChanged); - connect(m_ui->versionSelectionBox, &QComboBox::currentTextChanged, this, &ModrinthShaderPackPage::onVersionSelectionChanged); + connect(m_ui->versionSelectionBox, QOverload::of(&QComboBox::currentIndexChanged), this, + &ModrinthShaderPackPage::onVersionSelectionChanged); connect(m_ui->resourceSelectionButton, &QPushButton::clicked, this, &ModrinthShaderPackPage::onResourceSelected); m_ui->packDescription->setMetaEntry(metaEntryBase()); 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/themes/BrightTheme.cpp b/launcher/ui/themes/BrightTheme.cpp index 39a5bfd148..81bdd773eb 100644 --- a/launcher/ui/themes/BrightTheme.cpp +++ b/launcher/ui/themes/BrightTheme.cpp @@ -46,11 +46,6 @@ QString BrightTheme::name() return QObject::tr("Bright"); } -bool BrightTheme::hasColorScheme() -{ - return true; -} - QPalette BrightTheme::colorScheme() { QPalette brightPalette; diff --git a/launcher/ui/themes/BrightTheme.h b/launcher/ui/themes/BrightTheme.h index 750e7bfc51..070eef124a 100644 --- a/launcher/ui/themes/BrightTheme.h +++ b/launcher/ui/themes/BrightTheme.h @@ -45,7 +45,6 @@ class BrightTheme : public FusionTheme { QString tooltip() override; bool hasStyleSheet() override; QString appStyleSheet() override; - bool hasColorScheme() override; QPalette colorScheme() override; double fadeAmount() override; QColor fadeColor() override; diff --git a/launcher/ui/themes/CustomTheme.cpp b/launcher/ui/themes/CustomTheme.cpp index 22b366b624..081ba19008 100644 --- a/launcher/ui/themes/CustomTheme.cpp +++ b/launcher/ui/themes/CustomTheme.cpp @@ -2,6 +2,7 @@ /* * Prism Launcher - Minecraft Launcher * Copyright (C) 2024 Tayou + * Copyright (C) 2024 TheKodeToad * * 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 @@ -39,121 +40,6 @@ const char* themeFile = "theme.json"; -static bool readThemeJson(const QString& path, - QPalette& palette, - double& fadeAmount, - QColor& fadeColor, - QString& name, - QString& widgets, - QString& qssFilePath, - bool& dataIncomplete) -{ - QFileInfo pathInfo(path); - if (pathInfo.exists() && pathInfo.isFile()) { - try { - auto doc = Json::requireDocument(path, "Theme JSON file"); - const QJsonObject root = doc.object(); - dataIncomplete = !root.contains("qssFilePath"); - name = Json::requireString(root, "name", "Theme name"); - widgets = Json::requireString(root, "widgets", "Qt widget theme"); - qssFilePath = Json::ensureString(root, "qssFilePath", "themeStyle.css"); - auto colorsRoot = Json::requireObject(root, "colors", "colors object"); - auto readColor = [&](QString colorName) -> QColor { - auto colorValue = Json::ensureString(colorsRoot, colorName, QString()); - if (!colorValue.isEmpty()) { - QColor color(colorValue); - if (!color.isValid()) { - themeWarningLog() << "Color value" << colorValue << "for" << colorName << "was not recognized."; - return QColor(); - } - return color; - } - return QColor(); - }; - auto readAndSetColor = [&](QPalette::ColorRole role, QString colorName) { - auto color = readColor(colorName); - if (color.isValid()) { - palette.setColor(role, color); - } else { - themeDebugLog() << "Color value for" << colorName << "was not present."; - } - }; - - // palette - readAndSetColor(QPalette::Window, "Window"); - readAndSetColor(QPalette::WindowText, "WindowText"); - readAndSetColor(QPalette::Base, "Base"); - readAndSetColor(QPalette::AlternateBase, "AlternateBase"); - readAndSetColor(QPalette::ToolTipBase, "ToolTipBase"); - readAndSetColor(QPalette::ToolTipText, "ToolTipText"); - readAndSetColor(QPalette::Text, "Text"); - readAndSetColor(QPalette::Button, "Button"); - readAndSetColor(QPalette::ButtonText, "ButtonText"); - readAndSetColor(QPalette::BrightText, "BrightText"); - readAndSetColor(QPalette::Link, "Link"); - readAndSetColor(QPalette::Highlight, "Highlight"); - readAndSetColor(QPalette::HighlightedText, "HighlightedText"); - - // fade - fadeColor = readColor("fadeColor"); - fadeAmount = Json::ensureDouble(colorsRoot, "fadeAmount", 0.5, "fade amount"); - - } catch (const Exception& e) { - themeWarningLog() << "Couldn't load theme json: " << e.cause(); - return false; - } - } else { - themeDebugLog() << "No theme json present."; - return false; - } - return true; -} - -static bool writeThemeJson(const QString& path, - const QPalette& palette, - double fadeAmount, - QColor fadeColor, - QString name, - QString widgets, - QString qssFilePath) -{ - QJsonObject rootObj; - rootObj.insert("name", name); - rootObj.insert("widgets", widgets); - rootObj.insert("qssFilePath", qssFilePath); - - QJsonObject colorsObj; - auto insertColor = [&](QPalette::ColorRole role, QString colorName) { colorsObj.insert(colorName, palette.color(role).name()); }; - - // palette - insertColor(QPalette::Window, "Window"); - insertColor(QPalette::WindowText, "WindowText"); - insertColor(QPalette::Base, "Base"); - insertColor(QPalette::AlternateBase, "AlternateBase"); - insertColor(QPalette::ToolTipBase, "ToolTipBase"); - insertColor(QPalette::ToolTipText, "ToolTipText"); - insertColor(QPalette::Text, "Text"); - insertColor(QPalette::Button, "Button"); - insertColor(QPalette::ButtonText, "ButtonText"); - insertColor(QPalette::BrightText, "BrightText"); - insertColor(QPalette::Link, "Link"); - insertColor(QPalette::Highlight, "Highlight"); - insertColor(QPalette::HighlightedText, "HighlightedText"); - - // fade - colorsObj.insert("fadeColor", fadeColor.name()); - colorsObj.insert("fadeAmount", fadeAmount); - - rootObj.insert("colors", colorsObj); - try { - Json::write(rootObj, path); - return true; - } catch ([[maybe_unused]] const Exception& e) { - themeWarningLog() << "Failed to write theme json to" << path; - return false; - } -} - /// @param baseTheme Base Theme /// @param fileInfo FileInfo object for file to load /// @param isManifest whether to load a theme manifest or a qss file @@ -176,23 +62,22 @@ CustomTheme::CustomTheme(ITheme* baseTheme, QFileInfo& fileInfo, bool isManifest auto themeFilePath = FS::PathCombine(path, themeFile); - bool jsonDataIncomplete = false; - m_palette = baseTheme->colorScheme(); - if (readThemeJson(themeFilePath, m_palette, m_fadeAmount, m_fadeColor, m_name, m_widgets, m_qssFilePath, jsonDataIncomplete)) { + + bool hasCustomLogColors = false; + + if (read(themeFilePath, hasCustomLogColors)) { // If theme data was found, fade "Disabled" color of each role according to FadeAmount m_palette = fadeInactive(m_palette, m_fadeAmount, m_fadeColor); + + if (!hasCustomLogColors) + m_logColors = defaultLogColors(m_palette); } else { themeDebugLog() << "Did not read theme json file correctly, not changing theme, keeping previous."; + m_logColors = defaultLogColors(m_palette); return; } - // FIXME: This is kinda jank, it only actually checks if the qss file path is not present. It should actually check for any relevant - // missing data (e.g. name, colors) - if (jsonDataIncomplete) { - writeThemeJson(fileInfo.absoluteFilePath(), m_palette, m_fadeAmount, m_fadeColor, m_name, m_widgets, m_qssFilePath); - } - auto qssFilePath = FS::PathCombine(path, m_qssFilePath); QFileInfo info(qssFilePath); if (info.isFile()) { @@ -251,11 +136,6 @@ QString CustomTheme::name() return m_name; } -bool CustomTheme::hasColorScheme() -{ - return true; -} - QPalette CustomTheme::colorScheme() { return m_palette; @@ -289,3 +169,99 @@ QString CustomTheme::tooltip() { return m_tooltip; } + +bool CustomTheme::read(const QString& path, bool& hasCustomLogColors) +{ + QFileInfo pathInfo(path); + if (pathInfo.exists() && pathInfo.isFile()) { + try { + auto doc = Json::requireDocument(path, "Theme JSON file"); + const QJsonObject root = doc.object(); + m_name = Json::requireString(root, "name", "Theme name"); + m_widgets = Json::requireString(root, "widgets", "Qt widget theme"); + m_qssFilePath = Json::ensureString(root, "qssFilePath", "themeStyle.css"); + + auto readColor = [&](const QJsonObject& colors, const QString& colorName) -> QColor { + auto colorValue = Json::ensureString(colors, colorName, QString()); + if (!colorValue.isEmpty()) { + QColor color(colorValue); + if (!color.isValid()) { + themeWarningLog() << "Color value" << colorValue << "for" << colorName << "was not recognized."; + return {}; + } + return color; + } + return {}; + }; + + if (root.contains("colors")) { + auto colorsRoot = Json::requireObject(root, "colors"); + auto readAndSetPaletteColor = [&](QPalette::ColorRole role, const QString& colorName) { + auto color = readColor(colorsRoot, colorName); + if (color.isValid()) { + m_palette.setColor(role, color); + } else { + themeDebugLog() << "Color value for" << colorName << "was not present."; + } + }; + + // palette + readAndSetPaletteColor(QPalette::Window, "Window"); + readAndSetPaletteColor(QPalette::WindowText, "WindowText"); + readAndSetPaletteColor(QPalette::Base, "Base"); + readAndSetPaletteColor(QPalette::AlternateBase, "AlternateBase"); + readAndSetPaletteColor(QPalette::ToolTipBase, "ToolTipBase"); + readAndSetPaletteColor(QPalette::ToolTipText, "ToolTipText"); + readAndSetPaletteColor(QPalette::Text, "Text"); + readAndSetPaletteColor(QPalette::Button, "Button"); + readAndSetPaletteColor(QPalette::ButtonText, "ButtonText"); + readAndSetPaletteColor(QPalette::BrightText, "BrightText"); + readAndSetPaletteColor(QPalette::Link, "Link"); + readAndSetPaletteColor(QPalette::Highlight, "Highlight"); + readAndSetPaletteColor(QPalette::HighlightedText, "HighlightedText"); + + // fade + m_fadeColor = readColor(colorsRoot, "fadeColor"); + m_fadeAmount = Json::ensureDouble(colorsRoot, "fadeAmount", 0.5, "fade amount"); + } + + if (root.contains("logColors")) { + hasCustomLogColors = true; + + auto logColorsRoot = Json::requireObject(root, "logColors"); + auto readAndSetLogColor = [&](MessageLevel::Enum level, bool fg, const QString& colorName) { + auto color = readColor(logColorsRoot, colorName); + if (color.isValid()) { + if (fg) + m_logColors.foreground[level] = color; + else + m_logColors.background[level] = color; + } else { + themeDebugLog() << "Color value for" << colorName << "was not present."; + } + }; + + readAndSetLogColor(MessageLevel::Message, false, "MessageHighlight"); + readAndSetLogColor(MessageLevel::Launcher, false, "LauncherHighlight"); + readAndSetLogColor(MessageLevel::Debug, false, "DebugHighlight"); + readAndSetLogColor(MessageLevel::Warning, false, "WarningHighlight"); + readAndSetLogColor(MessageLevel::Error, false, "ErrorHighlight"); + readAndSetLogColor(MessageLevel::Fatal, false, "FatalHighlight"); + + readAndSetLogColor(MessageLevel::Message, true, "Message"); + readAndSetLogColor(MessageLevel::Launcher, true, "Launcher"); + readAndSetLogColor(MessageLevel::Debug, true, "Debug"); + readAndSetLogColor(MessageLevel::Warning, true, "Warning"); + readAndSetLogColor(MessageLevel::Error, true, "Error"); + readAndSetLogColor(MessageLevel::Fatal, true, "Fatal"); + } + } catch (const Exception& e) { + themeWarningLog() << "Couldn't load theme json: " << e.cause(); + return false; + } + } else { + themeDebugLog() << "No theme json present."; + return false; + } + return true; +} diff --git a/launcher/ui/themes/CustomTheme.h b/launcher/ui/themes/CustomTheme.h index 761a2bd90f..b8d0739212 100644 --- a/launcher/ui/themes/CustomTheme.h +++ b/launcher/ui/themes/CustomTheme.h @@ -2,6 +2,7 @@ /* * Prism Launcher - Minecraft Launcher * Copyright (C) 2024 Tayou + * Copyright (C) 2024 TheKodeToad * * 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 @@ -47,14 +48,16 @@ class CustomTheme : public ITheme { QString tooltip() override; bool hasStyleSheet() override; QString appStyleSheet() override; - bool hasColorScheme() override; QPalette colorScheme() override; double fadeAmount() override; QColor fadeColor() override; QString qtTheme() override; + LogColors logColorScheme() override { return m_logColors; } QStringList searchPaths() override; - private: /* data */ + private: + bool read(const QString& path, bool& hasCustomLogColors); + QPalette m_palette; QColor m_fadeColor; double m_fadeAmount; @@ -63,6 +66,7 @@ class CustomTheme : public ITheme { QString m_id; QString m_widgets; QString m_qssFilePath; + LogColors m_logColors; /** * The tooltip could be defined in the theme json, * or composed of other fields that could be in there. diff --git a/launcher/ui/themes/DarkTheme.cpp b/launcher/ui/themes/DarkTheme.cpp index 429d046ac8..804126547c 100644 --- a/launcher/ui/themes/DarkTheme.cpp +++ b/launcher/ui/themes/DarkTheme.cpp @@ -2,6 +2,7 @@ /* * Prism Launcher - Minecraft Launcher * Copyright (C) 2024 Tayou + * Copyright (C) 2024 TheKodeToad * * 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 @@ -46,11 +47,6 @@ QString DarkTheme::name() return QObject::tr("Dark"); } -bool DarkTheme::hasColorScheme() -{ - return true; -} - QPalette DarkTheme::colorScheme() { QPalette darkPalette; @@ -90,6 +86,7 @@ QString DarkTheme::appStyleSheet() { return "QToolTip { color: #ffffff; background-color: #2a82da; border: 1px solid white; }"; } + QString DarkTheme::tooltip() { return ""; diff --git a/launcher/ui/themes/DarkTheme.h b/launcher/ui/themes/DarkTheme.h index 819f6a9343..c97edbcbef 100644 --- a/launcher/ui/themes/DarkTheme.h +++ b/launcher/ui/themes/DarkTheme.h @@ -45,7 +45,6 @@ class DarkTheme : public FusionTheme { QString tooltip() override; bool hasStyleSheet() override; QString appStyleSheet() override; - bool hasColorScheme() override; QPalette colorScheme() override; double fadeAmount() override; QColor fadeColor() override; diff --git a/launcher/ui/themes/ITheme.cpp b/launcher/ui/themes/ITheme.cpp index 046ae16b40..cae6e90dba 100644 --- a/launcher/ui/themes/ITheme.cpp +++ b/launcher/ui/themes/ITheme.cpp @@ -44,9 +44,7 @@ void ITheme::apply(bool) { APPLICATION->setStyleSheet(QString()); QApplication::setStyle(new HintOverrideProxyStyle(QStyleFactory::create(qtTheme()))); - if (hasColorScheme()) { - QApplication::setPalette(colorScheme()); - } + QApplication::setPalette(colorScheme()); APPLICATION->setStyleSheet(appStyleSheet()); QDir::setSearchPaths("theme", searchPaths()); } @@ -73,3 +71,30 @@ QPalette ITheme::fadeInactive(QPalette in, qreal bias, QColor color) blend(QPalette::HighlightedText); return in; } + +LogColors ITheme::defaultLogColors(const QPalette& palette) +{ + LogColors result; + + const QColor& bg = palette.color(QPalette::Base); + const QColor& fg = palette.color(QPalette::Text); + + auto blend = [bg, fg](QColor color) { + if (Rainbow::luma(fg) > Rainbow::luma(bg)) { + // for dark color schemes, produce a fitting color first + color = Rainbow::tint(fg, color, 0.5); + } + // adapt contrast + return Rainbow::mix(fg, color, 1); + }; + + result.background[MessageLevel::Fatal] = Qt::black; + + result.foreground[MessageLevel::Launcher] = blend(QColor("purple")); + result.foreground[MessageLevel::Debug] = blend(QColor("green")); + result.foreground[MessageLevel::Warning] = blend(QColor("orange")); + result.foreground[MessageLevel::Error] = blend(QColor("red")); + result.foreground[MessageLevel::Fatal] = blend(QColor("red")); + + return result; +} diff --git a/launcher/ui/themes/ITheme.h b/launcher/ui/themes/ITheme.h index 45d3e27390..7dc5fc64a8 100644 --- a/launcher/ui/themes/ITheme.h +++ b/launcher/ui/themes/ITheme.h @@ -2,6 +2,7 @@ /* * Prism Launcher - Minecraft Launcher * Copyright (C) 2022 Tayou + * Copyright (C) 2024 TheKodeToad * * 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 @@ -33,11 +34,19 @@ * limitations under the License. */ #pragma once +#include +#include #include #include class QStyle; +struct LogColors { + QMap background; + QMap foreground; +}; + +// TODO: rename to Theme; this is not an interface as it contains method implementations class ITheme { public: virtual ~ITheme() {} @@ -48,11 +57,12 @@ class ITheme { virtual bool hasStyleSheet() = 0; virtual QString appStyleSheet() = 0; virtual QString qtTheme() = 0; - virtual bool hasColorScheme() = 0; virtual QPalette colorScheme() = 0; virtual QColor fadeColor() = 0; virtual double fadeAmount() = 0; + virtual LogColors logColorScheme() { return defaultLogColors(colorScheme()); } virtual QStringList searchPaths() { return {}; } static QPalette fadeInactive(QPalette in, qreal bias, QColor color); + static LogColors defaultLogColors(const QPalette& palette); }; diff --git a/launcher/ui/themes/SystemTheme.cpp b/launcher/ui/themes/SystemTheme.cpp index 70de218942..a1674455ab 100644 --- a/launcher/ui/themes/SystemTheme.cpp +++ b/launcher/ui/themes/SystemTheme.cpp @@ -40,11 +40,11 @@ #include "HintOverrideProxyStyle.h" #include "ThemeManager.h" -SystemTheme::SystemTheme(QString& styleName, bool isSystemTheme) +SystemTheme::SystemTheme(const QString& styleName, const QPalette& palette, bool isDefaultTheme) { - themeName = isSystemTheme ? "system" : styleName; + themeName = isDefaultTheme ? "system" : styleName; widgetTheme = styleName; - colorPalette = QApplication::palette(); + colorPalette = palette; } void SystemTheme::apply(bool initial) @@ -125,8 +125,3 @@ bool SystemTheme::hasStyleSheet() { return false; } - -bool SystemTheme::hasColorScheme() -{ - return true; -} diff --git a/launcher/ui/themes/SystemTheme.h b/launcher/ui/themes/SystemTheme.h index 5c58856cb5..7c260fdc49 100644 --- a/launcher/ui/themes/SystemTheme.h +++ b/launcher/ui/themes/SystemTheme.h @@ -38,7 +38,7 @@ class SystemTheme : public ITheme { public: - SystemTheme(QString& themeName, bool isSystemTheme = false); + SystemTheme(const QString& styleName, const QPalette& palette, bool isDefaultTheme); virtual ~SystemTheme() {} void apply(bool initial) override; @@ -48,7 +48,6 @@ class SystemTheme : public ITheme { QString qtTheme() override; bool hasStyleSheet() override; QString appStyleSheet() override; - bool hasColorScheme() override; QPalette colorScheme() override; double fadeAmount() override; QColor fadeColor() override; diff --git a/launcher/ui/themes/ThemeManager.cpp b/launcher/ui/themes/ThemeManager.cpp index d57e166f40..691a51668d 100644 --- a/launcher/ui/themes/ThemeManager.cpp +++ b/launcher/ui/themes/ThemeManager.cpp @@ -36,6 +36,13 @@ ThemeManager::ThemeManager() { + themeDebugLog() << "Determining System Widget Theme..."; + const auto& style = QApplication::style(); + m_defaultStyle = style->objectName(); + themeDebugLog() << "System theme seems to be:" << m_defaultStyle; + + m_defaultPalette = QApplication::palette(); + initializeThemes(); initializeCatPacks(); } @@ -121,13 +128,8 @@ void ThemeManager::initializeIcons() void ThemeManager::initializeWidgets() { - themeDebugLog() << "Determining System Widget Theme..."; - const auto& style = QApplication::style(); - currentlySelectedSystemTheme = style->objectName(); - themeDebugLog() << "System theme seems to be:" << currentlySelectedSystemTheme; - themeDebugLog() << "<> Initializing Widget Themes"; - themeDebugLog() << "Loading Built-in Theme:" << addTheme(std::make_unique(currentlySelectedSystemTheme, true)); + themeDebugLog() << "Loading Built-in Theme:" << addTheme(std::make_unique(m_defaultStyle, m_defaultPalette, true)); auto darkThemeId = addTheme(std::make_unique()); themeDebugLog() << "Loading Built-in Theme:" << darkThemeId; themeDebugLog() << "Loading Built-in Theme:" << addTheme(std::make_unique()); @@ -140,7 +142,7 @@ void ThemeManager::initializeWidgets() continue; } #endif - themeDebugLog() << "Loading System Theme:" << addTheme(std::make_unique(st)); + themeDebugLog() << "Loading System Theme:" << addTheme(std::make_unique(st, m_defaultPalette, false)); } // TODO: need some way to differentiate same name themes in different subdirectories @@ -196,8 +198,8 @@ QList ThemeManager::getValidApplicationThemes() QList ThemeManager::getValidCatPacks() { QList ret; - ret.reserve(m_cat_packs.size()); - for (auto&& [id, theme] : m_cat_packs) { + ret.reserve(m_catPacks.size()); + for (auto&& [id, theme] : m_catPacks) { ret.append(theme.get()); } return ret; @@ -246,6 +248,8 @@ void ThemeManager::setApplicationTheme(const QString& name, bool initial) auto& theme = themeIter->second; themeDebugLog() << "applying theme" << theme->name(); theme->apply(initial); + + m_logColors = theme->logColorScheme(); } else { themeWarningLog() << "Tried to set invalid theme:" << name; } @@ -258,7 +262,7 @@ void ThemeManager::applyCurrentlySelectedTheme(bool initial) themeDebugLog() << "<> Icon theme set."; auto applicationTheme = settings->get("ApplicationTheme").toString(); if (applicationTheme == "") { - applicationTheme = currentlySelectedSystemTheme; + applicationTheme = m_defaultStyle; } setApplicationTheme(applicationTheme, initial); themeDebugLog() << "<> Application theme set."; @@ -266,8 +270,8 @@ void ThemeManager::applyCurrentlySelectedTheme(bool initial) QString ThemeManager::getCatPack(QString catName) { - auto catIter = m_cat_packs.find(!catName.isEmpty() ? catName : APPLICATION->settings()->get("BackgroundCat").toString()); - if (catIter != m_cat_packs.end()) { + auto catIter = m_catPacks.find(!catName.isEmpty() ? catName : APPLICATION->settings()->get("BackgroundCat").toString()); + if (catIter != m_catPacks.end()) { auto& catPack = catIter->second; themeDebugLog() << "applying catpack" << catPack->id(); return catPack->path(); @@ -275,14 +279,14 @@ QString ThemeManager::getCatPack(QString catName) themeWarningLog() << "Tried to get invalid catPack:" << catName; } - return m_cat_packs.begin()->second->path(); + return m_catPacks.begin()->second->path(); } QString ThemeManager::addCatPack(std::unique_ptr catPack) { QString id = catPack->id(); - if (m_cat_packs.find(id) == m_cat_packs.end()) - m_cat_packs.emplace(id, std::move(catPack)); + if (m_catPacks.find(id) == m_catPacks.end()) + m_catPacks.emplace(id, std::move(catPack)); else themeWarningLog() << "CatPack(" << id << ") not added to prevent id duplication"; return id; @@ -340,8 +344,8 @@ void ThemeManager::refresh() { m_themes.clear(); m_icons.clear(); - m_cat_packs.clear(); + m_catPacks.clear(); initializeThemes(); initializeCatPacks(); -}; \ No newline at end of file +} \ No newline at end of file diff --git a/launcher/ui/themes/ThemeManager.h b/launcher/ui/themes/ThemeManager.h index 9d01d38e73..c7e32bc8c9 100644 --- a/launcher/ui/themes/ThemeManager.h +++ b/launcher/ui/themes/ThemeManager.h @@ -2,7 +2,7 @@ /* * Prism Launcher - Minecraft Launcher * Copyright (C) 2024 Tayou - * Copyright (C) 2023 TheKodeToad + * Copyright (C) 2024 TheKodeToad * * 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 @@ -55,6 +55,8 @@ class ThemeManager { QString getCatPack(QString catName = ""); QList getValidCatPacks(); + const LogColors& getLogColors() { return m_logColors; } + void refresh(); private: @@ -63,8 +65,10 @@ class ThemeManager { QDir m_iconThemeFolder{ "iconthemes" }; QDir m_applicationThemeFolder{ "themes" }; QDir m_catPacksFolder{ "catpacks" }; - std::map> m_cat_packs; - QString currentlySelectedSystemTheme; + std::map> m_catPacks; + QString m_defaultStyle; + QPalette m_defaultPalette; + LogColors m_logColors; void initializeThemes(); void initializeCatPacks(); diff --git a/launcher/ui/widgets/JavaSettingsWidget.cpp b/launcher/ui/widgets/JavaSettingsWidget.cpp index bd6b6b1181..bf58bf9f7d 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,19 @@ 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(); + auto checked = button == QMessageBox::Yes; + m_autodetectJavaCheckBox->setChecked(checked); + m_autodownloadCheckBox->setChecked(checked); + } } void JavaSettingsWidget::refresh() @@ -153,20 +206,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 +335,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({}, nullptr, this); + jdialog->exec(); +} + void JavaSettingsWidget::on_javaStatusBtn_clicked() { QString text; @@ -359,30 +445,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 +484,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 +504,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 +519,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; }; diff --git a/launcher/ui/widgets/VersionSelectWidget.cpp b/launcher/ui/widgets/VersionSelectWidget.cpp index a24630b31d..2d735d18fd 100644 --- a/launcher/ui/widgets/VersionSelectWidget.cpp +++ b/launcher/ui/widgets/VersionSelectWidget.cpp @@ -105,14 +105,14 @@ bool VersionSelectWidget::eventFilter(QObject* watched, QEvent* event) return QObject::eventFilter(watched, event); } -void VersionSelectWidget::initialize(BaseVersionList* vlist) +void VersionSelectWidget::initialize(BaseVersionList* vlist, bool forceLoad) { m_vlist = vlist; m_proxyModel->setSourceModel(vlist); listView->header()->setSectionResizeMode(QHeaderView::ResizeToContents); listView->header()->setSectionResizeMode(resizeOnColumn, QHeaderView::Stretch); - if (!m_vlist->isLoaded()) { + if (!m_vlist->isLoaded() || forceLoad) { loadList(); } else { if (m_proxyModel->rowCount() == 0) { @@ -129,16 +129,12 @@ void VersionSelectWidget::closeEvent(QCloseEvent* event) void VersionSelectWidget::loadList() { - auto newTask = m_vlist->getLoadTask(); - if (!newTask) { - return; - } - loadTask = newTask.get(); - connect(loadTask, &Task::succeeded, this, &VersionSelectWidget::onTaskSucceeded); - connect(loadTask, &Task::failed, this, &VersionSelectWidget::onTaskFailed); - connect(loadTask, &Task::progress, this, &VersionSelectWidget::changeProgress); - if (!loadTask->isRunning()) { - loadTask->start(); + m_load_task = m_vlist->getLoadTask(); + connect(m_load_task.get(), &Task::succeeded, this, &VersionSelectWidget::onTaskSucceeded); + connect(m_load_task.get(), &Task::failed, this, &VersionSelectWidget::onTaskFailed); + connect(m_load_task.get(), &Task::progress, this, &VersionSelectWidget::changeProgress); + if (!m_load_task->isRunning()) { + m_load_task->start(); } sneakyProgressBar->setHidden(false); } @@ -150,7 +146,7 @@ void VersionSelectWidget::onTaskSucceeded() } sneakyProgressBar->setHidden(true); preselect(); - loadTask = nullptr; + m_load_task.reset(); } void VersionSelectWidget::onTaskFailed(const QString& reason) diff --git a/launcher/ui/widgets/VersionSelectWidget.h b/launcher/ui/widgets/VersionSelectWidget.h index d5ef1cc9ff..c16d4c0ddd 100644 --- a/launcher/ui/widgets/VersionSelectWidget.h +++ b/launcher/ui/widgets/VersionSelectWidget.h @@ -54,7 +54,7 @@ class VersionSelectWidget : public QWidget { ~VersionSelectWidget(); //! loads the list if needed. - void initialize(BaseVersionList* vlist); + void initialize(BaseVersionList* vlist, bool forceLoad = false); //! Starts a task that loads the list. void loadList(); @@ -98,7 +98,7 @@ class VersionSelectWidget : public QWidget { BaseVersionList* m_vlist = nullptr; VersionProxyModel* m_proxyModel = nullptr; int resizeOnColumn = 0; - Task* loadTask; + Task::Ptr m_load_task; bool preselectedAlready = false; QVBoxLayout* verticalLayout = nullptr; diff --git a/launcher/ui/widgets/WideBar.cpp b/launcher/ui/widgets/WideBar.cpp index 46caaaef22..2940d7ce71 100644 --- a/launcher/ui/widgets/WideBar.cpp +++ b/launcher/ui/widgets/WideBar.cpp @@ -309,4 +309,15 @@ bool WideBar::checkHash(QByteArray const& old_hash) const return old_hash == getHash(); } +void WideBar::removeAction(QAction* action) +{ + auto iter = getMatching(action); + if (iter == m_entries.end()) + return; + + iter->bar_action->setVisible(false); + removeAction(iter->bar_action); + m_entries.erase(iter); +} + #include "WideBar.moc" diff --git a/launcher/ui/widgets/WideBar.h b/launcher/ui/widgets/WideBar.h index c47f3a596c..f4877a89ac 100644 --- a/launcher/ui/widgets/WideBar.h +++ b/launcher/ui/widgets/WideBar.h @@ -38,6 +38,8 @@ class WideBar : public QToolBar { [[nodiscard]] QByteArray getVisibilityState() const; void setVisibilityState(QByteArray&&); + void removeAction(QAction* action); + private: struct BarEntry { enum class Type { None, Action, Separator, Spacer } type = Type::None; diff --git a/libraries/launcher/org/prismlauncher/launcher/impl/AbstractLauncher.java b/libraries/launcher/org/prismlauncher/launcher/impl/AbstractLauncher.java index de28a04017..a5f027ba60 100644 --- a/libraries/launcher/org/prismlauncher/launcher/impl/AbstractLauncher.java +++ b/libraries/launcher/org/prismlauncher/launcher/impl/AbstractLauncher.java @@ -70,7 +70,7 @@ public abstract class AbstractLauncher implements Launcher { // secondary parameters protected final int width, height; protected final boolean maximize; - protected final String serverAddress, serverPort; + protected final String serverAddress, serverPort, worldName; protected final String mainClassName; @@ -80,6 +80,7 @@ protected AbstractLauncher(Parameters params) { serverAddress = params.getString("serverAddress", null); serverPort = params.getString("serverPort", null); + worldName = params.getString("worldName", null); String windowParams = params.getString("windowParams", null); diff --git a/libraries/launcher/org/prismlauncher/launcher/impl/StandardLauncher.java b/libraries/launcher/org/prismlauncher/launcher/impl/StandardLauncher.java index 49e5d518f6..dc518be64a 100644 --- a/libraries/launcher/org/prismlauncher/launcher/impl/StandardLauncher.java +++ b/libraries/launcher/org/prismlauncher/launcher/impl/StandardLauncher.java @@ -62,13 +62,15 @@ import java.util.List; public final class StandardLauncher extends AbstractLauncher { - private final boolean quickPlaySupported; + private final boolean quickPlayMultiplayerSupported; + private final boolean quickPlaySingleplayerSupported; public StandardLauncher(Parameters params) { super(params); List traits = params.getList("traits", Collections.emptyList()); - quickPlaySupported = traits.contains("feature:is_quick_play_multiplayer"); + quickPlayMultiplayerSupported = traits.contains("feature:is_quick_play_multiplayer"); + quickPlaySingleplayerSupported = traits.contains("feature:is_quick_play_singleplayer"); } @Override @@ -83,7 +85,7 @@ public void launch() throws Throwable { } if (serverAddress != null) { - if (quickPlaySupported) { + if (quickPlayMultiplayerSupported) { // as of 23w14a gameArgs.add("--quickPlayMultiplayer"); gameArgs.add(serverAddress + ':' + serverPort); @@ -93,6 +95,9 @@ public void launch() throws Throwable { gameArgs.add("--port"); gameArgs.add(serverPort); } + } else if (worldName != null && quickPlaySingleplayerSupported) { + gameArgs.add("--quickPlaySingleplayer"); + gameArgs.add(worldName); } // find and invoke the main method diff --git a/nix/README.md b/nix/README.md index f7923577f0..76cb8bf270 100644 --- a/nix/README.md +++ b/nix/README.md @@ -15,7 +15,6 @@ to temporarily enable it when using `nix` commands. Example (NixOS): ```nix -{...}: { nix.settings = { trusted-substituters = [ @@ -29,9 +28,9 @@ Example (NixOS): } ``` -### Using the overlay +### Installing the package directly -After adding `github:PrismLauncher/PrismLauncher` to your flake inputs, you can add the `default` overlay to your nixpkgs instance. +After adding `github:PrismLauncher/PrismLauncher` to your flake inputs, you can access the flake's `packages` output. Example: @@ -39,34 +38,47 @@ Example: { inputs = { nixpkgs.url = "github:NixOS/nixpkgs/nixos-unstable"; + prismlauncher = { url = "github:PrismLauncher/PrismLauncher"; + # Optional: Override the nixpkgs input of prismlauncher to use the same revision as the rest of your flake - # Note that overriding any input of prismlauncher may break reproducibility + # Note that this may break the reproducibility mentioned above, and you might not be able to access the binary cache + # # inputs.nixpkgs.follows = "nixpkgs"; + + # This is not required for Flakes + inputs.flake-compat.follows = ""; }; }; - outputs = {nixpkgs, prismlauncher}: { - nixosConfigurations.foo = nixpkgs.lib.nixosSystem { - system = "x86_64-linux"; - - modules = [ - ({pkgs, ...}: { - nixpkgs.overlays = [prismlauncher.overlays.default]; - - environment.systemPackages = [pkgs.prismlauncher]; - }) - ]; + outputs = + { nixpkgs, prismlauncher, ... }: + { + nixosConfigurations.foo = nixpkgs.lib.nixosSystem { + modules = [ + ./configuration.nix + + ( + { pkgs, ... }: + { + environment.systemPackages = [ prismlauncher.packages.${pkgs.system}.prismlauncher ]; + } + ) + ]; + }; }; - } } ``` -### Installing the package directly +### Using the overlay + +Alternatively, if you don't want to use our `packages` output, you can add our overlay to your nixpkgs instance. +This will ensure Prism is built with your system's packages. -Alternatively, if you don't want to use an overlay, you can install Prism Launcher directly by installing the `prismlauncher` package. -This way the installed package is fully reproducible. +> [!WARNING] +> Depending on what revision of nixpkgs your system uses, this may result in binaries that differ from the above `packages` output +> If this is the case, you will not be able to use the binary cache Example: @@ -74,25 +86,38 @@ Example: { inputs = { nixpkgs.url = "github:NixOS/nixpkgs/nixos-unstable"; + prismlauncher = { url = "github:PrismLauncher/PrismLauncher"; + # Optional: Override the nixpkgs input of prismlauncher to use the same revision as the rest of your flake - # Note that overriding any input of prismlauncher may break reproducibility + # Note that this may break the reproducibility mentioned above, and you might not be able to access the binary cache + # # inputs.nixpkgs.follows = "nixpkgs"; + + # This is not required for Flakes + inputs.flake-compat.follows = ""; }; }; - outputs = {nixpkgs, prismlauncher}: { - nixosConfigurations.foo = nixpkgs.lib.nixosSystem { - system = "x86_64-linux"; - - modules = [ - ({pkgs, ...}: { - environment.systemPackages = [prismlauncher.packages.${pkgs.system}.prismlauncher]; - }) - ]; + outputs = + { nixpkgs, prismlauncher, ... }: + { + nixosConfigurations.foo = nixpkgs.lib.nixosSystem { + modules = [ + ./configuration.nix + + ( + { pkgs, ... }: + { + nixpkgs.overlays = [ prismlauncher.overlays.default ]; + + environment.systemPackages = [ pkgs.prismlauncher ]; + } + ) + ]; + }; }; - } } ``` @@ -118,7 +143,6 @@ If you want to avoid rebuilds you may add the garnix cache to your substitutors. Example (NixOS): ```nix -{...}: { nix.settings = { trusted-substituters = [ @@ -132,30 +156,40 @@ Example (NixOS): } ``` -### Using the overlay (`fetchTarball`) +### Installing the package directly (`fetchTarball`) We use flake-compat to allow using this Flake on a system that doesn't use flakes. Example: ```nix -{pkgs, ...}: { - nixpkgs.overlays = [(import (builtins.fetchTarball "https://github.com/PrismLauncher/PrismLauncher/archive/develop.tar.gz")).overlays.default]; - - environment.systemPackages = [pkgs.prismlauncher]; +{ pkgs, ... }: +{ + environment.systemPackages = [ + (import ( + builtins.fetchTarball "https://github.com/PrismLauncher/PrismLauncher/archive/develop.tar.gz" + )).packages.${pkgs.system}.prismlauncher + ]; } ``` -### Installing the package directly (`fetchTarball`) +### Using the overlay (`fetchTarball`) -Alternatively, if you don't want to use an overlay, you can install Prism Launcher directly by installing the `prismlauncher` package. -This way the installed package is fully reproducible. +Alternatively, if you don't want to use our `packages` output, you can add our overlay to your instance of nixpkgs. +This results in Prism using your system's libraries Example: ```nix -{pkgs, ...}: { - environment.systemPackages = [(import (builtins.fetchTarball "https://github.com/PrismLauncher/PrismLauncher/archive/develop.tar.gz")).packages.${pkgs.system}.prismlauncher]; +{ pkgs, ... }: +{ + nixpkgs.overlays = [ + (import ( + builtins.fetchTarball "https://github.com/PrismLauncher/PrismLauncher/archive/develop.tar.gz" + )).overlays.default + ]; + + environment.systemPackages = [ pkgs.prismlauncher ]; } ``` @@ -177,18 +211,20 @@ nix-env -iA prismlauncher.prismlauncher Both Nixpkgs and this repository offer the following packages: -- `prismlauncher` - Preferred build using Qt 6 -- `prismlauncher-qt5` - Legacy build using Qt 5 (i.e. for Qt 5 theming support) - -Both of these packages also have `-unwrapped` counterparts, that are not wrapped and can therefore be customized even further than what the wrapper packages offer. +- `prismlauncher` - The preferred build, wrapped with everything necessary to run the launcher and Minecraft +- `prismlauncher-unwrapped` - A minimal build that allows for advanced customization of the launcher's runtime environment ### Customizing wrapped packages -The wrapped packages (`prismlauncher` and `prismlauncher-qt5`) offer some build parameters to further customize the launcher's environment. +The wrapped package (`prismlauncher`) offers some build parameters to further customize the launcher's environment. The following parameters can be overridden: -- `msaClientID` (default: `null`, requires full rebuild!) Client ID used for Microsoft Authentication -- `gamemodeSupport` (default: `true`) Turn on/off support for [Feral GameMode](https://github.com/FeralInteractive/gamemode) -- `jdks` (default: `[ jdk17 jdk8 ]`) Java runtimes added to `PRISMLAUNCHER_JAVA_PATHS` variable - `additionalLibs` (default: `[ ]`) Additional libraries that will be added to `LD_LIBRARY_PATH` +- `additionalPrograms` (default: `[ ]`) Additional libraries that will be added to `PATH` +- `controllerSupport` (default: `isLinux`) Turn on/off support for controllers on Linux (macOS will always have this) +- `gamemodeSupport` (default: `isLinux`) Turn on/off support for [Feral GameMode](https://github.com/FeralInteractive/gamemode) on Linux +- `jdks` (default: `[ jdk21 jdk17 jdk8 ]`) Java runtimes added to `PRISMLAUNCHER_JAVA_PATHS` variable +- `msaClientID` (default: `null`, requires full rebuild!) Client ID used for Microsoft Authentication +- `textToSpeechSupport` (default: `isLinux`) Turn on/off support for text-to-speech on Linux (macOS will always have this) +- `withWaylandGLFW` (default: `isLinux`) Build with support for native Wayland via a custom GLFW diff --git a/nix/checks.nix b/nix/checks.nix new file mode 100644 index 0000000000..40a2e272f0 --- /dev/null +++ b/nix/checks.nix @@ -0,0 +1,42 @@ +{ + runCommand, + deadnix, + llvmPackages_18, + markdownlint-cli, + nixfmt-rfc-style, + statix, + self, +}: +{ + formatting = + runCommand "check-formatting" + { + nativeBuildInputs = [ + deadnix + llvmPackages_18.clang-tools + markdownlint-cli + nixfmt-rfc-style + statix + ]; + } + '' + cd ${self} + + echo "Running clang-format...." + clang-format -i --style='file' --Werror */**.{c,cc,cpp,h,hh,hpp} + + echo "Running deadnix..." + deadnix --fail + + echo "Running markdownlint..." + markdownlint --dot . + + echo "Running nixfmt..." + nixfmt --check . + + echo "Running statix" + statix check . + + touch $out + ''; +} diff --git a/nix/dev.nix b/nix/dev.nix deleted file mode 100644 index cf61449a74..0000000000 --- a/nix/dev.nix +++ /dev/null @@ -1,36 +0,0 @@ -{ - perSystem = { - config, - lib, - pkgs, - ... - }: { - pre-commit.settings = { - hooks = { - markdownlint.enable = true; - - alejandra.enable = true; - deadnix.enable = true; - nil.enable = true; - - clang-format = { - enable = true; - types_or = ["c" "c++" "java" "json" "objective-c"]; - }; - }; - - tools.clang-tools = lib.mkForce pkgs.clang-tools_18; - }; - - devShells.default = pkgs.mkShell { - shellHook = '' - ${config.pre-commit.installationScript} - ''; - - inputsFrom = [config.packages.prismlauncher-unwrapped]; - buildInputs = with pkgs; [ccache ninja]; - }; - - formatter = pkgs.alejandra; - }; -} diff --git a/nix/distribution.nix b/nix/distribution.nix deleted file mode 100644 index 28ef7ced11..0000000000 --- a/nix/distribution.nix +++ /dev/null @@ -1,37 +0,0 @@ -{ - inputs, - self, - ... -}: { - perSystem = { - lib, - pkgs, - ... - }: { - packages = let - ourPackages = lib.fix (final: self.overlays.default final pkgs); - in { - inherit - (ourPackages) - prismlauncher-unwrapped - prismlauncher - ; - default = ourPackages.prismlauncher; - }; - }; - - flake = { - overlays.default = final: prev: let - version = builtins.substring 0 8 self.lastModifiedDate or "dirty"; - in { - prismlauncher-unwrapped = prev.callPackage ./pkg { - inherit (inputs) libnbtplusplus; - inherit version; - }; - - prismlauncher = prev.qt6Packages.callPackage ./pkg/wrapper.nix { - inherit (final) prismlauncher-unwrapped; - }; - }; - }; -} diff --git a/nix/pkg/default.nix b/nix/pkg/default.nix deleted file mode 100644 index f3ff3789c6..0000000000 --- a/nix/pkg/default.nix +++ /dev/null @@ -1,106 +0,0 @@ -{ - lib, - stdenv, - cmake, - cmark, - darwin, - extra-cmake-modules, - gamemode, - ghc_filesystem, - jdk17, - kdePackages, - ninja, - stripJavaArchivesHook, - tomlplusplus, - zlib, - msaClientID ? null, - gamemodeSupport ? stdenv.isLinux, - version, - libnbtplusplus, -}: -assert lib.assertMsg ( - gamemodeSupport -> stdenv.isLinux -) "gamemodeSupport is only available on Linux."; - stdenv.mkDerivation { - pname = "prismlauncher-unwrapped"; - inherit version; - - src = lib.fileset.toSource { - root = ../../.; - fileset = lib.fileset.unions (map (fileName: ../../${fileName}) [ - "buildconfig" - "cmake" - "launcher" - "libraries" - "program_info" - "tests" - "COPYING.md" - "CMakeLists.txt" - ]); - }; - - postUnpack = '' - rm -rf source/libraries/libnbtplusplus - ln -s ${libnbtplusplus} source/libraries/libnbtplusplus - ''; - - nativeBuildInputs = [ - cmake - ninja - extra-cmake-modules - jdk17 - stripJavaArchivesHook - ]; - - buildInputs = - [ - cmark - ghc_filesystem - kdePackages.qtbase - kdePackages.qtnetworkauth - kdePackages.quazip - tomlplusplus - zlib - ] - ++ lib.optionals stdenv.isDarwin [darwin.apple_sdk.frameworks.Cocoa] - ++ lib.optional gamemodeSupport gamemode; - - hardeningEnable = lib.optionals stdenv.isLinux ["pie"]; - - cmakeFlags = - [ - (lib.cmakeFeature "Launcher_BUILD_PLATFORM" "nixpkgs") - ] - ++ lib.optionals (msaClientID != null) [ - (lib.cmakeFeature "Launcher_MSA_CLIENT_ID" (toString msaClientID)) - ] - ++ lib.optionals (lib.versionOlder kdePackages.qtbase.version "6") [ - (lib.cmakeFeature "Launcher_QT_VERSION_MAJOR" "5") - ] - ++ lib.optionals stdenv.isDarwin [ - # we wrap our binary manually - (lib.cmakeFeature "INSTALL_BUNDLE" "nodeps") - # disable built-in updater - (lib.cmakeFeature "MACOSX_SPARKLE_UPDATE_FEED_URL" "''") - (lib.cmakeFeature "CMAKE_INSTALL_PREFIX" "${placeholder "out"}/Applications/") - ]; - - dontWrapQtApps = true; - - meta = { - description = "Free, open source launcher for Minecraft"; - longDescription = '' - Allows you to have multiple, separate instances of Minecraft (each with - their own mods, texture packs, saves, etc) and helps you manage them and - their associated options with a simple interface. - ''; - homepage = "https://prismlauncher.org/"; - license = lib.licenses.gpl3Only; - maintainers = with lib.maintainers; [ - Scrumplex - getchoo - ]; - mainProgram = "prismlauncher"; - platforms = lib.platforms.linux ++ lib.platforms.darwin; - }; - } diff --git a/nix/pkg/wrapper.nix b/nix/pkg/wrapper.nix deleted file mode 100644 index e7516397ed..0000000000 --- a/nix/pkg/wrapper.nix +++ /dev/null @@ -1,145 +0,0 @@ -{ - lib, - stdenv, - symlinkJoin, - prismlauncher-unwrapped, - addOpenGLRunpath, - flite, - gamemode, - glfw, - glfw-wayland-minecraft, - glxinfo, - jdk8, - jdk17, - jdk21, - kdePackages, - libGL, - libpulseaudio, - libusb1, - makeWrapper, - openal, - pciutils, - udev, - vulkan-loader, - xorg, - additionalLibs ? [], - additionalPrograms ? [], - controllerSupport ? stdenv.isLinux, - gamemodeSupport ? stdenv.isLinux, - jdks ? [ - jdk21 - jdk17 - jdk8 - ], - msaClientID ? null, - textToSpeechSupport ? stdenv.isLinux, - # Adds `glfw-wayland-minecraft` to `LD_LIBRARY_PATH` - # when launched on wayland, allowing for the game to be run natively. - # Make sure to enable "Use system installation of GLFW" in instance settings - # for this to take effect - # - # Warning: This build of glfw may be unstable, and the launcher - # itself can take slightly longer to start - withWaylandGLFW ? false, -}: -assert lib.assertMsg ( - controllerSupport -> stdenv.isLinux -) "controllerSupport only has an effect on Linux."; -assert lib.assertMsg ( - textToSpeechSupport -> stdenv.isLinux -) "textToSpeechSupport only has an effect on Linux."; -assert lib.assertMsg ( - withWaylandGLFW -> stdenv.isLinux -) "withWaylandGLFW is only available on Linux."; let - prismlauncher' = prismlauncher-unwrapped.override {inherit msaClientID gamemodeSupport;}; -in - symlinkJoin { - name = "prismlauncher-${prismlauncher'.version}"; - - paths = [prismlauncher']; - - nativeBuildInputs = - [kdePackages.wrapQtAppsHook] - # purposefully using a shell wrapper here for variable expansion - # see https://github.com/NixOS/nixpkgs/issues/172583 - ++ lib.optional withWaylandGLFW makeWrapper; - - buildInputs = - [ - kdePackages.qtbase - kdePackages.qtsvg - ] - ++ lib.optional ( - lib.versionAtLeast kdePackages.qtbase.version "6" && stdenv.isLinux - ) - kdePackages.qtwayland; - - env = { - waylandPreExec = lib.optionalString withWaylandGLFW '' - if [ -n "$WAYLAND_DISPLAY" ]; then - export LD_LIBRARY_PATH=${lib.getLib glfw-wayland-minecraft}/lib:"$LD_LIBRARY_PATH" - fi - ''; - }; - - postBuild = - lib.optionalString withWaylandGLFW '' - qtWrapperArgs+=(--run "$waylandPreExec") - '' - + '' - wrapQtAppsHook - ''; - - qtWrapperArgs = let - runtimeLibs = - [ - # lwjgl - glfw - libpulseaudio - libGL - openal - stdenv.cc.cc.lib - - vulkan-loader # VulkanMod's lwjgl - - udev # oshi - - xorg.libX11 - xorg.libXext - xorg.libXcursor - xorg.libXrandr - xorg.libXxf86vm - ] - ++ lib.optional textToSpeechSupport flite - ++ lib.optional gamemodeSupport gamemode.lib - ++ lib.optional controllerSupport libusb1 - ++ additionalLibs; - - runtimePrograms = - [ - glxinfo - pciutils # need lspci - xorg.xrandr # needed for LWJGL [2.9.2, 3) https://github.com/LWJGL/lwjgl/issues/128 - ] - ++ additionalPrograms; - in - ["--prefix PRISMLAUNCHER_JAVA_PATHS : ${lib.makeSearchPath "bin/java" jdks}"] - ++ lib.optionals stdenv.isLinux [ - "--set LD_LIBRARY_PATH ${addOpenGLRunpath.driverLink}/lib:${lib.makeLibraryPath runtimeLibs}" - "--prefix PATH : ${lib.makeBinPath runtimePrograms}" - ]; - - meta = { - inherit - (prismlauncher'.meta) - description - longDescription - homepage - changelog - license - maintainers - mainProgram - platforms - ; - }; - } diff --git a/nix/unwrapped.nix b/nix/unwrapped.nix new file mode 100644 index 0000000000..f75acf1de8 --- /dev/null +++ b/nix/unwrapped.nix @@ -0,0 +1,108 @@ +{ + lib, + stdenv, + cmake, + cmark, + darwin, + extra-cmake-modules, + gamemode, + ghc_filesystem, + jdk17, + kdePackages, + ninja, + nix-filter, + self, + stripJavaArchivesHook, + tomlplusplus, + zlib, + msaClientID ? null, + gamemodeSupport ? stdenv.isLinux, + version, + libnbtplusplus, +}: + +assert lib.assertMsg ( + gamemodeSupport -> stdenv.isLinux +) "gamemodeSupport is only available on Linux."; + +stdenv.mkDerivation { + pname = "prismlauncher-unwrapped"; + inherit version; + + src = nix-filter.lib { + root = self; + include = [ + "buildconfig" + "cmake" + "launcher" + "libraries" + "program_info" + "tests" + ../COPYING.md + ../CMakeLists.txt + ]; + }; + + postUnpack = '' + rm -rf source/libraries/libnbtplusplus + ln -s ${libnbtplusplus} source/libraries/libnbtplusplus + ''; + + nativeBuildInputs = [ + cmake + ninja + extra-cmake-modules + jdk17 + stripJavaArchivesHook + ]; + + buildInputs = + [ + cmark + ghc_filesystem + kdePackages.qtbase + kdePackages.qtnetworkauth + kdePackages.quazip + tomlplusplus + zlib + ] + ++ lib.optionals stdenv.isDarwin [ darwin.apple_sdk.frameworks.Cocoa ] + ++ lib.optional gamemodeSupport gamemode; + + hardeningEnable = lib.optionals stdenv.isLinux [ "pie" ]; + + cmakeFlags = + [ (lib.cmakeFeature "Launcher_BUILD_PLATFORM" "nixpkgs") ] + ++ lib.optionals (msaClientID != null) [ + (lib.cmakeFeature "Launcher_MSA_CLIENT_ID" (toString msaClientID)) + ] + ++ lib.optionals (lib.versionOlder kdePackages.qtbase.version "6") [ + (lib.cmakeFeature "Launcher_QT_VERSION_MAJOR" "5") + ] + ++ lib.optionals stdenv.isDarwin [ + # we wrap our binary manually + (lib.cmakeFeature "INSTALL_BUNDLE" "nodeps") + # disable built-in updater + (lib.cmakeFeature "MACOSX_SPARKLE_UPDATE_FEED_URL" "''") + (lib.cmakeFeature "CMAKE_INSTALL_PREFIX" "${placeholder "out"}/Applications/") + ]; + + dontWrapQtApps = true; + + meta = { + description = "Free, open source launcher for Minecraft"; + longDescription = '' + Allows you to have multiple, separate instances of Minecraft (each with + their own mods, texture packs, saves, etc) and helps you manage them and + their associated options with a simple interface. + ''; + homepage = "https://prismlauncher.org/"; + license = lib.licenses.gpl3Only; + maintainers = with lib.maintainers; [ + Scrumplex + getchoo + ]; + mainProgram = "prismlauncher"; + platforms = lib.platforms.linux ++ lib.platforms.darwin; + }; +} diff --git a/nix/wrapper.nix b/nix/wrapper.nix new file mode 100644 index 0000000000..5632d483ba --- /dev/null +++ b/nix/wrapper.nix @@ -0,0 +1,147 @@ +{ + lib, + stdenv, + symlinkJoin, + prismlauncher-unwrapped, + addOpenGLRunpath, + flite, + gamemode, + glfw, + glfw-wayland-minecraft, + glxinfo, + jdk8, + jdk17, + jdk21, + kdePackages, + libGL, + libpulseaudio, + libusb1, + makeWrapper, + openal, + pciutils, + udev, + vulkan-loader, + xorg, + additionalLibs ? [ ], + additionalPrograms ? [ ], + controllerSupport ? stdenv.isLinux, + gamemodeSupport ? stdenv.isLinux, + jdks ? [ + jdk21 + jdk17 + jdk8 + ], + msaClientID ? null, + textToSpeechSupport ? stdenv.isLinux, + # Adds `glfw-wayland-minecraft` to `LD_LIBRARY_PATH` + # when launched on wayland, allowing for the game to be run natively. + # Make sure to enable "Use system installation of GLFW" in instance settings + # for this to take effect + # + # Warning: This build of glfw may be unstable, and the launcher + # itself can take slightly longer to start + withWaylandGLFW ? false, +}: + +assert lib.assertMsg ( + controllerSupport -> stdenv.isLinux +) "controllerSupport only has an effect on Linux."; + +assert lib.assertMsg ( + textToSpeechSupport -> stdenv.isLinux +) "textToSpeechSupport only has an effect on Linux."; + +assert lib.assertMsg ( + withWaylandGLFW -> stdenv.isLinux +) "withWaylandGLFW is only available on Linux."; + +let + prismlauncher' = prismlauncher-unwrapped.override { inherit msaClientID gamemodeSupport; }; +in +symlinkJoin { + name = "prismlauncher-${prismlauncher'.version}"; + + paths = [ prismlauncher' ]; + + nativeBuildInputs = + [ kdePackages.wrapQtAppsHook ] + # purposefully using a shell wrapper here for variable expansion + # see https://github.com/NixOS/nixpkgs/issues/172583 + ++ lib.optional withWaylandGLFW makeWrapper; + + buildInputs = + [ + kdePackages.qtbase + kdePackages.qtsvg + ] + ++ lib.optional ( + lib.versionAtLeast kdePackages.qtbase.version "6" && stdenv.isLinux + ) kdePackages.qtwayland; + + env = { + waylandPreExec = lib.optionalString withWaylandGLFW '' + if [ -n "$WAYLAND_DISPLAY" ]; then + export LD_LIBRARY_PATH=${lib.getLib glfw-wayland-minecraft}/lib:"$LD_LIBRARY_PATH" + fi + ''; + }; + + postBuild = + lib.optionalString withWaylandGLFW '' + qtWrapperArgs+=(--run "$waylandPreExec") + '' + + '' + wrapQtAppsHook + ''; + + qtWrapperArgs = + let + runtimeLibs = + [ + # lwjgl + glfw + libpulseaudio + libGL + openal + stdenv.cc.cc.lib + + vulkan-loader # VulkanMod's lwjgl + + udev # oshi + + xorg.libX11 + xorg.libXext + xorg.libXcursor + xorg.libXrandr + xorg.libXxf86vm + ] + ++ lib.optional textToSpeechSupport flite + ++ lib.optional gamemodeSupport gamemode.lib + ++ lib.optional controllerSupport libusb1 + ++ additionalLibs; + + runtimePrograms = [ + glxinfo + pciutils # need lspci + xorg.xrandr # needed for LWJGL [2.9.2, 3) https://github.com/LWJGL/lwjgl/issues/128 + ] ++ additionalPrograms; + in + [ "--prefix PRISMLAUNCHER_JAVA_PATHS : ${lib.makeSearchPath "bin/java" jdks}" ] + ++ lib.optionals stdenv.isLinux [ + "--set LD_LIBRARY_PATH ${addOpenGLRunpath.driverLink}/lib:${lib.makeLibraryPath runtimeLibs}" + "--prefix PATH : ${lib.makeBinPath runtimePrograms}" + ]; + + meta = { + inherit (prismlauncher'.meta) + description + longDescription + homepage + changelog + license + maintainers + mainProgram + platforms + ; + }; +}