diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 4eb663f0c..a1ed0a25a 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -173,7 +173,7 @@ jobs: - name: Retrieve ccache cache (Windows MinGW-w64) if: runner.os == 'Windows' && matrix.msystem != '' && inputs.build_type == 'Debug' - uses: actions/cache@v4.0.2 + uses: actions/cache@v4.1.1 with: path: '${{ github.workspace }}\.ccache' key: ${{ matrix.os }}-mingw-w64-ccache-${{ github.run_id }} @@ -410,9 +410,8 @@ jobs: if: matrix.name == 'macOS' run: | if [ '${{ secrets.SPARKLE_ED25519_KEY }}' != '' ]; then - brew install openssl@3 echo '${{ secrets.SPARKLE_ED25519_KEY }}' > ed25519-priv.pem - signature=$(/usr/local/opt/openssl@3/bin/openssl pkeyutl -sign -rawin -in ${{ github.workspace }}/PrismLauncher.zip -inkey ed25519-priv.pem | openssl base64 | tr -d \\n) + signature=$(/opt/homebrew/opt/openssl@3/bin/openssl pkeyutl -sign -rawin -in ${{ github.workspace }}/PrismLauncher.zip -inkey ed25519-priv.pem | openssl base64 | tr -d \\n) rm ed25519-priv.pem cat >> $GITHUB_STEP_SUMMARY << EOF ### Artifact Information :information_source: @@ -672,7 +671,7 @@ jobs: uses: actions/checkout@v4 - name: Install Nix - uses: cachix/install-nix-action@v29 + uses: cachix/install-nix-action@v30 # For PRs - name: Setup Nix Magic Cache diff --git a/.github/workflows/update-flake.yml b/.github/workflows/update-flake.yml index 0c8cfde67..5e978f356 100644 --- a/.github/workflows/update-flake.yml +++ b/.github/workflows/update-flake.yml @@ -17,7 +17,7 @@ jobs: steps: - uses: actions/checkout@v4 - - uses: cachix/install-nix-action@9f70348d77d0422624097c4b7a75563948901306 # v29 + - uses: cachix/install-nix-action@08dcb3a5e62fa31e2da3d490afc4176ef55ecd72 # v30 - uses: DeterminateSystems/update-flake-lock@v24 with: diff --git a/CMakeLists.txt b/CMakeLists.txt index 3d70fe79b..b677b0b7c 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -99,7 +99,7 @@ if ((CMAKE_BUILD_TYPE STREQUAL "Debug" OR CMAKE_BUILD_TYPE STREQUAL "RelWithDebI message(STATUS "Address Sanitizer enabled for Debug builds, Turn it off with -DDEBUG_ADDRESS_SANITIZER=off") if ("${CMAKE_CXX_COMPILER_ID}" MATCHES "Clang") if (CMAKE_CXX_COMPILER_FRONTEND_VARIANT STREQUAL "MSVC") - # using clang with clang-cl front end + # using clang with clang-cl front end message(STATUS "Address Sanitizer available on Clang MSVC frontend") set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} /fsanitize=address /Oy-") set(CMAKE_C_FLAGS "${CMAKE_C_FLAGS} /fsanitize=address /Oy-") @@ -225,7 +225,7 @@ set(Launcher_QT_VERSION_MAJOR "6" CACHE STRING "Major Qt version to build agains # Java downloader set(Launcher_ENABLE_JAVA_DOWNLOADER_DEFAULT ON) -# Although we recommend enabling this, we cannot guarantee binary compatibility 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) @@ -438,10 +438,10 @@ elseif(UNIX) set(PLUGIN_DEST_DIR "plugins") set(BUNDLE_DEST_DIR ".") set(RESOURCES_DEST_DIR ".") - + # Apps to bundle set(APPS "\${CMAKE_INSTALL_PREFIX}/bin/${Launcher_APP_BINARY_NAME}") - + # directories to look for dependencies set(DIRS ${QT_LIBS_DIR} ${QT_LIBEXECS_DIR} ${CMAKE_LIBRARY_OUTPUT_DIRECTORY} ${CMAKE_RUNTIME_OUTPUT_DIRECTORY}) endif() @@ -495,7 +495,7 @@ if(FORCE_BUNDLED_ZLIB) set(SKIP_INSTALL_ALL ON) add_subdirectory(libraries/zlib EXCLUDE_FROM_ALL) - # On OS where unistd.h exists, zlib's generated header defines `Z_HAVE_UNISTD_H`, while the included header does not. + # On OS where unistd.h exists, zlib's generated header defines `Z_HAVE_UNISTD_H`, while the included header does not. # We cannot safely undo the rename on those systems, and they generally have packages for zlib anyway. check_include_file(unistd.h NEED_GENERATED_ZCONF) if (EXISTS "${CMAKE_CURRENT_SOURCE_DIR}/libraries/zlib/zconf.h.included" AND NOT NEED_GENERATED_ZCONF) @@ -532,10 +532,12 @@ else() endif() if(NOT cmark_FOUND) message(STATUS "Using bundled cmark") + set(ORIGINAL_BUILD_TESTING ${BUILD_TESTING}) set(BUILD_TESTING 0) - set(BUILD_SHARED_LIBS 0) + set(BUILD_SHARED_LIBS 0) add_subdirectory(libraries/cmark EXCLUDE_FROM_ALL) # Markdown parser add_library(cmark::cmark ALIAS cmark) + set(BUILD_TESTING ${ORIGINAL_BUILD_TESTING}) else() message(STATUS "Using system cmark") endif() diff --git a/flake.lock b/flake.lock index 6897c162d..a82e6f65f 100644 --- a/flake.lock +++ b/flake.lock @@ -49,11 +49,11 @@ }, "nixpkgs": { "locked": { - "lastModified": 1728018373, - "narHash": "sha256-NOiTvBbRLIOe5F6RbHaAh6++BNjsb149fGZd1T4+KBg=", + "lastModified": 1729256560, + "narHash": "sha256-/uilDXvCIEs3C9l73JTACm4quuHUsIHcns1c+cHUJwA=", "owner": "NixOS", "repo": "nixpkgs", - "rev": "bc947f541ae55e999ffdb4013441347d83b00feb", + "rev": "4c2fcb090b1f3e5b47eaa7bd33913b574a11e0a0", "type": "github" }, "original": { diff --git a/flatpak/org.prismlauncher.PrismLauncher.yml b/flatpak/org.prismlauncher.PrismLauncher.yml index bd09f7fd8..09dd8d73b 100644 --- a/flatpak/org.prismlauncher.PrismLauncher.yml +++ b/flatpak/org.prismlauncher.PrismLauncher.yml @@ -3,9 +3,7 @@ runtime: org.kde.Platform runtime-version: 6.7 sdk: org.kde.Sdk sdk-extensions: - - org.freedesktop.Sdk.Extension.openjdk21 - org.freedesktop.Sdk.Extension.openjdk17 - - org.freedesktop.Sdk.Extension.openjdk8 command: prismlauncher finish-args: @@ -22,9 +20,6 @@ finish-args: # FTBApp import - --filesystem=~/.ftba:ro -cleanup: - - /lib/libGLU* - modules: # Might be needed by some Controller mods (see https://github.com/isXander/Controlify/issues/31) - shared-modules/libusb/libusb.json @@ -42,23 +37,11 @@ modules: env: JAVA_HOME: /usr/lib/sdk/openjdk17/jvm/openjdk-17 JAVA_COMPILER: /usr/lib/sdk/openjdk17/jvm/openjdk-17/bin/javac + run-tests: true sources: - type: dir path: ../ - - name: openjdk - buildsystem: simple - build-commands: - - mkdir -p /app/jdk/ - - /usr/lib/sdk/openjdk21/install.sh - - mv /app/jre /app/jdk/21 - - /usr/lib/sdk/openjdk17/install.sh - - mv /app/jre /app/jdk/17 - - /usr/lib/sdk/openjdk8/install.sh - - mv /app/jre /app/jdk/8 - cleanup: - - /jre - - name: glfw buildsystem: cmake-ninja config-opts: diff --git a/launcher/Application.cpp b/launcher/Application.cpp index b8dcc1099..ea749ca4c 100644 --- a/launcher/Application.cpp +++ b/launcher/Application.cpp @@ -1071,6 +1071,9 @@ Application::Application(int& argc, char** argv) : QApplication(argc, argv) bool Application::createSetupWizard() { bool javaRequired = [&]() { + if (BuildConfig.JAVA_DOWNLOADER_ENABLED && m_settings->get("AutomaticJavaDownload").toBool()) { + return false; + } bool ignoreJavaWizard = m_settings->get("IgnoreJavaWizard").toBool(); if (ignoreJavaWizard) { return false; @@ -1083,10 +1086,7 @@ bool Application::createSetupWizard() } QString currentJavaPath = settings()->get("JavaPath").toString(); QString actualPath = FS::ResolveExecutable(currentJavaPath); - if (actualPath.isNull()) { - return true; - } - return false; + return actualPath.isNull(); }(); bool askjava = BuildConfig.JAVA_DOWNLOADER_ENABLED && !javaRequired && !m_settings->get("AutomaticJavaDownload").toBool() && !m_settings->get("AutomaticJavaSwitch").toBool() && !m_settings->get("UserAskedAboutAutomaticJavaDownload").toBool(); @@ -1883,3 +1883,31 @@ const QString Application::javaPath() { return m_settings->get("JavaDir").toString(); } + +void Application::addQSavePath(QString path) +{ + QMutexLocker locker(&m_qsaveResourcesMutex); + m_qsaveResources[path] = m_qsaveResources.value(path, 0) + 1; +} + +void Application::removeQSavePath(QString path) +{ + QMutexLocker locker(&m_qsaveResourcesMutex); + auto count = m_qsaveResources.value(path, 0) - 1; + if (count <= 0) { + m_qsaveResources.remove(path); + } else { + m_qsaveResources[path] = count; + } +} + +bool Application::checkQSavePath(QString path) +{ + QMutexLocker locker(&m_qsaveResourcesMutex); + for (auto partialPath : m_qsaveResources.keys()) { + if (path.startsWith(partialPath) && m_qsaveResources.value(partialPath, 0) > 0) { + return true; + } + } + return false; +} \ No newline at end of file diff --git a/launcher/Application.h b/launcher/Application.h index 7432c9683..692625f4a 100644 --- a/launcher/Application.h +++ b/launcher/Application.h @@ -42,6 +42,7 @@ #include #include #include +#include #include #include @@ -81,6 +82,12 @@ class Index; #endif #define APPLICATION (static_cast(QCoreApplication::instance())) +// Used for checking if is a test +#if defined(APPLICATION_DYN) +#undef APPLICATION_DYN +#endif +#define APPLICATION_DYN (dynamic_cast(QCoreApplication::instance())) + class Application : public QApplication { // friends for the purpose of limiting access to deprecated stuff Q_OBJECT @@ -297,4 +304,13 @@ class Application : public QApplication { QList m_urlsToImport; QString m_instanceIdToShowWindowOf; std::unique_ptr logFile; + + public: + void addQSavePath(QString); + void removeQSavePath(QString); + bool checkQSavePath(QString); + + private: + QHash m_qsaveResources; + mutable QMutex m_qsaveResourcesMutex; }; diff --git a/launcher/CMakeLists.txt b/launcher/CMakeLists.txt index e7c62b570..db3762a8b 100644 --- a/launcher/CMakeLists.txt +++ b/launcher/CMakeLists.txt @@ -30,6 +30,7 @@ set(CORE_SOURCES StringUtils.cpp QVariantUtils.h RuntimeContext.h + PSaveFile.h # Basic instance manipulation tasks (derived from InstanceTask) InstanceCreationTask.h @@ -607,7 +608,7 @@ set(PRISMUPDATER_SOURCES updater/prismupdater/UpdaterDialogs.cpp updater/prismupdater/GitHubRelease.h updater/prismupdater/GitHubRelease.cpp - + Json.h Json.cpp FileSystem.h @@ -624,7 +625,7 @@ set(PRISMUPDATER_SOURCES # Zip MMCZip.h MMCZip.cpp - + # Time MMCTime.h MMCTime.cpp @@ -1264,14 +1265,10 @@ include(CompilerWarnings) # Add executable add_library(Launcher_logic STATIC ${LOGIC_SOURCES} ${LAUNCHER_SOURCES} ${LAUNCHER_UI} ${LAUNCHER_RESOURCES}) -if(BUILD_TESTING) -target_compile_definitions(Launcher_logic PUBLIC LAUNCHER_TEST) -endif() set_project_warnings(Launcher_logic "${Launcher_MSVC_WARNINGS}" "${Launcher_CLANG_WARNINGS}" "${Launcher_GCC_WARNINGS}") -target_compile_definitions(Launcher_logic PUBLIC LAUNCHER_APPLICATION) target_include_directories(Launcher_logic PUBLIC ${CMAKE_CURRENT_SOURCE_DIR}) target_compile_definitions(Launcher_logic PUBLIC LAUNCHER_APPLICATION) target_link_libraries(Launcher_logic @@ -1375,7 +1372,7 @@ if(Launcher_BUILD_UPDATER) add_executable("${Launcher_Name}_updater" WIN32 updater/prismupdater/updater_main.cpp) target_sources("${Launcher_Name}_updater" PRIVATE updater/prismupdater/updater.exe.manifest) target_link_libraries("${Launcher_Name}_updater" prism_updater_logic) - + if(DEFINED Launcher_APP_BINARY_NAME) set_target_properties("${Launcher_Name}_updater" PROPERTIES OUTPUT_NAME "${Launcher_APP_BINARY_NAME}_updater") endif() diff --git a/launcher/FileSystem.cpp b/launcher/FileSystem.cpp index 7f38cff04..512de28c2 100644 --- a/launcher/FileSystem.cpp +++ b/launcher/FileSystem.cpp @@ -45,7 +45,6 @@ #include #include #include -#include #include #include #include @@ -54,6 +53,7 @@ #include #include "DesktopServices.h" +#include "PSaveFile.h" #include "StringUtils.h" #if defined Q_OS_WIN32 @@ -191,8 +191,8 @@ void ensureExists(const QDir& dir) void write(const QString& filename, const QByteArray& data) { ensureExists(QFileInfo(filename).dir()); - QSaveFile file(filename); - if (!file.open(QSaveFile::WriteOnly)) { + PSaveFile file(filename); + if (!file.open(PSaveFile::WriteOnly)) { throw FileSystemException("Couldn't open " + filename + " for writing: " + file.errorString()); } if (data.size() != file.write(data)) { @@ -213,8 +213,8 @@ void appendSafe(const QString& filename, const QByteArray& data) buffer = QByteArray(); } buffer.append(data); - QSaveFile file(filename); - if (!file.open(QSaveFile::WriteOnly)) { + PSaveFile file(filename); + if (!file.open(PSaveFile::WriteOnly)) { throw FileSystemException("Couldn't open " + filename + " for writing: " + file.errorString()); } if (buffer.size() != file.write(buffer)) { @@ -971,8 +971,7 @@ bool createShortcut(QString destination, QString target, QStringList args, QStri if (!args.empty()) argstring = " \"" + args.join("\" \"") + "\""; - stream << "#!/bin/bash" - << "\n"; + stream << "#!/bin/bash" << "\n"; stream << "\"" << target << "\" " << argstring << "\n"; stream.flush(); @@ -1016,12 +1015,9 @@ bool createShortcut(QString destination, QString target, QStringList args, QStri if (!args.empty()) argstring = " '" + args.join("' '") + "'"; - stream << "[Desktop Entry]" - << "\n"; - stream << "Type=Application" - << "\n"; - stream << "Categories=Game;ActionGame;AdventureGame;Simulation" - << "\n"; + stream << "[Desktop Entry]" << "\n"; + stream << "Type=Application" << "\n"; + stream << "Categories=Game;ActionGame;AdventureGame;Simulation" << "\n"; stream << "Exec=\"" << target.toLocal8Bit() << "\"" << argstring.toLocal8Bit() << "\n"; stream << "Name=" << name.toLocal8Bit() << "\n"; if (!icon.isEmpty()) { diff --git a/launcher/InstanceCreationTask.cpp b/launcher/InstanceCreationTask.cpp index 9c17dfc9f..3e7b3142f 100644 --- a/launcher/InstanceCreationTask.cpp +++ b/launcher/InstanceCreationTask.cpp @@ -38,22 +38,29 @@ void InstanceCreationTask::executeTask() // files scheduled to, and we'd better not let the user abort in the middle of it, since it'd // put the instance in an invalid state. if (shouldOverride()) { + bool deleteFailed = false; + setAbortable(false); setStatus(tr("Removing old conflicting files...")); qDebug() << "Removing old files"; - for (auto path : m_files_to_remove) { + for (const QString& path : m_files_to_remove) { if (!QFile::exists(path)) continue; + qDebug() << "Removing" << path; - if (!FS::deletePath(path)) { - qCritical() << "Couldn't remove the old conflicting files."; - emitFailed(tr("Failed to remove old conflicting files.")); - return; + + if (!QFile::remove(path)) { + qCritical() << "Could not remove" << path; + deleteFailed = true; } } + + if (deleteFailed) { + emitFailed(tr("Failed to remove old conflicting files.")); + return; + } } emitSucceeded(); - return; } diff --git a/launcher/Launcher.in b/launcher/Launcher.in index 1a23f2555..706d7022b 100755 --- a/launcher/Launcher.in +++ b/launcher/Launcher.in @@ -39,8 +39,16 @@ if [ "x$DEPS_LIST" = "x" ]; then # Just to be sure... chmod +x "${LAUNCHER_DIR}/bin/${LAUNCHER_NAME}" + ARGS=("${LAUNCHER_DIR}/${LAUNCHER_NAME}" "${LAUNCHER_DIR}/bin/${LAUNCHER_NAME}") + + if [ -f portable.txt ]; then + ARGS+=("-d" "${LAUNCHER_DIR}") + fi + + ARGS+=("$@") + # Run the launcher - exec -a "${LAUNCHER_DIR}/${LAUNCHER_NAME}" "${LAUNCHER_DIR}/bin/${LAUNCHER_NAME}" -d "${LAUNCHER_DIR}" "$@" + exec -a "${ARGS[@]}" # Run the launcher in valgrind # valgrind --log-file="valgrind.log" --leak-check=full --track-origins=yes "${LAUNCHER_DIR}/bin/${LAUNCHER_NAME}" -d "${LAUNCHER_DIR}" "$@" diff --git a/launcher/LoggedProcess.cpp b/launcher/LoggedProcess.cpp index fadd64e68..35ce4e0e5 100644 --- a/launcher/LoggedProcess.cpp +++ b/launcher/LoggedProcess.cpp @@ -39,7 +39,8 @@ #include #include "MessageLevel.h" -LoggedProcess::LoggedProcess(QObject* parent) : QProcess(parent) +LoggedProcess::LoggedProcess(const QTextCodec* output_codec, QObject* parent) + : QProcess(parent), m_err_decoder(output_codec), m_out_decoder(output_codec) { // QProcess has a strange interface... let's map a lot of those into a few. connect(this, &QProcess::readyReadStandardOutput, this, &LoggedProcess::on_stdOut); diff --git a/launcher/LoggedProcess.h b/launcher/LoggedProcess.h index 46bdaa830..75ba15dfd 100644 --- a/launcher/LoggedProcess.h +++ b/launcher/LoggedProcess.h @@ -49,7 +49,7 @@ class LoggedProcess : public QProcess { enum State { NotRunning, Starting, FailedToStart, Running, Finished, Crashed, Aborted }; public: - explicit LoggedProcess(QObject* parent = 0); + explicit LoggedProcess(const QTextCodec* output_codec = QTextCodec::codecForLocale(), QObject* parent = 0); virtual ~LoggedProcess(); State state() const; @@ -80,8 +80,8 @@ class LoggedProcess : public QProcess { QStringList reprocess(const QByteArray& data, QTextDecoder& decoder); private: - QTextDecoder m_err_decoder = QTextDecoder(QTextCodec::codecForLocale()); - QTextDecoder m_out_decoder = QTextDecoder(QTextCodec::codecForLocale()); + QTextDecoder m_err_decoder; + QTextDecoder m_out_decoder; QString m_leftover_line; bool m_killed = false; State m_state = NotRunning; diff --git a/launcher/MTPixmapCache.h b/launcher/MTPixmapCache.h index b6bd13045..0ba9c5ac8 100644 --- a/launcher/MTPixmapCache.h +++ b/launcher/MTPixmapCache.h @@ -101,7 +101,7 @@ class PixmapCache final : public QObject { */ bool _markCacheMissByEviciton() { - static constexpr uint maxInt = static_cast(std::numeric_limits::max()); + static constexpr uint maxCache = static_cast(std::numeric_limits::max()) / 4; static constexpr uint step = 10240; static constexpr int oneSecond = 1000; @@ -118,8 +118,8 @@ class PixmapCache final : public QObject { if (m_consecutive_fast_evicitons >= m_consecutive_fast_evicitons_threshold) { // increase the cache size uint newSize = _cacheLimit() + step; - if (newSize >= maxInt) { // increase it until you overflow :D - newSize = maxInt; + if (newSize >= maxCache) { // increase it until you overflow :D + newSize = maxCache; qDebug() << m_consecutive_fast_evicitons << tr("pixmap cache misses by eviction happened too fast, doing nothing as the cache size reached it's limit"); } else { diff --git a/launcher/PSaveFile.h b/launcher/PSaveFile.h new file mode 100644 index 000000000..ba6154ad8 --- /dev/null +++ b/launcher/PSaveFile.h @@ -0,0 +1,71 @@ +// 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 "Application.h" + +#if defined(LAUNCHER_APPLICATION) + +/* PSaveFile + * A class that mimics QSaveFile for Windows. + * + * When reading resources, we need to avoid accessing temporary files + * generated by QSaveFile. If we start reading such a file, we may + * inadvertently keep it open while QSaveFile is trying to remove it, + * or we might detect the file just before it is removed, leading to + * race conditions and errors. + * + * Unfortunately, QSaveFile doesn't provide a way to retrieve the + * temporary file name or to set a specific template for the temporary + * file name it uses. By default, QSaveFile appends a `.XXXXXX` suffix + * to the original file name, where the `XXXXXX` part is dynamically + * generated to ensure uniqueness. + * + * This class acts like a lock by adding and removing the target file + * name into/from a global string set, helping to manage access to + * files during critical operations. + * + * Note: Please do not use the `setFileName` function directly, as it + * is not virtual and cannot be overridden. + */ +class PSaveFile : public QSaveFile { + public: + PSaveFile(const QString& name) : QSaveFile(name) { addPath(name); } + PSaveFile(const QString& name, QObject* parent) : QSaveFile(name, parent) { addPath(name); } + virtual ~PSaveFile() + { + if (auto app = APPLICATION_DYN) { + app->removeQSavePath(m_absoluteFilePath); + } + } + + private: + void addPath(const QString& path) + { + m_absoluteFilePath = QFileInfo(path).absoluteFilePath() + "."; // add dot for tmp files only + if (auto app = APPLICATION_DYN) { + app->addQSavePath(m_absoluteFilePath); + } + } + QString m_absoluteFilePath; +}; +#else +#define PSaveFile QSaveFile +#endif \ No newline at end of file diff --git a/launcher/Version.cpp b/launcher/Version.cpp index 511aa9c35..2edb17e72 100644 --- a/launcher/Version.cpp +++ b/launcher/Version.cpp @@ -123,8 +123,7 @@ QDebug operator<<(QDebug debug, const Version& v) first = false; } - debug.nospace() << " ]" - << " }"; + debug.nospace() << " ]" << " }"; return debug; } diff --git a/launcher/java/JavaVersion.cpp b/launcher/java/JavaVersion.cpp index 5e9700012..bca50f2c9 100644 --- a/launcher/java/JavaVersion.cpp +++ b/launcher/java/JavaVersion.cpp @@ -48,6 +48,12 @@ bool JavaVersion::requiresPermGen() const return !m_parseable || m_major < 8; } +bool JavaVersion::defaultsToUtf8() const +{ + // starting from Java 18, UTF-8 is the default charset: https://openjdk.org/jeps/400 + return m_parseable && m_major >= 18; +} + bool JavaVersion::isModular() const { return m_parseable && m_major >= 9; diff --git a/launcher/java/JavaVersion.h b/launcher/java/JavaVersion.h index dfb4770da..c070bdeec 100644 --- a/launcher/java/JavaVersion.h +++ b/launcher/java/JavaVersion.h @@ -25,7 +25,7 @@ class JavaVersion { bool operator>(const JavaVersion& rhs); bool requiresPermGen() const; - + bool defaultsToUtf8() const; bool isModular() const; QString toString() const; diff --git a/launcher/launch/LaunchTask.cpp b/launcher/launch/LaunchTask.cpp index 4e4f5ead4..0251b302d 100644 --- a/launcher/launch/LaunchTask.cpp +++ b/launcher/launch/LaunchTask.cpp @@ -51,14 +51,14 @@ void LaunchTask::init() m_instance->setRunning(true); } -shared_qobject_ptr LaunchTask::create(InstancePtr inst) +shared_qobject_ptr LaunchTask::create(MinecraftInstancePtr inst) { shared_qobject_ptr proc(new LaunchTask(inst)); proc->init(); return proc; } -LaunchTask::LaunchTask(InstancePtr instance) : m_instance(instance) {} +LaunchTask::LaunchTask(MinecraftInstancePtr instance) : m_instance(instance) {} void LaunchTask::appendStep(shared_qobject_ptr step) { diff --git a/launcher/launch/LaunchTask.h b/launcher/launch/LaunchTask.h index 2fd8c78c7..56065af5b 100644 --- a/launcher/launch/LaunchTask.h +++ b/launcher/launch/LaunchTask.h @@ -37,6 +37,7 @@ #pragma once #include +#include #include #include "BaseInstance.h" #include "LaunchStep.h" @@ -46,21 +47,21 @@ class LaunchTask : public Task { Q_OBJECT protected: - explicit LaunchTask(InstancePtr instance); + explicit LaunchTask(MinecraftInstancePtr instance); void init(); public: enum State { NotStarted, Running, Waiting, Failed, Aborted, Finished }; public: /* methods */ - static shared_qobject_ptr create(InstancePtr inst); + static shared_qobject_ptr create(MinecraftInstancePtr inst); virtual ~LaunchTask() = default; void appendStep(shared_qobject_ptr step); void prependStep(shared_qobject_ptr step); void setCensorFilter(QMap filter); - InstancePtr instance() { return m_instance; } + MinecraftInstancePtr instance() { return m_instance; } void setPid(qint64 pid) { m_pid = pid; } @@ -115,7 +116,7 @@ class LaunchTask : public Task { void finalizeSteps(bool successful, const QString& error); protected: /* data */ - InstancePtr m_instance; + MinecraftInstancePtr m_instance; shared_qobject_ptr m_logModel; QList> m_steps; QMap m_censorFilter; diff --git a/launcher/minecraft/World.cpp b/launcher/minecraft/World.cpp index 1eba148a5..bd28f9e9a 100644 --- a/launcher/minecraft/World.cpp +++ b/launcher/minecraft/World.cpp @@ -38,7 +38,6 @@ #include #include #include -#include #include #include @@ -57,6 +56,7 @@ #include #include "FileSystem.h" +#include "PSaveFile.h" using std::nullopt; using std::optional; @@ -183,7 +183,7 @@ bool putLevelDatDataToFS(const QFileInfo& file, QByteArray& data) if (fullFilePath.isNull()) { return false; } - QSaveFile f(fullFilePath); + PSaveFile f(fullFilePath); if (!f.open(QIODevice::WriteOnly)) { return false; } diff --git a/launcher/minecraft/auth/Parsers.cpp b/launcher/minecraft/auth/Parsers.cpp index 3aa458ace..f9d89baa2 100644 --- a/launcher/minecraft/auth/Parsers.cpp +++ b/launcher/minecraft/auth/Parsers.cpp @@ -180,6 +180,7 @@ bool parseMinecraftProfile(QByteArray& data, MinecraftProfile& output) if (!getString(skinObj.value("url"), skinOut.url)) { continue; } + skinOut.url.replace("http://textures.minecraft.net", "https://textures.minecraft.net"); if (!getString(skinObj.value("variant"), skinOut.variant)) { continue; } @@ -221,9 +222,9 @@ namespace { // these skin URLs are for the MHF_Steve and MHF_Alex accounts (made by a Mojang employee) // they are needed because the session server doesn't return skin urls for default skins static const QString SKIN_URL_STEVE = - "http://textures.minecraft.net/texture/1a4af718455d4aab528e7a61f86fa25e6a369d1768dcb13f7df319a713eb810b"; + "https://textures.minecraft.net/texture/1a4af718455d4aab528e7a61f86fa25e6a369d1768dcb13f7df319a713eb810b"; static const QString SKIN_URL_ALEX = - "http://textures.minecraft.net/texture/83cee5ca6afcdb171285aa00e8049c297b2dbeba0efb8ff970a5677a1b644032"; + "https://textures.minecraft.net/texture/83cee5ca6afcdb171285aa00e8049c297b2dbeba0efb8ff970a5677a1b644032"; bool isDefaultModelSteve(QString uuid) { diff --git a/launcher/minecraft/launch/AutoInstallJava.cpp b/launcher/minecraft/launch/AutoInstallJava.cpp index 4fad6f15f..b23b23ed2 100644 --- a/launcher/minecraft/launch/AutoInstallJava.cpp +++ b/launcher/minecraft/launch/AutoInstallJava.cpp @@ -58,7 +58,7 @@ AutoInstallJava::AutoInstallJava(LaunchTask* parent) : LaunchStep(parent) - , m_instance(std::dynamic_pointer_cast(m_parent->instance())) + , m_instance(m_parent->instance()) , m_supported_arch(SysInfo::getSupportedJavaArchitecture()) {}; void AutoInstallJava::executeTask() diff --git a/launcher/minecraft/launch/CreateGameFolders.cpp b/launcher/minecraft/launch/CreateGameFolders.cpp index 36f5e6407..07bdbb600 100644 --- a/launcher/minecraft/launch/CreateGameFolders.cpp +++ b/launcher/minecraft/launch/CreateGameFolders.cpp @@ -8,16 +8,15 @@ CreateGameFolders::CreateGameFolders(LaunchTask* parent) : LaunchStep(parent) {} void CreateGameFolders::executeTask() { auto instance = m_parent->instance(); - std::shared_ptr minecraftInstance = std::dynamic_pointer_cast(instance); - if (!FS::ensureFolderPathExists(minecraftInstance->gameRoot())) { + if (!FS::ensureFolderPathExists(instance->gameRoot())) { emit logLine("Couldn't create the main game folder", MessageLevel::Error); emitFailed(tr("Couldn't create the main game folder")); return; } // HACK: this is a workaround for MCL-3732 - 'server-resource-packs' folder is created. - if (!FS::ensureFolderPathExists(FS::PathCombine(minecraftInstance->gameRoot(), "server-resource-packs"))) { + if (!FS::ensureFolderPathExists(FS::PathCombine(instance->gameRoot(), "server-resource-packs"))) { emit logLine("Couldn't create the 'server-resource-packs' folder", MessageLevel::Error); } emitSucceeded(); diff --git a/launcher/minecraft/launch/ExtractNatives.cpp b/launcher/minecraft/launch/ExtractNatives.cpp index 405008f40..afe091758 100644 --- a/launcher/minecraft/launch/ExtractNatives.cpp +++ b/launcher/minecraft/launch/ExtractNatives.cpp @@ -70,17 +70,16 @@ static bool unzipNatives(QString source, QString targetFolder, bool applyJnilibH void ExtractNatives::executeTask() { auto instance = m_parent->instance(); - std::shared_ptr minecraftInstance = std::dynamic_pointer_cast(instance); - auto toExtract = minecraftInstance->getNativeJars(); + auto toExtract = instance->getNativeJars(); if (toExtract.isEmpty()) { emitSucceeded(); return; } - auto settings = minecraftInstance->settings(); + auto settings = instance->settings(); - auto outputPath = minecraftInstance->getNativePath(); + auto outputPath = instance->getNativePath(); FS::ensureFolderPathExists(outputPath); - auto javaVersion = minecraftInstance->getJavaVersion(); + auto javaVersion = instance->getJavaVersion(); bool jniHackEnabled = javaVersion.major() >= 8; for (const auto& source : toExtract) { if (!unzipNatives(source, outputPath, jniHackEnabled)) { diff --git a/launcher/minecraft/launch/LauncherPartLaunch.cpp b/launcher/minecraft/launch/LauncherPartLaunch.cpp index 2b932ae47..d9a2b9b6b 100644 --- a/launcher/minecraft/launch/LauncherPartLaunch.cpp +++ b/launcher/minecraft/launch/LauncherPartLaunch.cpp @@ -48,18 +48,20 @@ #include "gamemode_client.h" #endif -LauncherPartLaunch::LauncherPartLaunch(LaunchTask* parent) : LaunchStep(parent) +LauncherPartLaunch::LauncherPartLaunch(LaunchTask* parent) + : LaunchStep(parent) + , m_process(parent->instance()->getJavaVersion().defaultsToUtf8() ? QTextCodec::codecForName("UTF-8") : QTextCodec::codecForLocale()) { - auto instance = parent->instance(); - if (instance->settings()->get("CloseAfterLaunch").toBool()) { + if (parent->instance()->settings()->get("CloseAfterLaunch").toBool()) { std::shared_ptr connection{ new QMetaObject::Connection }; - *connection = connect(&m_process, &LoggedProcess::log, this, [=](QStringList lines, [[maybe_unused]] MessageLevel::Enum level) { - qDebug() << lines; - if (lines.filter(QRegularExpression(".*Setting user.+", QRegularExpression::CaseInsensitiveOption)).length() != 0) { - APPLICATION->closeAllWindows(); - disconnect(*connection); - } - }); + *connection = + connect(&m_process, &LoggedProcess::log, this, [=](const QStringList& lines, [[maybe_unused]] MessageLevel::Enum level) { + qDebug() << lines; + if (lines.filter(QRegularExpression(".*Setting user.+", QRegularExpression::CaseInsensitiveOption)).length() != 0) { + APPLICATION->closeAllWindows(); + disconnect(*connection); + } + }); } connect(&m_process, &LoggedProcess::log, this, &LauncherPartLaunch::logLines); @@ -77,10 +79,9 @@ void LauncherPartLaunch::executeTask() } auto instance = m_parent->instance(); - std::shared_ptr minecraftInstance = std::dynamic_pointer_cast(instance); QString legacyJarPath; - if (minecraftInstance->getLauncher() == "legacy" || minecraftInstance->shouldApplyOnlineFixes()) { + if (instance->getLauncher() == "legacy" || instance->shouldApplyOnlineFixes()) { legacyJarPath = APPLICATION->getJarPath("NewLaunchLegacy.jar"); if (legacyJarPath.isEmpty()) { const char* reason = QT_TR_NOOP("Legacy launcher library could not be found. Please check your installation."); @@ -90,8 +91,8 @@ void LauncherPartLaunch::executeTask() } } - m_launchScript = minecraftInstance->createLaunchScript(m_session, m_targetToJoin); - QStringList args = minecraftInstance->javaArguments(); + m_launchScript = instance->createLaunchScript(m_session, m_targetToJoin); + QStringList args = instance->javaArguments(); QString allArgs = args.join(", "); emit logLine("Java Arguments:\n[" + m_parent->censorPrivateInfo(allArgs) + "]\n\n", MessageLevel::Launcher); @@ -102,13 +103,13 @@ void LauncherPartLaunch::executeTask() // make detachable - this will keep the process running even if the object is destroyed m_process.setDetachable(true); - auto classPath = minecraftInstance->getClassPath(); + auto classPath = instance->getClassPath(); classPath.prepend(jarPath); if (!legacyJarPath.isEmpty()) classPath.prepend(legacyJarPath); - auto natPath = minecraftInstance->getNativePath(); + auto natPath = instance->getNativePath(); #ifdef Q_OS_WIN natPath = FS::getPathNameInLocal8bit(natPath); #endif diff --git a/launcher/minecraft/launch/ModMinecraftJar.cpp b/launcher/minecraft/launch/ModMinecraftJar.cpp index 6e73333b1..e06080ba7 100644 --- a/launcher/minecraft/launch/ModMinecraftJar.cpp +++ b/launcher/minecraft/launch/ModMinecraftJar.cpp @@ -42,7 +42,7 @@ void ModMinecraftJar::executeTask() { - auto m_inst = std::dynamic_pointer_cast(m_parent->instance()); + auto m_inst = m_parent->instance(); if (!m_inst->getJarMods().size()) { emitSucceeded(); @@ -82,7 +82,7 @@ void ModMinecraftJar::finalize() bool ModMinecraftJar::removeJar() { - auto m_inst = std::dynamic_pointer_cast(m_parent->instance()); + auto m_inst = m_parent->instance(); auto finalJarPath = QDir(m_inst->binRoot()).absoluteFilePath("minecraft.jar"); QFile finalJar(finalJarPath); if (finalJar.exists()) { diff --git a/launcher/minecraft/launch/ReconstructAssets.cpp b/launcher/minecraft/launch/ReconstructAssets.cpp index 843ccc554..21ae395f0 100644 --- a/launcher/minecraft/launch/ReconstructAssets.cpp +++ b/launcher/minecraft/launch/ReconstructAssets.cpp @@ -22,12 +22,11 @@ void ReconstructAssets::executeTask() { auto instance = m_parent->instance(); - std::shared_ptr minecraftInstance = std::dynamic_pointer_cast(instance); - auto components = minecraftInstance->getPackProfile(); + auto components = instance->getPackProfile(); auto profile = components->getProfile(); auto assets = profile->getMinecraftAssets(); - if (!AssetsUtils::reconstructAssets(assets->id, minecraftInstance->resourcesDir())) { + if (!AssetsUtils::reconstructAssets(assets->id, instance->resourcesDir())) { emit logLine("Failed to reconstruct Minecraft assets.", MessageLevel::Error); } diff --git a/launcher/minecraft/launch/ScanModFolders.cpp b/launcher/minecraft/launch/ScanModFolders.cpp index 7e08a4e36..1a2ddf194 100644 --- a/launcher/minecraft/launch/ScanModFolders.cpp +++ b/launcher/minecraft/launch/ScanModFolders.cpp @@ -42,7 +42,7 @@ void ScanModFolders::executeTask() { - auto m_inst = std::dynamic_pointer_cast(m_parent->instance()); + auto m_inst = m_parent->instance(); auto loaders = m_inst->loaderModList(); connect(loaders.get(), &ModFolderModel::updateFinished, this, &ScanModFolders::modsDone); diff --git a/launcher/minecraft/launch/VerifyJavaInstall.cpp b/launcher/minecraft/launch/VerifyJavaInstall.cpp index 1e7448089..bc950d673 100644 --- a/launcher/minecraft/launch/VerifyJavaInstall.cpp +++ b/launcher/minecraft/launch/VerifyJavaInstall.cpp @@ -46,7 +46,7 @@ void VerifyJavaInstall::executeTask() { - auto instance = std::dynamic_pointer_cast(m_parent->instance()); + auto instance = m_parent->instance(); auto packProfile = instance->getPackProfile(); auto settings = instance->settings(); auto storedVersion = settings->get("JavaVersion").toString(); diff --git a/launcher/minecraft/mod/Mod.cpp b/launcher/minecraft/mod/Mod.cpp index 8a4c19dab..c5b13ddd3 100644 --- a/launcher/minecraft/mod/Mod.cpp +++ b/launcher/minecraft/mod/Mod.cpp @@ -35,6 +35,7 @@ */ #include "Mod.h" +#include #include #include @@ -210,7 +211,7 @@ void Mod::finishResolvingWithDetails(ModDetails&& details) m_local_details = std::move(details); if (!iconPath().isEmpty()) { - m_pack_image_cache_key.was_read_attempt = false; + m_packImageCacheKey.wasReadAttempt = false; } } @@ -224,45 +225,53 @@ auto Mod::issueTracker() const -> QString return details().issue_tracker; } -void Mod::setIcon(QImage new_image) const +QPixmap Mod::setIcon(QImage new_image) const { QMutexLocker locker(&m_data_lock); Q_ASSERT(!new_image.isNull()); - if (m_pack_image_cache_key.key.isValid()) - PixmapCache::remove(m_pack_image_cache_key.key); + if (m_packImageCacheKey.key.isValid()) + PixmapCache::remove(m_packImageCacheKey.key); // scale the image to avoid flooding the pixmapcache auto pixmap = QPixmap::fromImage(new_image.scaled({ 64, 64 }, Qt::AspectRatioMode::KeepAspectRatioByExpanding, Qt::SmoothTransformation)); - m_pack_image_cache_key.key = PixmapCache::insert(pixmap); - m_pack_image_cache_key.was_ever_used = true; - m_pack_image_cache_key.was_read_attempt = true; + m_packImageCacheKey.key = PixmapCache::insert(pixmap); + m_packImageCacheKey.wasEverUsed = true; + m_packImageCacheKey.wasReadAttempt = true; + return pixmap; } QPixmap Mod::icon(QSize size, Qt::AspectRatioMode mode) const { - QPixmap cached_image; - if (PixmapCache::find(m_pack_image_cache_key.key, &cached_image)) { + auto pixmap_transform = [&size, &mode](QPixmap pixmap) { if (size.isNull()) - return cached_image; - return cached_image.scaled(size, mode, Qt::SmoothTransformation); + return pixmap; + return pixmap.scaled(size, mode, Qt::SmoothTransformation); + }; + + QPixmap cached_image; + if (PixmapCache::find(m_packImageCacheKey.key, &cached_image)) { + return pixmap_transform(cached_image); } // No valid image we can get - if ((!m_pack_image_cache_key.was_ever_used && m_pack_image_cache_key.was_read_attempt) || iconPath().isEmpty()) + if ((!m_packImageCacheKey.wasEverUsed && m_packImageCacheKey.wasReadAttempt) || iconPath().isEmpty()) return {}; - if (m_pack_image_cache_key.was_ever_used) { + if (m_packImageCacheKey.wasEverUsed) { qDebug() << "Mod" << name() << "Had it's icon evicted from the cache. reloading..."; PixmapCache::markCacheMissByEviciton(); } // Image got evicted from the cache or an attempt to load it has not been made. load it and retry. - m_pack_image_cache_key.was_read_attempt = true; - ModUtils::loadIconFile(*this); - return icon(size); + m_packImageCacheKey.wasReadAttempt = true; + if (ModUtils::loadIconFile(*this, &cached_image)) { + return pixmap_transform(cached_image); + } + // Image failed to load + return {}; } bool Mod::valid() const diff --git a/launcher/minecraft/mod/Mod.h b/launcher/minecraft/mod/Mod.h index 01fa9c99f..a86a2fad0 100644 --- a/launcher/minecraft/mod/Mod.h +++ b/launcher/minecraft/mod/Mod.h @@ -77,7 +77,7 @@ class Mod : public Resource { /** Gets the icon of the mod, converted to a QPixmap for drawing, and scaled to size. */ [[nodiscard]] QPixmap icon(QSize size, Qt::AspectRatioMode mode = Qt::AspectRatioMode::IgnoreAspectRatio) const; /** Thread-safe. */ - void setIcon(QImage new_image) const; + QPixmap setIcon(QImage new_image) const; void setDetails(const ModDetails& details); @@ -100,7 +100,7 @@ class Mod : public Resource { struct { QPixmapCache::Key key; - bool was_ever_used = false; - bool was_read_attempt = false; - } mutable m_pack_image_cache_key; + bool wasEverUsed = false; + bool wasReadAttempt = false; + } mutable m_packImageCacheKey; }; diff --git a/launcher/minecraft/mod/ResourceFolderModel.cpp b/launcher/minecraft/mod/ResourceFolderModel.cpp index 12f13ec12..4c7af3c02 100644 --- a/launcher/minecraft/mod/ResourceFolderModel.cpp +++ b/launcher/minecraft/mod/ResourceFolderModel.cpp @@ -40,10 +40,9 @@ ResourceFolderModel::ResourceFolderModel(const QDir& dir, BaseInstance* instance connect(&m_watcher, &QFileSystemWatcher::directoryChanged, this, &ResourceFolderModel::directoryChanged); connect(&m_helper_thread_task, &ConcurrentTask::finished, this, [this] { m_helper_thread_task.clear(); }); -#ifndef LAUNCHER_TEST - // in tests the application macro doesn't work - m_helper_thread_task.setMaxConcurrent(APPLICATION->settings()->get("NumberOfConcurrentTasks").toInt()); -#endif + if (APPLICATION_DYN) { // in tests the application macro doesn't work + m_helper_thread_task.setMaxConcurrent(APPLICATION->settings()->get("NumberOfConcurrentTasks").toInt()); + } } ResourceFolderModel::~ResourceFolderModel() diff --git a/launcher/minecraft/mod/tasks/BasicFolderLoadTask.h b/launcher/minecraft/mod/tasks/BasicFolderLoadTask.h new file mode 100644 index 000000000..fb2e22de6 --- /dev/null +++ b/launcher/minecraft/mod/tasks/BasicFolderLoadTask.h @@ -0,0 +1,85 @@ +#pragma once + +#include +#include +#include +#include + +#include + +#include "Application.h" +#include "FileSystem.h" +#include "minecraft/mod/Resource.h" + +#include "tasks/Task.h" + +/** Very simple task that just loads a folder's contents directly. + */ +class BasicFolderLoadTask : public Task { + Q_OBJECT + public: + struct Result { + QMap resources; + }; + using ResultPtr = std::shared_ptr; + + [[nodiscard]] ResultPtr result() const { return m_result; } + + public: + BasicFolderLoadTask(QDir dir) : Task(nullptr, false), m_dir(dir), m_result(new Result), m_thread_to_spawn_into(thread()) + { + m_create_func = [](QFileInfo const& entry) -> Resource::Ptr { return makeShared(entry); }; + } + BasicFolderLoadTask(QDir dir, std::function create_function) + : Task(nullptr, false) + , m_dir(dir) + , m_result(new Result) + , m_create_func(std::move(create_function)) + , m_thread_to_spawn_into(thread()) + {} + + [[nodiscard]] bool canAbort() const override { return true; } + bool abort() override + { + m_aborted.store(true); + return true; + } + + void executeTask() override + { + if (thread() != m_thread_to_spawn_into) + connect(this, &Task::finished, this->thread(), &QThread::quit); + + m_dir.refresh(); + for (auto entry : m_dir.entryInfoList()) { + auto filePath = entry.absoluteFilePath(); + if (auto app = APPLICATION_DYN; app && app->checkQSavePath(filePath)) { + continue; + } + auto newFilePath = FS::getUniqueResourceName(filePath); + if (newFilePath != filePath) { + FS::move(filePath, newFilePath); + entry = QFileInfo(newFilePath); + } + auto resource = m_create_func(entry); + resource->moveToThread(m_thread_to_spawn_into); + m_result->resources.insert(resource->internal_id(), resource); + } + + if (m_aborted) + emit finished(); + else + emitSucceeded(); + } + + private: + QDir m_dir; + ResultPtr m_result; + + std::atomic m_aborted = false; + + std::function m_create_func; + + /** This is the thread in which we should put new mod objects */ + QThread* m_thread_to_spawn_into; +}; diff --git a/launcher/minecraft/mod/tasks/LocalModParseTask.cpp b/launcher/minecraft/mod/tasks/LocalModParseTask.cpp index 60257ce0c..d456211f8 100644 --- a/launcher/minecraft/mod/tasks/LocalModParseTask.cpp +++ b/launcher/minecraft/mod/tasks/LocalModParseTask.cpp @@ -647,11 +647,11 @@ bool validate(QFileInfo file) return ModUtils::process(mod, ProcessingLevel::BasicInfoOnly) && mod.valid(); } -bool processIconPNG(const Mod& mod, QByteArray&& raw_data) +bool processIconPNG(const Mod& mod, QByteArray&& raw_data, QPixmap* pixmap) { auto img = QImage::fromData(raw_data); if (!img.isNull()) { - mod.setIcon(img); + *pixmap = mod.setIcon(img); } else { qWarning() << "Failed to parse mod logo:" << mod.iconPath() << "from" << mod.name(); return false; @@ -659,15 +659,15 @@ bool processIconPNG(const Mod& mod, QByteArray&& raw_data) return true; } -bool loadIconFile(const Mod& mod) +bool loadIconFile(const Mod& mod, QPixmap* pixmap) { if (mod.iconPath().isEmpty()) { qWarning() << "No Iconfile set, be sure to parse the mod first"; return false; } - auto png_invalid = [&mod]() { - qWarning() << "Mod at" << mod.fileinfo().filePath() << "does not have a valid icon"; + auto png_invalid = [&mod](const QString& reason) { + qWarning() << "Mod at" << mod.fileinfo().filePath() << "does not have a valid icon:" << reason; return false; }; @@ -676,24 +676,26 @@ bool loadIconFile(const Mod& mod) QFileInfo icon_info(FS::PathCombine(mod.fileinfo().filePath(), mod.iconPath())); if (icon_info.exists() && icon_info.isFile()) { QFile icon(icon_info.filePath()); - if (!icon.open(QIODevice::ReadOnly)) - return false; + if (!icon.open(QIODevice::ReadOnly)) { + return png_invalid("failed to open file " + icon_info.filePath()); + } auto data = icon.readAll(); - bool icon_result = ModUtils::processIconPNG(mod, std::move(data)); + bool icon_result = ModUtils::processIconPNG(mod, std::move(data), pixmap); icon.close(); if (!icon_result) { - return png_invalid(); // icon invalid + return png_invalid("invalid png image"); // icon invalid } + return true; } - return false; + return png_invalid("file '" + icon_info.filePath() + "' does not exists or is not a file"); } case ResourceType::ZIPFILE: { QuaZip zip(mod.fileinfo().filePath()); if (!zip.open(QuaZip::mdUnzip)) - return false; + return png_invalid("failed to open '" + mod.fileinfo().filePath() + "' as a zip archive"); QuaZipFile file(&zip); @@ -701,28 +703,27 @@ bool loadIconFile(const Mod& mod) if (!file.open(QIODevice::ReadOnly)) { qCritical() << "Failed to open file in zip."; zip.close(); - return png_invalid(); + return png_invalid("Failed to open '" + mod.iconPath() + "' in zip archive"); } auto data = file.readAll(); - bool icon_result = ModUtils::processIconPNG(mod, std::move(data)); + bool icon_result = ModUtils::processIconPNG(mod, std::move(data), pixmap); file.close(); if (!icon_result) { - return png_invalid(); // icon png invalid + return png_invalid("invalid png image"); // icon png invalid } - } else { - return png_invalid(); // could not set icon as current file. + return true; } - return false; + return png_invalid("Failed to set '" + mod.iconPath() + + "' as current file in zip archive"); // could not set icon as current file. } case ResourceType::LITEMOD: { - return false; // can lightmods even have icons? + return png_invalid("litemods do not have icons"); // can lightmods even have icons? } default: - qWarning() << "Invalid type for mod, can not load icon."; - return false; + return png_invalid("Invalid type for mod, can not load icon."); } } diff --git a/launcher/minecraft/mod/tasks/LocalModParseTask.h b/launcher/minecraft/mod/tasks/LocalModParseTask.h index ff1ff822e..7ce5a84d2 100644 --- a/launcher/minecraft/mod/tasks/LocalModParseTask.h +++ b/launcher/minecraft/mod/tasks/LocalModParseTask.h @@ -26,8 +26,8 @@ bool processLitemod(Mod& mod, ProcessingLevel level = ProcessingLevel::Full); /** Checks whether a file is valid as a mod or not. */ bool validate(QFileInfo file); -bool processIconPNG(const Mod& mod, QByteArray&& raw_data); -bool loadIconFile(const Mod& mod); +bool processIconPNG(const Mod& mod, QByteArray&& raw_data, QPixmap* pixmap); +bool loadIconFile(const Mod& mod, QPixmap* pixmap); } // namespace ModUtils class LocalModParseTask : public Task { diff --git a/launcher/minecraft/mod/tasks/ResourceFolderLoadTask.cpp b/launcher/minecraft/mod/tasks/ResourceFolderLoadTask.cpp index 9aa931d1c..1cdde7541 100644 --- a/launcher/minecraft/mod/tasks/ResourceFolderLoadTask.cpp +++ b/launcher/minecraft/mod/tasks/ResourceFolderLoadTask.cpp @@ -36,6 +36,7 @@ #include "ResourceFolderLoadTask.h" +#include "Application.h" #include "FileSystem.h" #include "minecraft/mod/MetadataHandler.h" @@ -70,6 +71,9 @@ void ResourceFolderLoadTask::executeTask() m_resource_dir.refresh(); for (auto entry : m_resource_dir.entryInfoList()) { auto filePath = entry.absoluteFilePath(); + if (auto app = APPLICATION_DYN; app && app->checkQSavePath(filePath)) { + continue; + } auto newFilePath = FS::getUniqueResourceName(filePath); if (newFilePath != filePath) { FS::move(filePath, newFilePath); diff --git a/launcher/modplatform/ModIndex.h b/launcher/modplatform/ModIndex.h index b3f86933f..11a230798 100644 --- a/launcher/modplatform/ModIndex.h +++ b/launcher/modplatform/ModIndex.h @@ -36,7 +36,7 @@ Q_DECLARE_FLAGS(ModLoaderTypes, ModLoaderType) enum class ResourceProvider { MODRINTH, FLAME }; -enum class ResourceType { MOD, RESOURCE_PACK, SHADER_PACK }; +enum class ResourceType { MOD, RESOURCE_PACK, SHADER_PACK, MODPACK }; enum class DependencyType { REQUIRED, OPTIONAL, INCOMPATIBLE, EMBEDDED, TOOL, INCLUDE, UNKNOWN }; diff --git a/launcher/modplatform/flame/FlameAPI.cpp b/launcher/modplatform/flame/FlameAPI.cpp index 72437976d..ddd48c2b1 100644 --- a/launcher/modplatform/flame/FlameAPI.cpp +++ b/launcher/modplatform/flame/FlameAPI.cpp @@ -221,14 +221,20 @@ QList FlameAPI::getSortingMethods() const { 8, "GameVersion", QObject::tr("Sort by Game Version") } }; } -Task::Ptr FlameAPI::getModCategories(std::shared_ptr response) +Task::Ptr FlameAPI::getCategories(std::shared_ptr response, ModPlatform::ResourceType type) { auto netJob = makeShared(QString("Flame::GetCategories"), APPLICATION->network()); - netJob->addNetAction(Net::ApiDownload::makeByteArray(QUrl("https://api.curseforge.com/v1/categories?gameId=432&classId=6"), response)); + netJob->addNetAction(Net::ApiDownload::makeByteArray( + QUrl(QString("https://api.curseforge.com/v1/categories?gameId=432&classId=%1").arg(getClassId(type))), response)); QObject::connect(netJob.get(), &Task::failed, [](QString msg) { qDebug() << "Flame failed to get categories:" << msg; }); return netJob; } +Task::Ptr FlameAPI::getModCategories(std::shared_ptr response) +{ + return getCategories(response, ModPlatform::ResourceType::MOD); +} + QList FlameAPI::loadModCategories(std::shared_ptr response) { QList categories; diff --git a/launcher/modplatform/flame/FlameAPI.h b/launcher/modplatform/flame/FlameAPI.h index 1160151c5..3ca0d5448 100644 --- a/launcher/modplatform/flame/FlameAPI.h +++ b/launcher/modplatform/flame/FlameAPI.h @@ -25,6 +25,7 @@ class FlameAPI : public NetworkResourceAPI { Task::Ptr getFiles(const QStringList& fileIds, std::shared_ptr response) const; Task::Ptr getFile(const QString& addonId, const QString& fileId, std::shared_ptr response) const; + static Task::Ptr getCategories(std::shared_ptr response, ModPlatform::ResourceType type); static Task::Ptr getModCategories(std::shared_ptr response); static QList loadModCategories(std::shared_ptr response); @@ -46,6 +47,8 @@ class FlameAPI : public NetworkResourceAPI { return 12; case ModPlatform::ResourceType::SHADER_PACK: return 6552; + case ModPlatform::ResourceType::MODPACK: + return 4471; } } @@ -82,12 +85,9 @@ class FlameAPI : public NetworkResourceAPI { static const QString getModLoaderFilters(ModPlatform::ModLoaderTypes types) { return "[" + getModLoaderStrings(types).join(',') + "]"; } - private: + public: [[nodiscard]] std::optional getSearchURL(SearchArgs const& args) const override { - auto gameVersionStr = - args.versions.has_value() ? QString("gameVersion=%1").arg(args.versions.value().front().toString()) : QString(); - QStringList get_arguments; get_arguments.append(QString("classId=%1").arg(getClassId(args.type))); get_arguments.append(QString("index=%1").arg(args.offset)); @@ -97,20 +97,22 @@ class FlameAPI : public NetworkResourceAPI { if (args.sorting.has_value()) get_arguments.append(QString("sortField=%1").arg(args.sorting.value().index)); get_arguments.append("sortOrder=desc"); - if (args.loaders.has_value()) + if (args.loaders.has_value() && args.loaders.value() != 0) get_arguments.append(QString("modLoaderTypes=%1").arg(getModLoaderFilters(args.loaders.value()))); if (args.categoryIds.has_value() && !args.categoryIds->empty()) get_arguments.append(QString("categoryIds=[%1]").arg(args.categoryIds->join(","))); - get_arguments.append(gameVersionStr); + if (args.versions.has_value() && !args.versions.value().empty()) + get_arguments.append(QString("gameVersion=%1").arg(args.versions.value().front().toString())); return "https://api.curseforge.com/v1/mods/search?gameId=432&" + get_arguments.join('&'); - }; + } + private: [[nodiscard]] std::optional getInfoURL(QString const& id) const override { return QString("https://api.curseforge.com/v1/mods/%1").arg(id); - }; + } [[nodiscard]] std::optional getVersionsURL(VersionSearchArgs const& args) const override { @@ -125,7 +127,7 @@ class FlameAPI : public NetworkResourceAPI { url += QString("&modLoaderType=%1").arg(mappedModLoader); } return url; - }; + } [[nodiscard]] std::optional getDependencyURL(DependencySearchArgs const& args) const override { @@ -137,5 +139,5 @@ class FlameAPI : public NetworkResourceAPI { url += QString("&modLoaderType=%1").arg(mappedModLoader); } return url; - }; + } }; diff --git a/launcher/modplatform/flame/FlamePackIndex.cpp b/launcher/modplatform/flame/FlamePackIndex.cpp index ca8e0a853..8c25b0482 100644 --- a/launcher/modplatform/flame/FlamePackIndex.cpp +++ b/launcher/modplatform/flame/FlamePackIndex.cpp @@ -3,6 +3,7 @@ #include #include "Json.h" +#include "modplatform/ModIndex.h" void Flame::loadIndexedPack(Flame::IndexedPack& pack, QJsonObject& obj) { @@ -88,8 +89,27 @@ void Flame::loadIndexedPackVersions(Flame::IndexedPack& pack, QJsonArray& arr) continue; } + for (auto mcVer : versionArray) { + auto str = mcVer.toString(); + + if (str.contains('.')) + file.mcVersion.append(str); + + if (auto loader = str.toLower(); loader == "neoforge") + file.loaders |= ModPlatform::NeoForge; + else if (loader == "forge") + file.loaders |= ModPlatform::Forge; + else if (loader == "cauldron") + file.loaders |= ModPlatform::Cauldron; + else if (loader == "liteloader") + file.loaders |= ModPlatform::LiteLoader; + else if (loader == "fabric") + file.loaders |= ModPlatform::Fabric; + else if (loader == "quilt") + file.loaders |= ModPlatform::Quilt; + } + // pick the latest version supported - file.mcVersion = versionArray[0].toString(); file.version = Json::requireString(version, "displayName"); ModPlatform::IndexedVersionType::VersionType ver_type; diff --git a/launcher/modplatform/flame/FlamePackIndex.h b/launcher/modplatform/flame/FlamePackIndex.h index b2a12a67f..11633deee 100644 --- a/launcher/modplatform/flame/FlamePackIndex.h +++ b/launcher/modplatform/flame/FlamePackIndex.h @@ -18,6 +18,7 @@ struct IndexedVersion { int fileId; QString version; ModPlatform::IndexedVersionType version_type; + ModPlatform::ModLoaderTypes loaders = {}; QString mcVersion; QString downloadUrl; }; diff --git a/launcher/modplatform/modrinth/ModrinthAPI.cpp b/launcher/modplatform/modrinth/ModrinthAPI.cpp index 4798ace84..323711e02 100644 --- a/launcher/modplatform/modrinth/ModrinthAPI.cpp +++ b/launcher/modplatform/modrinth/ModrinthAPI.cpp @@ -129,7 +129,7 @@ Task::Ptr ModrinthAPI::getModCategories(std::shared_ptr response) return netJob; } -QList ModrinthAPI::loadModCategories(std::shared_ptr response) +QList ModrinthAPI::loadCategories(std::shared_ptr response, QString projectType) { QList categories; QJsonParseError parse_error{}; @@ -147,7 +147,7 @@ QList ModrinthAPI::loadModCategories(std::shared_ptr ModrinthAPI::loadModCategories(std::shared_ptr ModrinthAPI::loadModCategories(std::shared_ptr response) +{ + return loadCategories(response, "mod"); +}; diff --git a/launcher/modplatform/modrinth/ModrinthAPI.h b/launcher/modplatform/modrinth/ModrinthAPI.h index d1f8f712a..070f59dad 100644 --- a/launcher/modplatform/modrinth/ModrinthAPI.h +++ b/launcher/modplatform/modrinth/ModrinthAPI.h @@ -31,6 +31,7 @@ class ModrinthAPI : public NetworkResourceAPI { Task::Ptr getProjects(QStringList addonIds, std::shared_ptr response) const override; static Task::Ptr getModCategories(std::shared_ptr response); + static QList loadCategories(std::shared_ptr response, QString projectType); static QList loadModCategories(std::shared_ptr response); public: @@ -90,6 +91,8 @@ class ModrinthAPI : public NetworkResourceAPI { return "resourcepack"; case ModPlatform::ResourceType::SHADER_PACK: return "shader"; + case ModPlatform::ResourceType::MODPACK: + return "modpack"; default: qWarning() << "Invalid resource type for Modrinth API!"; break; @@ -102,9 +105,9 @@ class ModrinthAPI : public NetworkResourceAPI { { QStringList facets_list; - if (args.loaders.has_value()) + if (args.loaders.has_value() && args.loaders.value() != 0) facets_list.append(QString("[%1]").arg(getModLoaderFilters(args.loaders.value()))); - if (args.versions.has_value()) + if (args.versions.has_value() && !args.versions.value().empty()) facets_list.append(QString("[%1]").arg(getGameVersionsArray(args.versions.value()))); if (args.side.has_value()) { auto side = getSideFilters(args.side.value()); @@ -122,7 +125,7 @@ class ModrinthAPI : public NetworkResourceAPI { public: [[nodiscard]] inline auto getSearchURL(SearchArgs const& args) const -> std::optional override { - if (args.loaders.has_value()) { + if (args.loaders.has_value() && args.loaders.value() != 0) { if (!validateModLoaders(args.loaders.value())) { qWarning() << "Modrinth - or our interface - does not support any the provided mod loaders!"; return {}; @@ -163,7 +166,7 @@ class ModrinthAPI : public NetworkResourceAPI { .arg(BuildConfig.MODRINTH_PROD_URL, args.pack.addonId.toString(), get_arguments.isEmpty() ? "" : "?", get_arguments.join('&')); }; - auto getGameVersionsArray(std::list mcVersions) const -> QString + QString getGameVersionsArray(std::list mcVersions) const { QString s; for (auto& ver : mcVersions) { diff --git a/launcher/modplatform/modrinth/ModrinthInstanceCreationTask.cpp b/launcher/modplatform/modrinth/ModrinthInstanceCreationTask.cpp index f4e8dbb5a..f34fccfc5 100644 --- a/launcher/modplatform/modrinth/ModrinthInstanceCreationTask.cpp +++ b/launcher/modplatform/modrinth/ModrinthInstanceCreationTask.cpp @@ -136,7 +136,7 @@ bool ModrinthCreationTask::updateInstance() } auto old_client_overrides = Override::readOverrides("client-overrides", old_index_folder); - for (const auto& entry : old_overrides) { + for (const auto& entry : old_client_overrides) { if (entry.isEmpty()) continue; qDebug() << "Scheduling" << entry << "for removal"; @@ -302,6 +302,13 @@ bool ModrinthCreationTask::createInstance() loop.exec(); + if (!ended_well) { + for (auto m : mods) { + delete m; + } + return ended_well; + } + QEventLoop ensureMetaLoop; QDir folder = FS::PathCombine(instance.modsRoot(), ".index"); auto ensureMetadataTask = makeShared(resources, folder, ModPlatform::ResourceProvider::MODRINTH); diff --git a/launcher/modplatform/modrinth/ModrinthPackManifest.cpp b/launcher/modplatform/modrinth/ModrinthPackManifest.cpp index f360df43a..c52a1743b 100644 --- a/launcher/modplatform/modrinth/ModrinthPackManifest.cpp +++ b/launcher/modplatform/modrinth/ModrinthPackManifest.cpp @@ -135,6 +135,21 @@ auto loadIndexedVersion(QJsonObject& obj) -> ModpackVersion if (!gameVersions.isEmpty()) { file.gameVersion = Json::ensureString(gameVersions[0]); } + auto loaders = Json::requireArray(obj, "loaders"); + for (auto loader : loaders) { + if (loader == "neoforge") + file.loaders |= ModPlatform::NeoForge; + else if (loader == "forge") + file.loaders |= ModPlatform::Forge; + else if (loader == "cauldron") + file.loaders |= ModPlatform::Cauldron; + else if (loader == "liteloader") + file.loaders |= ModPlatform::LiteLoader; + else if (loader == "fabric") + file.loaders |= ModPlatform::Fabric; + else if (loader == "quilt") + file.loaders |= ModPlatform::Quilt; + } file.version_type = ModPlatform::IndexedVersionType(Json::requireString(obj, "version_type")); file.changelog = Json::ensureString(obj, "changelog"); diff --git a/launcher/modplatform/modrinth/ModrinthPackManifest.h b/launcher/modplatform/modrinth/ModrinthPackManifest.h index 2bd61c5d9..2e5e2da84 100644 --- a/launcher/modplatform/modrinth/ModrinthPackManifest.h +++ b/launcher/modplatform/modrinth/ModrinthPackManifest.h @@ -87,6 +87,7 @@ struct ModpackVersion { QString gameVersion; ModPlatform::IndexedVersionType version_type; QString changelog; + ModPlatform::ModLoaderTypes loaders = {}; QString id; QString project_id; diff --git a/launcher/modplatform/packwiz/Packwiz.cpp b/launcher/modplatform/packwiz/Packwiz.cpp index fe5ef34e7..bbccb72b8 100644 --- a/launcher/modplatform/packwiz/Packwiz.cpp +++ b/launcher/modplatform/packwiz/Packwiz.cpp @@ -72,7 +72,7 @@ auto stringEntry(toml::table table, QString entry_name) -> QString { auto node = table[StringUtils::toStdString(entry_name)]; if (!node) { - qCritical() << "Failed to read str property '" + entry_name + "' in mod metadata."; + qWarning() << "Failed to read str property '" + entry_name + "' in mod metadata."; return {}; } @@ -83,7 +83,7 @@ auto intEntry(toml::table table, QString entry_name) -> int { auto node = table[StringUtils::toStdString(entry_name)]; if (!node) { - qCritical() << "Failed to read int property '" + entry_name + "' in mod metadata."; + qWarning() << "Failed to read int property '" + entry_name + "' in mod metadata."; return {}; } diff --git a/launcher/net/FileSink.cpp b/launcher/net/FileSink.cpp index 1ecb21fdf..95c1a8f44 100644 --- a/launcher/net/FileSink.cpp +++ b/launcher/net/FileSink.cpp @@ -55,7 +55,7 @@ Task::State FileSink::init(QNetworkRequest& request) } wroteAnyData = false; - m_output_file.reset(new QSaveFile(m_filename)); + m_output_file.reset(new PSaveFile(m_filename)); if (!m_output_file->open(QIODevice::WriteOnly)) { qCCritical(taskNetLogC) << "Could not open " + m_filename + " for writing"; return Task::State::Failed; diff --git a/launcher/net/FileSink.h b/launcher/net/FileSink.h index 816254ff9..272f8ddc3 100644 --- a/launcher/net/FileSink.h +++ b/launcher/net/FileSink.h @@ -35,8 +35,7 @@ #pragma once -#include - +#include "PSaveFile.h" #include "Sink.h" namespace Net { @@ -60,6 +59,6 @@ class FileSink : public Sink { protected: QString m_filename; bool wroteAnyData = false; - std::unique_ptr m_output_file; + std::unique_ptr m_output_file; }; } // namespace Net diff --git a/launcher/net/NetJob.cpp b/launcher/net/NetJob.cpp index e363c911d..3cd3958f7 100644 --- a/launcher/net/NetJob.cpp +++ b/launcher/net/NetJob.cpp @@ -48,7 +48,7 @@ NetJob::NetJob(QString job_name, shared_qobject_ptr netwo : ConcurrentTask(nullptr, job_name), m_network(network) { #if defined(LAUNCHER_APPLICATION) - if (max_concurrent < 0) + if (APPLICATION_DYN && max_concurrent < 0) max_concurrent = APPLICATION->settings()->get("NumberOfConcurrentDownloads").toInt(); #endif if (max_concurrent > 0) @@ -161,7 +161,8 @@ bool NetJob::isOnline() void NetJob::emitFailed(QString reason) { #if defined(LAUNCHER_APPLICATION) - if (m_ask_retry && m_manual_try < APPLICATION->settings()->get("NumberOfManualRetries").toInt() && isOnline()) { + + if (APPLICATION_DYN && m_ask_retry && m_manual_try < APPLICATION->settings()->get("NumberOfManualRetries").toInt() && isOnline()) { m_manual_try++; auto response = CustomMessageBox::selectable(nullptr, "Confirm retry", "The tasks failed.\n" diff --git a/launcher/settings/INIFile.cpp b/launcher/settings/INIFile.cpp index e97741f20..2c7620e65 100644 --- a/launcher/settings/INIFile.cpp +++ b/launcher/settings/INIFile.cpp @@ -39,7 +39,6 @@ #include #include -#include #include #include #include diff --git a/launcher/ui/dialogs/ExportInstanceDialog.cpp b/launcher/ui/dialogs/ExportInstanceDialog.cpp index 9f2b3ac42..7e00f18f4 100644 --- a/launcher/ui/dialogs/ExportInstanceDialog.cpp +++ b/launcher/ui/dialogs/ExportInstanceDialog.cpp @@ -51,7 +51,6 @@ #include #include #include -#include #include #include #include diff --git a/launcher/ui/dialogs/MSALoginDialog.cpp b/launcher/ui/dialogs/MSALoginDialog.cpp index 799b5b332..4e38b4187 100644 --- a/launcher/ui/dialogs/MSALoginDialog.cpp +++ b/launcher/ui/dialogs/MSALoginDialog.cpp @@ -78,7 +78,7 @@ int MSALoginDialog::exec() connect(m_authflow_task.get(), &Task::failed, this, &MSALoginDialog::onTaskFailed); connect(m_authflow_task.get(), &Task::succeeded, this, &QDialog::accept); connect(m_authflow_task.get(), &Task::aborted, this, &MSALoginDialog::reject); - connect(m_authflow_task.get(), &Task::status, this, &MSALoginDialog::onTaskStatus); + connect(m_authflow_task.get(), &Task::status, this, &MSALoginDialog::onAuthFlowStatus); connect(m_authflow_task.get(), &AuthFlow::authorizeWithBrowser, this, &MSALoginDialog::authorizeWithBrowser); connect(m_authflow_task.get(), &AuthFlow::authorizeWithBrowserWithExtra, this, &MSALoginDialog::authorizeWithBrowserWithExtra); connect(ui->buttonBox->button(QDialogButtonBox::Cancel), &QPushButton::clicked, m_authflow_task.get(), &Task::abort); @@ -87,7 +87,7 @@ int MSALoginDialog::exec() connect(m_devicecode_task.get(), &Task::failed, this, &MSALoginDialog::onTaskFailed); connect(m_devicecode_task.get(), &Task::succeeded, this, &QDialog::accept); connect(m_devicecode_task.get(), &Task::aborted, this, &MSALoginDialog::reject); - connect(m_devicecode_task.get(), &Task::status, this, &MSALoginDialog::onTaskStatus); + connect(m_devicecode_task.get(), &Task::status, this, &MSALoginDialog::onDeviceFlowStatus); connect(m_devicecode_task.get(), &AuthFlow::authorizeWithBrowser, this, &MSALoginDialog::authorizeWithBrowser); connect(m_devicecode_task.get(), &AuthFlow::authorizeWithBrowserWithExtra, this, &MSALoginDialog::authorizeWithBrowserWithExtra); connect(ui->buttonBox->button(QDialogButtonBox::Cancel), &QPushButton::clicked, m_devicecode_task.get(), &Task::abort); @@ -132,7 +132,7 @@ void MSALoginDialog::onTaskFailed(QString reason) void MSALoginDialog::authorizeWithBrowser(const QUrl& url) { - ui->stackedWidget->setCurrentIndex(1); + ui->stackedWidget2->setCurrentIndex(1); ui->loginButton->setToolTip(QString("
%1
").arg(url.toString())); m_url = url; } @@ -152,12 +152,18 @@ void MSALoginDialog::authorizeWithBrowserWithExtra(QString url, QString code, in } } -void MSALoginDialog::onTaskStatus(QString status) +void MSALoginDialog::onDeviceFlowStatus(QString status) { ui->stackedWidget->setCurrentIndex(0); ui->status->setText(status); } +void MSALoginDialog::onAuthFlowStatus(QString status) +{ + ui->stackedWidget2->setCurrentIndex(0); + ui->status2->setText(status); +} + // Public interface MinecraftAccountPtr MSALoginDialog::newAccount(QWidget* parent) { diff --git a/launcher/ui/dialogs/MSALoginDialog.h b/launcher/ui/dialogs/MSALoginDialog.h index 70f480ca9..375ccc57a 100644 --- a/launcher/ui/dialogs/MSALoginDialog.h +++ b/launcher/ui/dialogs/MSALoginDialog.h @@ -40,7 +40,8 @@ class MSALoginDialog : public QDialog { protected slots: void onTaskFailed(QString reason); - void onTaskStatus(QString status); + void onDeviceFlowStatus(QString status); + void onAuthFlowStatus(QString status); void authorizeWithBrowser(const QUrl& url); void authorizeWithBrowserWithExtra(QString url, QString code, int expiresIn); diff --git a/launcher/ui/dialogs/MSALoginDialog.ui b/launcher/ui/dialogs/MSALoginDialog.ui index c6821782f..69cd2e1ab 100644 --- a/launcher/ui/dialogs/MSALoginDialog.ui +++ b/launcher/ui/dialogs/MSALoginDialog.ui @@ -7,7 +7,7 @@ 0 0 440 - 430 + 447 @@ -20,6 +20,171 @@ Add Microsoft Account + + + + 1 + + + + + + + Qt::Vertical + + + + 20 + 40 + + + + + + + + + 16 + 75 + true + + + + Please wait... + + + Qt::AlignCenter + + + true + + + + + + + Status + + + Qt::AlignCenter + + + true + + + + + + + Qt::Vertical + + + + 20 + 40 + + + + + + + + + + + + + + Qt::Horizontal + + + + 40 + 20 + + + + + + + + + 250 + 40 + + + + Sign in with Microsoft + + + true + + + + + + + Qt::Horizontal + + + + 40 + 20 + + + + + + + + + + + + + + + + + 0 + 0 + + + + Qt::Horizontal + + + + + + + + 16 + + + + Or + + + Qt::AlignCenter + + + + + + + + 0 + 0 + + + + Qt::Horizontal + + + + + @@ -28,7 +193,7 @@ - + Qt::Vertical @@ -89,98 +254,7 @@ - - - - - - - Qt::Horizontal - - - - 40 - 20 - - - - - - - - - 250 - 40 - - - - Sign in with Microsoft - - - true - - - - - - - Qt::Horizontal - - - - 40 - 20 - - - - - - - - - - - - - 0 - 0 - - - - Qt::Horizontal - - - - - - - - 16 - - - - Or - - - Qt::AlignCenter - - - - - - - - 0 - 0 - - - - Qt::Horizontal - - - - - + diff --git a/launcher/ui/dialogs/ProgressDialog.cpp b/launcher/ui/dialogs/ProgressDialog.cpp index 0ca3a1bd9..9897687e3 100644 --- a/launcher/ui/dialogs/ProgressDialog.cpp +++ b/launcher/ui/dialogs/ProgressDialog.cpp @@ -90,6 +90,9 @@ void ProgressDialog::on_skipButton_clicked(bool checked) ProgressDialog::~ProgressDialog() { + for (auto conn : this->m_taskConnections) { + disconnect(conn); + } delete ui; } @@ -140,15 +143,15 @@ int ProgressDialog::execWithTask(Task* task) } // Connect signals. - connect(task, &Task::started, this, &ProgressDialog::onTaskStarted); - connect(task, &Task::failed, this, &ProgressDialog::onTaskFailed); - connect(task, &Task::succeeded, this, &ProgressDialog::onTaskSucceeded); - connect(task, &Task::status, this, &ProgressDialog::changeStatus); - connect(task, &Task::details, this, &ProgressDialog::changeStatus); - connect(task, &Task::stepProgress, this, &ProgressDialog::changeStepProgress); - connect(task, &Task::progress, this, &ProgressDialog::changeProgress); - connect(task, &Task::aborted, this, &ProgressDialog::hide); - connect(task, &Task::abortStatusChanged, ui->skipButton, &QPushButton::setEnabled); + this->m_taskConnections.push_back(connect(task, &Task::started, this, &ProgressDialog::onTaskStarted)); + this->m_taskConnections.push_back(connect(task, &Task::failed, this, &ProgressDialog::onTaskFailed)); + this->m_taskConnections.push_back(connect(task, &Task::succeeded, this, &ProgressDialog::onTaskSucceeded)); + this->m_taskConnections.push_back(connect(task, &Task::status, this, &ProgressDialog::changeStatus)); + this->m_taskConnections.push_back(connect(task, &Task::details, this, &ProgressDialog::changeStatus)); + this->m_taskConnections.push_back(connect(task, &Task::stepProgress, this, &ProgressDialog::changeStepProgress)); + this->m_taskConnections.push_back(connect(task, &Task::progress, this, &ProgressDialog::changeProgress)); + this->m_taskConnections.push_back(connect(task, &Task::aborted, this, &ProgressDialog::hide)); + this->m_taskConnections.push_back(connect(task, &Task::abortStatusChanged, ui->skipButton, &QPushButton::setEnabled)); m_is_multi_step = task->isMultiStep(); ui->taskProgressScrollArea->setHidden(!m_is_multi_step); diff --git a/launcher/ui/dialogs/ProgressDialog.h b/launcher/ui/dialogs/ProgressDialog.h index 15eadf4e7..4a696a49d 100644 --- a/launcher/ui/dialogs/ProgressDialog.h +++ b/launcher/ui/dialogs/ProgressDialog.h @@ -93,6 +93,8 @@ class ProgressDialog : public QDialog { Ui::ProgressDialog* ui; Task* m_task; + + QList m_taskConnections; bool m_is_multi_step = false; QHash taskProgress; diff --git a/launcher/ui/dialogs/skins/SkinManageDialog.cpp b/launcher/ui/dialogs/skins/SkinManageDialog.cpp index 6c85ffa96..65840eb08 100644 --- a/launcher/ui/dialogs/skins/SkinManageDialog.cpp +++ b/launcher/ui/dialogs/skins/SkinManageDialog.cpp @@ -139,6 +139,9 @@ void SkinManageDialog::on_fileBtn_clicked() { auto filter = QMimeDatabase().mimeTypeForName("image/png").filterString(); QString raw_path = QFileDialog::getOpenFileName(this, tr("Select Skin Texture"), QString(), filter); + if (raw_path.isNull()) { + return; + } auto message = m_list.installSkin(raw_path, {}); if (!message.isEmpty()) { CustomMessageBox::selectable(this, tr("Selected file is not a valid skin"), message, QMessageBox::Critical)->show(); diff --git a/launcher/ui/pages/modplatform/ModModel.cpp b/launcher/ui/pages/modplatform/ModModel.cpp index e87a423fa..1f0329321 100644 --- a/launcher/ui/pages/modplatform/ModModel.cpp +++ b/launcher/ui/pages/modplatform/ModModel.cpp @@ -104,18 +104,6 @@ bool checkSide(QString filter, QString value) return filter.isEmpty() || value.isEmpty() || filter == "both" || value == "both" || filter == value; } -bool checkMcVersions(std::list filter, QStringList value) -{ - bool valid = false; - for (auto mcVersion : filter) { - if (value.contains(mcVersion.toString())) { - valid = true; - break; - } - } - return filter.empty() || valid; -} - bool ModModel::checkFilters(ModPlatform::IndexedPack::Ptr pack) { if (!m_filter) @@ -135,7 +123,7 @@ bool ModModel::checkVersionFilters(const ModPlatform::IndexedVersion& v) checkSide(m_filter->side, v.side) && // side (m_filter->releases.empty() || // releases std::find(m_filter->releases.cbegin(), m_filter->releases.cend(), v.version_type) != m_filter->releases.cend()) && - checkMcVersions(m_filter->versions, v.mcVersion)); // mcVersions + m_filter->checkMcVersions(v.mcVersion)); // mcVersions } } // namespace ResourceDownload diff --git a/launcher/ui/pages/modplatform/ResourceModel.cpp b/launcher/ui/pages/modplatform/ResourceModel.cpp index c8eb91570..50a170fff 100644 --- a/launcher/ui/pages/modplatform/ResourceModel.cpp +++ b/launcher/ui/pages/modplatform/ResourceModel.cpp @@ -31,9 +31,9 @@ QHash ResourceModel::s_running_models; ResourceModel::ResourceModel(ResourceAPI* api) : QAbstractListModel(), m_api(api) { s_running_models.insert(this, true); -#ifndef LAUNCHER_TEST - m_current_info_job.setMaxConcurrent(APPLICATION->settings()->get("NumberOfConcurrentDownloads").toInt()); -#endif + if (APPLICATION_DYN) { + m_current_info_job.setMaxConcurrent(APPLICATION->settings()->get("NumberOfConcurrentDownloads").toInt()); + } } ResourceModel::~ResourceModel() @@ -60,11 +60,15 @@ auto ResourceModel::data(const QModelIndex& index, int role) const -> QVariant return pack->description; } case Qt::DecorationRole: { - if (auto icon_or_none = const_cast(this)->getIcon(const_cast(index), pack->logoUrl); - icon_or_none.has_value()) - return icon_or_none.value(); + if (APPLICATION_DYN) { + if (auto icon_or_none = const_cast(this)->getIcon(const_cast(index), pack->logoUrl); + icon_or_none.has_value()) + return icon_or_none.value(); - return APPLICATION->getThemedIcon("screenshot-placeholder"); + return APPLICATION->getThemedIcon("screenshot-placeholder"); + } else { + return {}; + } } case Qt::SizeHintRole: return QSize(0, 58); diff --git a/launcher/ui/pages/modplatform/ResourcePage.cpp b/launcher/ui/pages/modplatform/ResourcePage.cpp index c8f5814ef..d8dc2e898 100644 --- a/launcher/ui/pages/modplatform/ResourcePage.cpp +++ b/launcher/ui/pages/modplatform/ResourcePage.cpp @@ -255,7 +255,10 @@ void ResourcePage::updateSelectionButton() m_ui->resourceSelectionButton->setEnabled(true); if (auto current_pack = getCurrentPack(); current_pack) { - if (!current_pack->isVersionSelected(m_selected_version_index)) + if (current_pack->versionsLoaded && current_pack->versions.empty()) { + m_ui->resourceSelectionButton->setEnabled(false); + qWarning() << tr("No version available for the selected pack"); + } else if (!current_pack->isVersionSelected(m_selected_version_index)) m_ui->resourceSelectionButton->setText(tr("Select %1 for download").arg(resourceString())); else m_ui->resourceSelectionButton->setText(tr("Deselect %1 for download").arg(resourceString())); diff --git a/launcher/ui/pages/modplatform/flame/FlameModel.cpp b/launcher/ui/pages/modplatform/flame/FlameModel.cpp index a92d5b579..cfdb185ff 100644 --- a/launcher/ui/pages/modplatform/flame/FlameModel.cpp +++ b/launcher/ui/pages/modplatform/flame/FlameModel.cpp @@ -1,6 +1,7 @@ #include "FlameModel.h" #include #include "Application.h" +#include "modplatform/ModIndex.h" #include "modplatform/ResourceAPI.h" #include "modplatform/flame/FlameAPI.h" #include "ui/widgets/ProjectItem.h" @@ -183,34 +184,28 @@ void ListModel::performPaginatedSearch() return; } } - auto netJob = makeShared("Flame::Search", APPLICATION->network()); - auto searchUrl = QString( - "https://api.curseforge.com/v1/mods/search?" - "gameId=432&" - "classId=4471&" - "index=%1&" - "pageSize=25&" - "searchFilter=%2&" - "sortField=%3&" - "sortOrder=desc") - .arg(nextSearchOffset) - .arg(currentSearchTerm) - .arg(currentSort + 1); + ResourceAPI::SortingMethod sort{}; + sort.index = currentSort + 1; - netJob->addNetAction(Net::ApiDownload::makeByteArray(QUrl(searchUrl), response)); + auto netJob = makeShared("Flame::Search", APPLICATION->network()); + auto searchUrl = FlameAPI().getSearchURL({ ModPlatform::ResourceType::MODPACK, nextSearchOffset, currentSearchTerm, sort, + m_filter->loaders, m_filter->versions, "", m_filter->categoryIds }); + + netJob->addNetAction(Net::ApiDownload::makeByteArray(QUrl(searchUrl.value()), response)); jobPtr = netJob; jobPtr->start(); QObject::connect(netJob.get(), &NetJob::succeeded, this, &ListModel::searchRequestFinished); QObject::connect(netJob.get(), &NetJob::failed, this, &ListModel::searchRequestFailed); } -void ListModel::searchWithTerm(const QString& term, int sort) +void ListModel::searchWithTerm(const QString& term, int sort, std::shared_ptr filter, bool filterChanged) { - if (currentSearchTerm == term && currentSearchTerm.isNull() == term.isNull() && currentSort == sort) { + if (currentSearchTerm == term && currentSearchTerm.isNull() == term.isNull() && currentSort == sort && !filterChanged) { return; } currentSearchTerm = term; currentSort = sort; + m_filter = filter; if (hasActiveSearchJob()) { jobPtr->abort(); searchState = ResetRequested; diff --git a/launcher/ui/pages/modplatform/flame/FlameModel.h b/launcher/ui/pages/modplatform/flame/FlameModel.h index 9b6d70fec..026f6d1ee 100644 --- a/launcher/ui/pages/modplatform/flame/FlameModel.h +++ b/launcher/ui/pages/modplatform/flame/FlameModel.h @@ -14,6 +14,7 @@ #include #include +#include "ui/widgets/ModFilterWidget.h" #include @@ -38,7 +39,7 @@ class ListModel : public QAbstractListModel { void fetchMore(const QModelIndex& parent) override; void getLogo(const QString& logo, const QString& logoUrl, LogoCallback callback); - void searchWithTerm(const QString& term, int sort); + void searchWithTerm(const QString& term, int sort, std::shared_ptr filter, bool filterChanged); [[nodiscard]] bool hasActiveSearchJob() const { return jobPtr && jobPtr->isRunning(); } [[nodiscard]] Task::Ptr activeSearchJob() { return hasActiveSearchJob() ? jobPtr : nullptr; } @@ -65,6 +66,7 @@ class ListModel : public QAbstractListModel { QString currentSearchTerm; int currentSort = 0; + std::shared_ptr m_filter; int nextSearchOffset = 0; enum SearchState { None, CanPossiblyFetchMore, ResetRequested, Finished } searchState = None; Task::Ptr jobPtr; diff --git a/launcher/ui/pages/modplatform/flame/FlamePage.cpp b/launcher/ui/pages/modplatform/flame/FlamePage.cpp index 4195683e7..9abf4a9c6 100644 --- a/launcher/ui/pages/modplatform/flame/FlamePage.cpp +++ b/launcher/ui/pages/modplatform/flame/FlamePage.cpp @@ -34,10 +34,14 @@ */ #include "FlamePage.h" +#include "Version.h" +#include "modplatform/flame/FlamePackIndex.h" #include "ui/dialogs/CustomMessageBox.h" +#include "ui/widgets/ModFilterWidget.h" #include "ui_FlamePage.h" #include +#include #include "Application.h" #include "FlameModel.h" @@ -88,6 +92,7 @@ FlamePage::FlamePage(NewInstanceDialog* dialog, QWidget* parent) ui->packView->setItemDelegate(new ProjectItemDelegate(this)); ui->packDescription->setMetaEntry("FlamePacks"); + createFilterWidget(); } FlamePage::~FlamePage() @@ -131,10 +136,25 @@ void FlamePage::openedImpl() void FlamePage::triggerSearch() { - listModel->searchWithTerm(ui->searchEdit->text(), ui->sortByBox->currentIndex()); + ui->packView->selectionModel()->setCurrentIndex({}, QItemSelectionModel::SelectionFlag::ClearAndSelect); + ui->packView->clearSelection(); + ui->packDescription->clear(); + ui->versionSelectionBox->clear(); + listModel->searchWithTerm(ui->searchEdit->text(), ui->sortByBox->currentIndex(), m_filterWidget->getFilter(), + m_filterWidget->changed()); m_fetch_progress.watch(listModel->activeSearchJob().get()); } +bool checkVersionFilters(const Flame::IndexedVersion& v, std::shared_ptr filter) +{ + if (!filter) + return true; + return ((!filter->loaders || !v.loaders || filter->loaders & v.loaders) && // loaders + (filter->releases.empty() || // releases + std::find(filter->releases.cbegin(), filter->releases.cend(), v.version_type) != filter->releases.cend()) && + filter->checkMcVersions({ v.mcVersion })); // mcVersions} +} + void FlamePage::onSelectionChanged(QModelIndex curr, [[maybe_unused]] QModelIndex prev) { ui->versionSelectionBox->clear(); @@ -148,7 +168,7 @@ void FlamePage::onSelectionChanged(QModelIndex curr, [[maybe_unused]] QModelInde current = listModel->data(curr, Qt::UserRole).value(); - if (current.versionsLoaded == false) { + if (!current.versionsLoaded || m_filterWidget->changed()) { qDebug() << "Loading flame modpack versions"; auto netJob = new NetJob(QString("Flame::PackVersions(%1)").arg(current.name), APPLICATION->network()); auto response = std::make_shared(); @@ -176,6 +196,16 @@ void FlamePage::onSelectionChanged(QModelIndex curr, [[maybe_unused]] QModelInde qWarning() << "Error while reading flame modpack version: " << e.cause(); } + auto pred = [this](const Flame::IndexedVersion& v) { return !checkVersionFilters(v, m_filterWidget->getFilter()); }; +#if QT_VERSION >= QT_VERSION_CHECK(6, 1, 0) + current.versions.removeIf(pred); +#else + for (auto it = current.versions.begin(); it != current.versions.end();) + if (pred(*it)) + it = current.versions.erase(it); + else + ++it; +#endif for (auto version : current.versions) { auto release_type = version.version_type.isValid() ? QString(" [%1]").arg(version.version_type.toString()) : ""; auto mcVersion = !version.mcVersion.isEmpty() && !version.version.contains(version.mcVersion) @@ -308,3 +338,25 @@ void FlamePage::setSearchTerm(QString term) { ui->searchEdit->setText(term); } + +void FlamePage::createFilterWidget() +{ + auto widget = ModFilterWidget::create(nullptr, false, this); + m_filterWidget.swap(widget); + auto old = ui->splitter->replaceWidget(0, m_filterWidget.get()); + // because we replaced the widget we also need to delete it + if (old) { + delete old; + } + + connect(ui->filterButton, &QPushButton::clicked, this, [this] { m_filterWidget->setHidden(!m_filterWidget->isHidden()); }); + + connect(m_filterWidget.get(), &ModFilterWidget::filterChanged, this, &FlamePage::triggerSearch); + auto response = std::make_shared(); + m_categoriesTask = FlameAPI::getCategories(response, ModPlatform::ResourceType::MODPACK); + QObject::connect(m_categoriesTask.get(), &Task::succeeded, [this, response]() { + auto categories = FlameAPI::loadModCategories(response); + m_filterWidget->setCategories(categories); + }); + m_categoriesTask->start(); +} diff --git a/launcher/ui/pages/modplatform/flame/FlamePage.h b/launcher/ui/pages/modplatform/flame/FlamePage.h index 45a3c6b22..27c96d2f1 100644 --- a/launcher/ui/pages/modplatform/flame/FlamePage.h +++ b/launcher/ui/pages/modplatform/flame/FlamePage.h @@ -41,6 +41,7 @@ #include #include #include "ui/pages/modplatform/ModpackProviderBasePage.h" +#include "ui/widgets/ModFilterWidget.h" #include "ui/widgets/ProgressWidget.h" namespace Ui { @@ -84,6 +85,7 @@ class FlamePage : public QWidget, public ModpackProviderBasePage { void triggerSearch(); void onSelectionChanged(QModelIndex first, QModelIndex second); void onVersionSelectionChanged(int index); + void createFilterWidget(); private: Ui::FlamePage* ui = nullptr; @@ -97,4 +99,7 @@ class FlamePage : public QWidget, public ModpackProviderBasePage { // Used to do instant searching with a delay to cache quick changes QTimer m_search_timer; + + unique_qobject_ptr m_filterWidget; + Task::Ptr m_categoriesTask; }; diff --git a/launcher/ui/pages/modplatform/flame/FlamePage.ui b/launcher/ui/pages/modplatform/flame/FlamePage.ui index d4ddb37a4..b028e7569 100644 --- a/launcher/ui/pages/modplatform/flame/FlamePage.ui +++ b/launcher/ui/pages/modplatform/flame/FlamePage.ui @@ -30,42 +30,59 @@ - - - Search and filter... - - - - - + - - - Qt::ScrollBarAlwaysOff - - - true - - - - 48 - 48 - + + + Search and filter... - - - true - - - true + + + Filter + + + + + 0 + 0 + + + + Qt::Horizontal + + + + + Qt::ScrollBarAlwaysOff + + + true + + + + 48 + 48 + + + + + + true + + + true + + + + diff --git a/launcher/ui/pages/modplatform/flame/FlameResourcePages.cpp b/launcher/ui/pages/modplatform/flame/FlameResourcePages.cpp index 62c22902e..ce8c03fa1 100644 --- a/launcher/ui/pages/modplatform/flame/FlameResourcePages.cpp +++ b/launcher/ui/pages/modplatform/flame/FlameResourcePages.cpp @@ -215,11 +215,11 @@ unique_qobject_ptr FlameModPage::createFilterWidget() void FlameModPage::prepareProviderCategories() { auto response = std::make_shared(); - auto task = FlameAPI::getModCategories(response); - QObject::connect(task.get(), &Task::succeeded, [this, response]() { + m_categoriesTask = FlameAPI::getModCategories(response); + QObject::connect(m_categoriesTask.get(), &Task::succeeded, [this, response]() { auto categories = FlameAPI::loadModCategories(response); m_filter_widget->setCategories(categories); }); - task->start(); + m_categoriesTask->start(); }; } // namespace ResourceDownload diff --git a/launcher/ui/pages/modplatform/flame/FlameResourcePages.h b/launcher/ui/pages/modplatform/flame/FlameResourcePages.h index 6eef3e435..052706549 100644 --- a/launcher/ui/pages/modplatform/flame/FlameResourcePages.h +++ b/launcher/ui/pages/modplatform/flame/FlameResourcePages.h @@ -100,6 +100,9 @@ class FlameModPage : public ModPage { protected: virtual void prepareProviderCategories() override; + + private: + Task::Ptr m_categoriesTask; }; class FlameResourcePackPage : public ResourcePackResourcePage { diff --git a/launcher/ui/pages/modplatform/modrinth/ModrinthModel.cpp b/launcher/ui/pages/modplatform/modrinth/ModrinthModel.cpp index b53eea4ef..417ff4080 100644 --- a/launcher/ui/pages/modplatform/modrinth/ModrinthModel.cpp +++ b/launcher/ui/pages/modplatform/modrinth/ModrinthModel.cpp @@ -152,33 +152,26 @@ void ModpackListModel::performPaginatedSearch() return; } } // TODO: Move to standalone API - auto netJob = makeShared("Modrinth::SearchModpack", APPLICATION->network()); - auto searchAllUrl = QString(BuildConfig.MODRINTH_PROD_URL + - "/search?" - "offset=%1&" - "limit=%2&" - "query=%3&" - "index=%4&" - "facets=[[\"project_type:modpack\"]]") - .arg(nextSearchOffset) - .arg(m_modpacks_per_page) - .arg(currentSearchTerm) - .arg(currentSort); + ResourceAPI::SortingMethod sort{}; + sort.name = currentSort; + auto searchUrl = ModrinthAPI().getSearchURL({ ModPlatform::ResourceType::MODPACK, nextSearchOffset, currentSearchTerm, sort, + m_filter->loaders, m_filter->versions, "", m_filter->categoryIds }); - netJob->addNetAction(Net::ApiDownload::makeByteArray(QUrl(searchAllUrl), m_all_response)); + auto netJob = makeShared("Modrinth::SearchModpack", APPLICATION->network()); + netJob->addNetAction(Net::ApiDownload::makeByteArray(QUrl(searchUrl.value()), m_allResponse)); QObject::connect(netJob.get(), &NetJob::succeeded, this, [this] { - QJsonParseError parse_error_all{}; + QJsonParseError parseError{}; - QJsonDocument doc_all = QJsonDocument::fromJson(*m_all_response, &parse_error_all); - if (parse_error_all.error != QJsonParseError::NoError) { - qWarning() << "Error while parsing JSON response from " << debugName() << " at " << parse_error_all.offset - << " reason: " << parse_error_all.errorString(); - qWarning() << *m_all_response; + QJsonDocument doc = QJsonDocument::fromJson(*m_allResponse, &parseError); + if (parseError.error != QJsonParseError::NoError) { + qWarning() << "Error while parsing JSON response from " << debugName() << " at " << parseError.offset + << " reason: " << parseError.errorString(); + qWarning() << *m_allResponse; return; } - searchRequestFinished(doc_all); + searchRequestFinished(doc); }); QObject::connect(netJob.get(), &NetJob::failed, this, &ModpackListModel::searchRequestFailed); @@ -220,19 +213,23 @@ static auto sortFromIndex(int index) -> QString } } -void ModpackListModel::searchWithTerm(const QString& term, const int sort) +void ModpackListModel::searchWithTerm(const QString& term, + const int sort, + std::shared_ptr filter, + bool filterChanged) { if (sort > 5 || sort < 0) return; auto sort_str = sortFromIndex(sort); - if (currentSearchTerm == term && currentSearchTerm.isNull() == term.isNull() && currentSort == sort_str) { + if (currentSearchTerm == term && currentSearchTerm.isNull() == term.isNull() && currentSort == sort_str && !filterChanged) { return; } currentSearchTerm = term; currentSort = sort_str; + m_filter = filter; refresh(); } diff --git a/launcher/ui/pages/modplatform/modrinth/ModrinthModel.h b/launcher/ui/pages/modplatform/modrinth/ModrinthModel.h index 514ee4484..640ddf688 100644 --- a/launcher/ui/pages/modplatform/modrinth/ModrinthModel.h +++ b/launcher/ui/pages/modplatform/modrinth/ModrinthModel.h @@ -71,7 +71,7 @@ class ModpackListModel : public QAbstractListModel { /* Ask the API for more information */ void fetchMore(const QModelIndex& parent) override; void refresh(); - void searchWithTerm(const QString& term, int sort); + void searchWithTerm(const QString& term, int sort, std::shared_ptr filter, bool filterChanged); [[nodiscard]] bool hasActiveSearchJob() const { return jobPtr && jobPtr->isRunning(); } [[nodiscard]] Task::Ptr activeSearchJob() { return hasActiveSearchJob() ? jobPtr : nullptr; } @@ -112,12 +112,13 @@ class ModpackListModel : public QAbstractListModel { QString currentSearchTerm; QString currentSort; + std::shared_ptr m_filter; int nextSearchOffset = 0; enum SearchState { None, CanPossiblyFetchMore, ResetRequested, Finished } searchState = None; Task::Ptr jobPtr; - std::shared_ptr m_all_response = std::make_shared(); + std::shared_ptr m_allResponse = std::make_shared(); QByteArray m_specific_response; int m_modpacks_per_page = 20; diff --git a/launcher/ui/pages/modplatform/modrinth/ModrinthPage.cpp b/launcher/ui/pages/modplatform/modrinth/ModrinthPage.cpp index a000044fa..8803c6dd9 100644 --- a/launcher/ui/pages/modplatform/modrinth/ModrinthPage.cpp +++ b/launcher/ui/pages/modplatform/modrinth/ModrinthPage.cpp @@ -35,6 +35,8 @@ */ #include "ModrinthPage.h" +#include "Version.h" +#include "modplatform/modrinth/ModrinthAPI.h" #include "ui/dialogs/CustomMessageBox.h" #include "ui_ModrinthPage.h" @@ -58,6 +60,7 @@ ModrinthPage::ModrinthPage(NewInstanceDialog* dialog, QWidget* parent) : QWidget(parent), ui(new Ui::ModrinthPage), dialog(dialog), m_fetch_progress(this, false) { ui->setupUi(this); + createFilterWidget(); ui->searchEdit->installEventFilter(this); m_model = new Modrinth::ModpackListModel(this); @@ -126,6 +129,16 @@ bool ModrinthPage::eventFilter(QObject* watched, QEvent* event) return QObject::eventFilter(watched, event); } +bool checkVersionFilters(const Modrinth::ModpackVersion& v, std::shared_ptr filter) +{ + if (!filter) + return true; + return ((!filter->loaders || !v.loaders || filter->loaders & v.loaders) && // loaders + (filter->releases.empty() || // releases + std::find(filter->releases.cbegin(), filter->releases.cend(), v.version_type) != filter->releases.cend()) && + filter->checkMcVersions({ v.gameVersion })); // gameVersion} +} + void ModrinthPage::onSelectionChanged(QModelIndex curr, [[maybe_unused]] QModelIndex prev) { ui->versionSelectionBox->clear(); @@ -190,7 +203,7 @@ void ModrinthPage::onSelectionChanged(QModelIndex curr, [[maybe_unused]] QModelI } else updateUI(); - if (!current.versionsLoaded) { + if (!current.versionsLoaded || m_filterWidget->changed()) { qDebug() << "Loading modrinth modpack versions"; auto netJob = new NetJob(QString("Modrinth::PackVersions(%1)").arg(current.name), APPLICATION->network()); @@ -221,6 +234,16 @@ void ModrinthPage::onSelectionChanged(QModelIndex curr, [[maybe_unused]] QModelI qDebug() << *response; qWarning() << "Error while reading modrinth modpack version: " << e.cause(); } + auto pred = [this](const Modrinth::ModpackVersion& v) { return !checkVersionFilters(v, m_filterWidget->getFilter()); }; +#if QT_VERSION >= QT_VERSION_CHECK(6, 1, 0) + current.versions.removeIf(pred); +#else + for (auto it = current.versions.begin(); it != current.versions.end();) + if (pred(*it)) + it = current.versions.erase(it); + else + ++it; +#endif for (auto version : current.versions) { auto release_type = version.version_type.isValid() ? QString(" [%1]").arg(version.version_type.toString()) : ""; auto mcVersion = !version.gameVersion.isEmpty() && !version.name.contains(version.gameVersion) @@ -338,7 +361,11 @@ void ModrinthPage::suggestCurrent() void ModrinthPage::triggerSearch() { - m_model->searchWithTerm(ui->searchEdit->text(), ui->sortByBox->currentIndex()); + ui->packView->selectionModel()->setCurrentIndex({}, QItemSelectionModel::SelectionFlag::ClearAndSelect); + ui->packView->clearSelection(); + ui->packDescription->clear(); + ui->versionSelectionBox->clear(); + m_model->searchWithTerm(ui->searchEdit->text(), ui->sortByBox->currentIndex(), m_filterWidget->getFilter(), m_filterWidget->changed()); m_fetch_progress.watch(m_model->activeSearchJob().get()); } @@ -361,3 +388,25 @@ QString ModrinthPage::getSerachTerm() const { return ui->searchEdit->text(); } + +void ModrinthPage::createFilterWidget() +{ + auto widget = ModFilterWidget::create(nullptr, true, this); + m_filterWidget.swap(widget); + auto old = ui->splitter->replaceWidget(0, m_filterWidget.get()); + // because we replaced the widget we also need to delete it + if (old) { + delete old; + } + + connect(ui->filterButton, &QPushButton::clicked, this, [this] { m_filterWidget->setHidden(!m_filterWidget->isHidden()); }); + + connect(m_filterWidget.get(), &ModFilterWidget::filterChanged, this, &ModrinthPage::triggerSearch); + auto response = std::make_shared(); + m_categoriesTask = ModrinthAPI::getModCategories(response); + QObject::connect(m_categoriesTask.get(), &Task::succeeded, [this, response]() { + auto categories = ModrinthAPI::loadCategories(response, "modpack"); + m_filterWidget->setCategories(categories); + }); + m_categoriesTask->start(); +} \ No newline at end of file diff --git a/launcher/ui/pages/modplatform/modrinth/ModrinthPage.h b/launcher/ui/pages/modplatform/modrinth/ModrinthPage.h index dd99e0d29..7f504cdbd 100644 --- a/launcher/ui/pages/modplatform/modrinth/ModrinthPage.h +++ b/launcher/ui/pages/modplatform/modrinth/ModrinthPage.h @@ -41,6 +41,7 @@ #include "modplatform/modrinth/ModrinthPackManifest.h" #include "ui/pages/modplatform/ModpackProviderBasePage.h" +#include "ui/widgets/ModFilterWidget.h" #include "ui/widgets/ProgressWidget.h" #include @@ -87,6 +88,7 @@ class ModrinthPage : public QWidget, public ModpackProviderBasePage { void onSelectionChanged(QModelIndex first, QModelIndex second); void onVersionSelectionChanged(int index); void triggerSearch(); + void createFilterWidget(); private: Ui::ModrinthPage* ui; @@ -100,4 +102,7 @@ class ModrinthPage : public QWidget, public ModpackProviderBasePage { // Used to do instant searching with a delay to cache quick changes QTimer m_search_timer; + + unique_qobject_ptr m_filterWidget; + Task::Ptr m_categoriesTask; }; diff --git a/launcher/ui/pages/modplatform/modrinth/ModrinthPage.ui b/launcher/ui/pages/modplatform/modrinth/ModrinthPage.ui index 7f4f903f6..ef44abb52 100644 --- a/launcher/ui/pages/modplatform/modrinth/ModrinthPage.ui +++ b/launcher/ui/pages/modplatform/modrinth/ModrinthPage.ui @@ -12,42 +12,59 @@ - - - Search and filter ... - - - - - + - - - Qt::ScrollBarAlwaysOff - - - true - - - - 48 - 48 - + + + Search and filter... - - - true - - - true + + + Filter + + + + + 0 + 0 + + + + Qt::Horizontal + + + + + Qt::ScrollBarAlwaysOff + + + true + + + + 48 + 48 + + + + + + true + + + true + + + + diff --git a/launcher/ui/widgets/CustomCommands.ui b/launcher/ui/widgets/CustomCommands.ui index 4a39ff7f7..b485c293e 100644 --- a/launcher/ui/widgets/CustomCommands.ui +++ b/launcher/ui/widgets/CustomCommands.ui @@ -38,19 +38,6 @@ false - - - - P&ost-exit command: - - - postExitCmdTextBox - - - - - - @@ -61,8 +48,8 @@ - - + + @@ -77,6 +64,19 @@ + + + + P&ost-exit command: + + + postExitCmdTextBox + + + + + + diff --git a/launcher/ui/widgets/ModFilterWidget.cpp b/launcher/ui/widgets/ModFilterWidget.cpp index bbb91eac2..68adcdb71 100644 --- a/launcher/ui/widgets/ModFilterWidget.cpp +++ b/launcher/ui/widgets/ModFilterWidget.cpp @@ -64,10 +64,49 @@ class VersionBasicModel : public QIdentityProxyModel { { if (role == Qt::DisplayRole) return QIdentityProxyModel::data(index, BaseVersionList::VersionIdRole); + if (role == Qt::UserRole) + return QIdentityProxyModel::data(index, BaseVersionList::VersionIdRole); return {}; } }; +class AllVersionProxyModel : public QSortFilterProxyModel { + Q_OBJECT + + public: + AllVersionProxyModel(QObject* parent = nullptr) : QSortFilterProxyModel(parent) {} + + int rowCount(const QModelIndex& parent = QModelIndex()) const override { return QSortFilterProxyModel::rowCount(parent) + 1; } + + QVariant data(const QModelIndex& index, int role = Qt::DisplayRole) const override + { + if (!index.isValid()) { + return {}; + } + + if (index.row() == 0) { + if (role == Qt::DisplayRole) { + return tr("All Versions"); + } + if (role == Qt::UserRole) { + return "all"; + } + return {}; + } + + QModelIndex newIndex = QSortFilterProxyModel::index(index.row() - 1, index.column()); + return QSortFilterProxyModel::data(newIndex, role); + } + + Qt::ItemFlags flags(const QModelIndex& index) const override + { + if (index.row() == 0) { + return Qt::ItemIsSelectable | Qt::ItemIsEnabled; + } + return QSortFilterProxyModel::flags(index); + } +}; + ModFilterWidget::ModFilterWidget(MinecraftInstance* instance, bool extended, QWidget* parent) : QTabWidget(parent), ui(new Ui::ModFilterWidget), m_instance(instance), m_filter(new Filter()) { @@ -76,14 +115,21 @@ ModFilterWidget::ModFilterWidget(MinecraftInstance* instance, bool extended, QWi m_versions_proxy = new VersionProxyModel(this); m_versions_proxy->setFilter(BaseVersionList::TypeRole, new ExactFilter("release")); - auto proxy = new VersionBasicModel(this); + QAbstractProxyModel* proxy = new VersionBasicModel(this); proxy->setSourceModel(m_versions_proxy); if (extended) { + if (!m_instance) { + ui->environmentGroup->hide(); + } ui->versions->setSourceModel(proxy); ui->versions->setSeparator(", "); + ui->versions->setDefaultText(tr("All Versions")); ui->version->hide(); } else { + auto allVersions = new AllVersionProxyModel(this); + allVersions->setSourceModel(proxy); + proxy = allVersions; ui->version->setModel(proxy); ui->versions->hide(); ui->showAllVersions->hide(); @@ -162,18 +208,22 @@ void ModFilterWidget::loadVersionList() void ModFilterWidget::prepareBasicFilter() { - m_filter->hideInstalled = false; - m_filter->side = ""; // or "both" - auto loaders = m_instance->getPackProfile()->getSupportedModLoaders().value(); - ui->neoForge->setChecked(loaders & ModPlatform::NeoForge); - ui->forge->setChecked(loaders & ModPlatform::Forge); - ui->fabric->setChecked(loaders & ModPlatform::Fabric); - ui->quilt->setChecked(loaders & ModPlatform::Quilt); - m_filter->loaders = loaders; - auto def = m_instance->getPackProfile()->getComponentVersion("net.minecraft"); - m_filter->versions.emplace_front(def); - ui->versions->setCheckedItems({ def }); - ui->version->setCurrentIndex(ui->version->findText(def)); + if (m_instance) { + m_filter->hideInstalled = false; + m_filter->side = ""; // or "both" + auto loaders = m_instance->getPackProfile()->getSupportedModLoaders().value(); + ui->neoForge->setChecked(loaders & ModPlatform::NeoForge); + ui->forge->setChecked(loaders & ModPlatform::Forge); + ui->fabric->setChecked(loaders & ModPlatform::Fabric); + ui->quilt->setChecked(loaders & ModPlatform::Quilt); + m_filter->loaders = loaders; + auto def = m_instance->getPackProfile()->getComponentVersion("net.minecraft"); + m_filter->versions.emplace_front(def); + ui->versions->setCheckedItems({ def }); + ui->version->setCurrentIndex(ui->version->findText(def)); + } else { + ui->hideInstalled->hide(); + } } void ModFilterWidget::onShowAllVersionsChanged() @@ -249,7 +299,9 @@ void ModFilterWidget::onHideInstalledFilterChanged() void ModFilterWidget::onVersionFilterTextChanged(const QString& version) { m_filter->versions.clear(); - m_filter->versions.emplace_back(version); + if (ui->version->currentData(Qt::UserRole) != "all") { + m_filter->versions.emplace_back(version); + } m_filter_changed = true; emit filterChanged(); } diff --git a/launcher/ui/widgets/ModFilterWidget.h b/launcher/ui/widgets/ModFilterWidget.h index fdfd2c8bb..50f0e06c9 100644 --- a/launcher/ui/widgets/ModFilterWidget.h +++ b/launcher/ui/widgets/ModFilterWidget.h @@ -71,6 +71,15 @@ class ModFilterWidget : public QTabWidget { releases == other.releases && categoryIds == other.categoryIds; } bool operator!=(const Filter& other) const { return !(*this == other); } + + bool checkMcVersions(QStringList value) + { + for (auto mcVersion : versions) + if (value.contains(mcVersion.toString())) + return true; + + return versions.empty(); + } }; static unique_qobject_ptr create(MinecraftInstance* instance, bool extended, QWidget* parent = nullptr); diff --git a/nix/unwrapped.nix b/nix/unwrapped.nix index f75acf1de..7fc383ff3 100644 --- a/nix/unwrapped.nix +++ b/nix/unwrapped.nix @@ -87,6 +87,8 @@ stdenv.mkDerivation { (lib.cmakeFeature "CMAKE_INSTALL_PREFIX" "${placeholder "out"}/Applications/") ]; + doCheck = true; + dontWrapQtApps = true; meta = { diff --git a/program_info/CMakeLists.txt b/program_info/CMakeLists.txt index ee2e336b1..e693d757a 100644 --- a/program_info/CMakeLists.txt +++ b/program_info/CMakeLists.txt @@ -57,7 +57,7 @@ Section "Visual Studio Runtime" Pop $0 ${If} $0 == "OK" DetailPrint "Download successful" - ExecWait "$INSTDIR\vc_redist\$vc_redist_exe /install /passive /norestart\" + ExecWait "$INSTDIR\vc_redist\$vc_redist_exe /install /passive /norestart" ${Else} DetailPrint "Download failed with error $0" ${EndIf}