diff --git a/.envrc b/.envrc index 3550a30f2..190b5b2b3 100644 --- a/.envrc +++ b/.envrc @@ -1 +1,2 @@ use flake +watch_file nix/*.nix diff --git a/.github/workflows/backport.yml b/.github/workflows/backport.yml index 77c1a8802..c705ff7b0 100644 --- a/.github/workflows/backport.yml +++ b/.github/workflows/backport.yml @@ -20,11 +20,11 @@ jobs: if: github.repository_owner == 'PrismLauncher' && github.event.pull_request.merged == true && (github.event_name != 'labeled' || startsWith('backport', github.event.label.name)) runs-on: ubuntu-latest steps: - - uses: actions/checkout@v3 + - uses: actions/checkout@v4 with: ref: ${{ github.event.pull_request.head.sha }} - name: Create backport PRs - uses: korthout/backport-action@v1.3.1 + uses: korthout/backport-action@v1.4.0 with: # Config README: https://github.com/korthout/backport-action#backport-action pull_description: |- diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 432f39fb0..059795a11 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -24,6 +24,12 @@ on: CACHIX_AUTH_TOKEN: description: Private token for authenticating against Cachix cache required: false + GPG_PRIVATE_KEY: + description: Private key for AppImage signing + required: false + GPG_PRIVATE_KEY_ID: + description: ID for the GPG_PRIVATE_KEY, to select the signing key + required: false jobs: build: @@ -119,7 +125,7 @@ jobs: # PREPARE ## - name: Checkout - uses: actions/checkout@v3 + uses: actions/checkout@v4 with: submodules: 'true' @@ -158,7 +164,7 @@ jobs: - name: Retrieve ccache cache (Windows MinGW-w64) if: runner.os == 'Windows' && matrix.msystem != '' && inputs.build_type == 'Debug' - uses: actions/cache@v3.3.1 + uses: actions/cache@v3.3.2 with: path: '${{ github.workspace }}\.ccache' key: ${{ matrix.os }}-mingw-w64-ccache-${{ github.run_id }} @@ -249,6 +255,8 @@ jobs: wget "https://github.com/linuxdeploy/linuxdeploy-plugin-appimage/releases/download/continuous/linuxdeploy-plugin-appimage-x86_64.AppImage" wget "https://github.com/linuxdeploy/linuxdeploy-plugin-qt/releases/download/continuous/linuxdeploy-plugin-qt-x86_64.AppImage" + wget "https://github.com/AppImageCommunity/AppImageUpdate/releases/download/continuous/AppImageUpdate-x86_64.AppImage" + ${{ github.workspace }}/.github/scripts/prepare_JREs.sh sudo apt install libopengl0 @@ -387,8 +395,8 @@ jobs: cd ${{ env.INSTALL_DIR }} if ("${{ matrix.qt_ver }}" -eq "5") { - Copy-Item D:/a/PrismLauncher/Qt/Tools/OpenSSL/Win_x86/bin/libcrypto-1_1.dll -Destination libcrypto-1_1.dll - Copy-Item D:/a/PrismLauncher/Qt/Tools/OpenSSL/Win_x86/bin/libssl-1_1.dll -Destination libssl-1_1.dll + Copy-Item ${{ runner.workspace }}/Qt/Tools/OpenSSL/Win_x86/bin/libcrypto-1_1.dll -Destination libcrypto-1_1.dll + Copy-Item ${{ runner.workspace }}/Qt/Tools/OpenSSL/Win_x86/bin/libssl-1_1.dll -Destination libssl-1_1.dll } cd ${{ github.workspace }} @@ -425,7 +433,7 @@ jobs: run: | cp -r ${{ env.INSTALL_DIR }} ${{ env.INSTALL_PORTABLE_DIR }} # cmake install on Windows is slow, let's just copy instead cmake --install ${{ env.BUILD_DIR }} --prefix ${{ env.INSTALL_PORTABLE_DIR }} --component portable - + Get-ChildItem ${{ env.INSTALL_PORTABLE_DIR }} -Recurse | ForEach FullName | Resolve-Path -Relative | %{ $_.TrimStart('.\') } | %{ $_.TrimStart('${{ env.INSTALL_PORTABLE_DIR }}') } | %{ $_.TrimStart('\') } | Out-File -FilePath ${{ env.INSTALL_DIR }}/manifest.txt - name: Package (Windows, installer) @@ -466,11 +474,15 @@ jobs: - name: Package AppImage (Linux) if: runner.os == 'Linux' && matrix.qt_ver != 5 shell: bash + env: + GPG_PRIVATE_KEY: ${{ secrets.GPG_PRIVATE_KEY }} run: | cmake --install ${{ env.BUILD_DIR }} --prefix ${{ env.INSTALL_APPIMAGE_DIR }}/usr + mv ${{ env.INSTALL_APPIMAGE_DIR }}/usr/share/metainfo/org.prismlauncher.PrismLauncher.metainfo.xml ${{ env.INSTALL_APPIMAGE_DIR }}/usr/share/metainfo/org.prismlauncher.PrismLauncher.appdata.xml export "NO_APPSTREAM=1" # we have to skip appstream checking because appstream on ubuntu 20.04 is outdated - export OUTPUT="PrismLauncher-${{ runner.os }}-${{ env.VERSION }}-${{ inputs.build_type }}-x86_64.AppImage" + + export OUTPUT="PrismLauncher-Linux-x86_64.AppImage" chmod +x linuxdeploy-*.AppImage @@ -481,8 +493,8 @@ jobs: cp -r ${{ github.workspace }}/JREs/jre17/* ${{ env.INSTALL_APPIMAGE_DIR }}/usr/lib/jvm/java-17-openjdk - cp -r /home/runner/work/PrismLauncher/Qt/${{ matrix.qt_version }}/gcc_64/plugins/iconengines/* ${{ env.INSTALL_APPIMAGE_DIR }}/usr/plugins/iconengines - + cp -r ${{ runner.workspace }}/Qt/${{ matrix.qt_version }}/gcc_64/plugins/iconengines/* ${{ env.INSTALL_APPIMAGE_DIR }}/usr/plugins/iconengines + cp /usr/lib/x86_64-linux-gnu/libcrypto.so.1.1 ${{ env.INSTALL_APPIMAGE_DIR }}/usr/lib/ cp /usr/lib/x86_64-linux-gnu/libssl.so.1.1 ${{ env.INSTALL_APPIMAGE_DIR }}/usr/lib/ cp /usr/lib/x86_64-linux-gnu/libOpenGL.so.0* ${{ env.INSTALL_APPIMAGE_DIR }}/usr/lib/ @@ -494,8 +506,33 @@ jobs: LD_LIBRARY_PATH="${LD_LIBRARY_PATH}:${{ env.INSTALL_APPIMAGE_DIR }}/usr/lib/jvm/java-17-openjdk/lib" export LD_LIBRARY_PATH + chmod +x AppImageUpdate-x86_64.AppImage + ./AppImageUpdate-x86_64.AppImage --appimage-extract + + mkdir -p ${{ env.INSTALL_APPIMAGE_DIR }}/usr/optional + mkdir -p ${{ env.INSTALL_APPIMAGE_DIR }}/usr/plugins + + cp -r squashfs-root/usr/bin/* ${{ env.INSTALL_APPIMAGE_DIR }}/usr/bin + cp -r squashfs-root/usr/lib/* ${{ env.INSTALL_APPIMAGE_DIR }}/usr/lib + cp -r squashfs-root/usr/optional/* ${{ env.INSTALL_APPIMAGE_DIR }}/usr/optional + cp -r squashfs-root/usr/optional/* ${{ env.INSTALL_APPIMAGE_DIR }}/usr/plugins + + export UPDATE_INFORMATION="gh-releases-zsync|${{ github.repository_owner }}|${{ github.event.repository.name }}|latest|PrismLauncher-Linux-x86_64.AppImage.zsync" + + if [ '${{ secrets.GPG_PRIVATE_KEY_ID }}' != '' ]; then + export SIGN=1 + export SIGN_KEY=${{ secrets.GPG_PRIVATE_KEY_ID }} + mkdir -p ~/.gnupg/ + printf "$GPG_PRIVATE_KEY" | base64 --decode > ~/.gnupg/private.key + gpg --import ~/.gnupg/private.key + else + echo ":warning: Skipped code signing for Linux AppImage, as gpg key was not present." >> $GITHUB_STEP_SUMMARY + fi + ./linuxdeploy-x86_64.AppImage --appdir ${{ env.INSTALL_APPIMAGE_DIR }} --output appimage --plugin qt -i ${{ env.INSTALL_APPIMAGE_DIR }}/usr/share/icons/hicolor/scalable/apps/org.prismlauncher.PrismLauncher.svg + mv "PrismLauncher-Linux-x86_64.AppImage" "PrismLauncher-Linux-${{ env.VERSION }}-${{ inputs.build_type }}-x86_64.AppImage" + ## # UPLOAD BUILDS ## @@ -562,6 +599,13 @@ jobs: with: name: PrismLauncher-${{ runner.os }}-${{ env.VERSION }}-${{ inputs.build_type }}-x86_64.AppImage path: PrismLauncher-${{ runner.os }}-${{ env.VERSION }}-${{ inputs.build_type }}-x86_64.AppImage + + - name: Upload AppImage Zsync (Linux) + if: runner.os == 'Linux' && matrix.qt_ver != 5 + uses: actions/upload-artifact@v3 + with: + name: PrismLauncher-${{ runner.os }}-${{ env.VERSION }}-${{ inputs.build_type }}-x86_64.AppImage.zsync + path: PrismLauncher-Linux-x86_64.AppImage.zsync - name: ccache stats (Windows MinGW-w64) if: runner.os == 'Windows' && matrix.msystem != '' @@ -576,7 +620,7 @@ jobs: options: --privileged steps: - name: Checkout - uses: actions/checkout@v3 + uses: actions/checkout@v4 if: inputs.build_type == 'Debug' with: submodules: 'true' diff --git a/.github/workflows/codeql.yml b/.github/workflows/codeql.yml index 0cd1f6e40..a77b4ae1e 100644 --- a/.github/workflows/codeql.yml +++ b/.github/workflows/codeql.yml @@ -8,7 +8,7 @@ jobs: steps: - name: Checkout repository - uses: actions/checkout@v3 + uses: actions/checkout@v4 with: submodules: 'true' diff --git a/.github/workflows/trigger_release.yml b/.github/workflows/trigger_release.yml index f19b83986..bda75e354 100644 --- a/.github/workflows/trigger_release.yml +++ b/.github/workflows/trigger_release.yml @@ -26,7 +26,7 @@ jobs: upload_url: ${{ steps.create_release.outputs.upload_url }} steps: - name: Checkout - uses: actions/checkout@v3 + uses: actions/checkout@v4 with: submodules: 'true' path: 'PrismLauncher-source' @@ -43,7 +43,8 @@ jobs: mv PrismLauncher-Linux-Qt6*/PrismLauncher.tar.gz PrismLauncher-Linux-Qt6-${{ env.VERSION }}.tar.gz mv PrismLauncher-Linux-Portable*/PrismLauncher-portable.tar.gz PrismLauncher-Linux-Portable-${{ env.VERSION }}.tar.gz mv PrismLauncher-Linux*/PrismLauncher.tar.gz PrismLauncher-Linux-${{ env.VERSION }}.tar.gz - mv PrismLauncher-*.AppImage/PrismLauncher-*.AppImage PrismLauncher-Linux-${{ env.VERSION }}-x86_64.AppImage + mv PrismLauncher-*.AppImage/PrismLauncher-*.AppImage PrismLauncher-Linux-x86_64.AppImage + mv PrismLauncher-*.AppImage.zsync/PrismLauncher-*.AppImage.zsync PrismLauncher-Linux-x86_64.AppImage.zsync mv PrismLauncher-macOS-Legacy*/PrismLauncher.tar.gz PrismLauncher-macOS-Legacy-${{ env.VERSION }}.tar.gz mv PrismLauncher-macOS*/PrismLauncher.tar.gz PrismLauncher-macOS-${{ env.VERSION }}.tar.gz @@ -78,9 +79,8 @@ jobs: - name: Create release id: create_release uses: softprops/action-gh-release@v1 - env: - GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} with: + token: ${{ secrets.GITHUB_TOKEN }} tag_name: ${{ github.ref }} name: Prism Launcher ${{ env.VERSION }} draft: true @@ -88,7 +88,8 @@ jobs: files: | PrismLauncher-Linux-${{ env.VERSION }}.tar.gz PrismLauncher-Linux-Portable-${{ env.VERSION }}.tar.gz - PrismLauncher-Linux-${{ env.VERSION }}-x86_64.AppImage + PrismLauncher-Linux-x86_64.AppImage + PrismLauncher-Linux-x86_64.AppImage.zsync PrismLauncher-Linux-Qt6-${{ env.VERSION }}.tar.gz PrismLauncher-Linux-Qt6-Portable-${{ env.VERSION }}.tar.gz PrismLauncher-Windows-MinGW-w64-${{ env.VERSION }}.zip diff --git a/.github/workflows/update-flake.yml b/.github/workflows/update-flake.yml index ad22120ee..6a16b0369 100644 --- a/.github/workflows/update-flake.yml +++ b/.github/workflows/update-flake.yml @@ -16,13 +16,15 @@ jobs: runs-on: ubuntu-latest steps: - - uses: actions/checkout@v3 - - uses: cachix/install-nix-action@v22 + - uses: actions/checkout@v4 + - uses: cachix/install-nix-action@6a9a9e84a173d90b3ffb42c5ddaf9ea033fad011 # v23 - - uses: DeterminateSystems/update-flake-lock@v19 + - uses: DeterminateSystems/update-flake-lock@v20 with: commit-msg: "chore(nix): update lockfile" pr-title: "chore(nix): update lockfile" pr-labels: | Linux + packaging simple change + changelog:omit diff --git a/CMakeLists.txt b/CMakeLists.txt index 17f7c3d8b..638fba051 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -33,6 +33,13 @@ if(MSVC) # Use /W4 as /Wall includes unnesserey warnings such as added padding to structs set(CMAKE_CXX_FLAGS "/GS /permissive- /W4 ${CMAKE_CXX_FLAGS}") + # /EHs Enables stack unwind semantics for standard C++ exceptions to ensure stackframes are unwound + # and object deconstructors are called when an exception is caught. + # without it memory leaks and a warning is printed + # /EHc tells the compiler to assume that functions declared as extern "C" never throw a C++ exception + # This appears to not always be a defualt compiler option in CMAKE + set(CMAKE_CXX_FLAGS "/EHsc ${CMAKE_CXX_FLAGS}") + # LINK accepts /SUBSYSTEM whics sets if we are a WINDOWS (gui) or a CONSOLE programs # This implicitly selects an entrypoint specific to the subsystem selected # qtmain/QtEntryPointLib provides the correct entrypoint (wWinMain) for gui programs @@ -88,35 +95,36 @@ set(CMAKE_CXX_FLAGS_RELEASE "-O2 -D_FORTIFY_SOURCE=2 ${CMAKE_CXX_FLAGS_RELEASE}" option(DEBUG_ADDRESS_SANITIZER "Enable Address Sanitizer in Debug builds" OFF) # If this is a Debug build turn on address sanitiser -if (CMAKE_BUILD_TYPE STREQUAL "Debug" AND DEBUG_ADDRESS_SANITIZER) +if ((CMAKE_BUILD_TYPE STREQUAL "Debug" OR CMAKE_BUILD_TYPE STREQUAL "RelWithDebInfo") AND DEBUG_ADDRESS_SANITIZER) 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 message(STATUS "Address Sanitizer available on Clang MSVC frontend") - set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} /fsanitize=address /O1 /Oy-") - set(CMAKE_C_FLAGS "${CMAKE_C_FLAGS} /fsanitize=address /O1 /Oy-") + set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} /fsanitize=address /Oy-") + set(CMAKE_C_FLAGS "${CMAKE_C_FLAGS} /fsanitize=address /Oy-") else() # AppleClang and Clang message(STATUS "Address Sanitizer available on Clang") - set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} -fsanitize=address -O1 -fno-omit-frame-pointer") - set(CMAKE_C_FLAGS "${CMAKE_C_FLAGS} -fsanitize=address -O1 -fno-omit-frame-pointer") + set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} -fsanitize=address -fno-omit-frame-pointer") + set(CMAKE_C_FLAGS "${CMAKE_C_FLAGS} -fsanitize=address -fno-omit-frame-pointer") endif() elseif ("${CMAKE_CXX_COMPILER_ID}" STREQUAL "GNU") # GCC message(STATUS "Address Sanitizer available on GCC") - set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} -fsanitize=address -O1 -fno-omit-frame-pointer") - set(CMAKE_C_FLAGS "${CMAKE_C_FLAGS} -fsanitize=address -O1 -fno-omit-frame-pointer") + set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} -fsanitize=address -fno-omit-frame-pointer") + set(CMAKE_C_FLAGS "${CMAKE_C_FLAGS} -fsanitize=address -fno-omit-frame-pointer") link_libraries("asan") elseif ("${CMAKE_CXX_COMPILER_ID}" STREQUAL "MSVC") message(STATUS "Address Sanitizer available on MSVC") - set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} /fsanitize=address /O1 /Oy-") - set(CMAKE_C_FLAGS "${CMAKE_C_FLAGS} /fsanitize=address /O1 /Oy-") + set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} /fsanitize=address /Oy-") + set(CMAKE_C_FLAGS "${CMAKE_C_FLAGS} /fsanitize=address /Oy-") else() message(STATUS "Address Sanitizer not available on compiler ${CMAKE_CXX_COMPILER_ID}") endif() endif() + option(ENABLE_LTO "Enable Link Time Optimization" off) if(ENABLE_LTO) @@ -208,6 +216,18 @@ set(Launcher_SUBREDDIT_URL "https://prismlauncher.org/reddit" CACHE STRING "URL set(Launcher_FORCE_BUNDLED_LIBS OFF CACHE BOOL "Prevent using system libraries, if they are available as submodules") set(Launcher_QT_VERSION_MAJOR "6" CACHE STRING "Major Qt version to build against") +# Native libraries +if(UNIX AND APPLE) + set(Launcher_GLFW_LIBRARY_NAME "libglfw.dylib" CACHE STRING "Name of native glfw library") + set(Launcher_OPENAL_LIBRARY_NAME "libopenal.dylib" CACHE STRING "Name of native openal library") +elseif(UNIX) + set(Launcher_GLFW_LIBRARY_NAME "libglfw.so" CACHE STRING "Name of native glfw library") + set(Launcher_OPENAL_LIBRARY_NAME "libopenal.so" CACHE STRING "Name of native openal library") +elseif(WIN32) + set(Launcher_GLFW_LIBRARY_NAME "glfw.dll" CACHE STRING "Name of native glfw library") + set(Launcher_OPENAL_LIBRARY_NAME "OpenAL.dll" CACHE STRING "Name of native openal library") +endif() + # API Keys # NOTE: These API keys are here for convenience. If you rebrand this software or intend to break the terms of service # of these platforms, please change these API keys beforehand. diff --git a/buildconfig/BuildConfig.cpp.in b/buildconfig/BuildConfig.cpp.in index d7662a7a4..1eb0022b8 100644 --- a/buildconfig/BuildConfig.cpp.in +++ b/buildconfig/BuildConfig.cpp.in @@ -110,6 +110,9 @@ Config::Config() FLAME_API_KEY = "@Launcher_CURSEFORGE_API_KEY@"; META_URL = "@Launcher_META_URL@"; + GLFW_LIBRARY_NAME = "@Launcher_GLFW_LIBRARY_NAME@"; + OPENAL_LIBRARY_NAME = "@Launcher_OPENAL_LIBRARY_NAME@"; + BUG_TRACKER_URL = "@Launcher_BUG_TRACKER_URL@"; TRANSLATIONS_URL = "@Launcher_TRANSLATIONS_URL@"; MATRIX_URL = "@Launcher_MATRIX_URL@"; diff --git a/buildconfig/BuildConfig.h b/buildconfig/BuildConfig.h index 387f494f3..a5649b98f 100644 --- a/buildconfig/BuildConfig.h +++ b/buildconfig/BuildConfig.h @@ -134,6 +134,9 @@ class Config { */ QString META_URL; + QString GLFW_LIBRARY_NAME; + QString OPENAL_LIBRARY_NAME; + QString BUG_TRACKER_URL; QString TRANSLATIONS_URL; QString MATRIX_URL; diff --git a/cmake/CompilerWarnings.cmake b/cmake/CompilerWarnings.cmake new file mode 100644 index 000000000..69e57b4d5 --- /dev/null +++ b/cmake/CompilerWarnings.cmake @@ -0,0 +1,151 @@ +# +# Function to set compiler warnings with reasonable defaults at the project level. +# Taken from https://github.com/aminya/project_options/blob/main/src/CompilerWarnings.cmake +# under the folowing license: +# +# MIT License +# +# Copyright (c) 2022-2100 Amin Yahyaabadi +# +# Permission is hereby granted, free of charge, to any person obtaining a copy +# of this software and associated documentation files (the "Software"), to deal +# in the Software without restriction, including without limitation the rights +# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +# copies of the Software, and to permit persons to whom the Software is +# furnished to do so, subject to the following conditions: +# +# The above copyright notice and this permission notice shall be included in all +# copies or substantial portions of the Software. +# +# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +# SOFTWARE. +# + +include_guard() + +function(_set_project_warnings_add_target_link_option TARGET OPTIONS) + target_link_options(${_project_name} INTERFACE ${OPTIONS}) +endfunction() + +# Set the compiler warnings +# +# https://clang.llvm.org/docs/DiagnosticsReference.html +# https://github.com/lefticus/cppbestpractices/blob/master/02-Use_the_Tools_Available.md +function( + set_project_warnings + _project_name + MSVC_WARNINGS + CLANG_WARNINGS + GCC_WARNINGS +) + if("${MSVC_WARNINGS}" STREQUAL "") + set(MSVC_WARNINGS + /W4 # Baseline reasonable warnings + /w14242 # 'identifier': conversion from 'type1' to 'type1', possible loss of data + /w14254 # 'operator': conversion from 'type1:field_bits' to 'type2:field_bits', possible loss of data + /w14263 # 'function': member function does not override any base class virtual member function + /w14265 # 'classname': class has virtual functions, but destructor is not virtual instances of this class may not + # be destructed correctly + /w14287 # 'operator': unsigned/negative constant mismatch + /we4289 # nonstandard extension used: 'variable': loop control variable declared in the for-loop is used outside + # the for-loop scope + /w14296 # 'operator': expression is always 'boolean_value' + /w14311 # 'variable': pointer truncation from 'type1' to 'type2' + /w14545 # expression before comma evaluates to a function which is missing an argument list + /w14546 # function call before comma missing argument list + /w14547 # 'operator': operator before comma has no effect; expected operator with side-effect + /w14549 # 'operator': operator before comma has no effect; did you intend 'operator'? + /w14555 # expression has no effect; expected expression with side- effect + /w14619 # pragma warning: there is no warning number 'number' + /w14640 # Enable warning on thread un-safe static member initialization + /w14826 # Conversion from 'type1' to 'type_2' is sign-extended. This may cause unexpected runtime behavior. + /w14905 # wide string literal cast to 'LPSTR' + /w14906 # string literal cast to 'LPWSTR' + /w14928 # illegal copy-initialization; more than one user-defined conversion has been implicitly applied + /permissive- # standards conformance mode for MSVC compiler. + ) + endif() + + if("${CLANG_WARNINGS}" STREQUAL "") + set(CLANG_WARNINGS + -Wall + -Wextra # reasonable and standard + -Wshadow # warn the user if a variable declaration shadows one from a parent context + -Wnon-virtual-dtor # warn the user if a class with virtual functions has a non-virtual destructor. This helps + # catch hard to track down memory errors + -Wold-style-cast # warn for c-style casts + -Wcast-align # warn for potential performance problem casts + -Wunused # warn on anything being unused + -Woverloaded-virtual # warn if you overload (not override) a virtual function + -Wpedantic # warn if non-standard C++ is used + -Wconversion # warn on type conversions that may lose data + -Wsign-conversion # warn on sign conversions + -Wnull-dereference # warn if a null dereference is detected + -Wdouble-promotion # warn if float is implicit promoted to double + -Wformat=2 # warn on security issues around functions that format output (ie printf) + -Wimplicit-fallthrough # warn on statements that fallthrough without an explicit annotation + ) + endif() + + if("${GCC_WARNINGS}" STREQUAL "") + set(GCC_WARNINGS + ${CLANG_WARNINGS} + -Wmisleading-indentation # warn if indentation implies blocks where blocks do not exist + -Wduplicated-cond # warn if if / else chain has duplicated conditions + -Wduplicated-branches # warn if if / else branches have duplicated code + -Wlogical-op # warn about logical operations being used where bitwise were probably wanted + -Wuseless-cast # warn if you perform a cast to the same type + ) + endif() + + if(MSVC) + set(PROJECT_WARNINGS_CXX ${MSVC_WARNINGS}) + elseif(CMAKE_CXX_COMPILER_ID MATCHES ".*Clang") + set(PROJECT_WARNINGS_CXX ${CLANG_WARNINGS}) + elseif(CMAKE_CXX_COMPILER_ID STREQUAL "GNU") + set(PROJECT_WARNINGS_CXX ${GCC_WARNINGS}) + else() + message(AUTHOR_WARNING "No compiler warnings set for CXX compiler: '${CMAKE_CXX_COMPILER_ID}'") + # TODO support Intel compiler + endif() + + # Add C warnings + set(PROJECT_WARNINGS_C "${PROJECT_WARNINGS_CXX}") + list( + REMOVE_ITEM + PROJECT_WARNINGS_C + -Wnon-virtual-dtor + -Wold-style-cast + -Woverloaded-virtual + -Wuseless-cast + -Wextra-semi + ) + + target_compile_options( + ${_project_name} + INTERFACE # C++ warnings + $<$:${PROJECT_WARNINGS_CXX}> + # C warnings + $<$:${PROJECT_WARNINGS_C}> + ) + + # If we are using the compiler as a linker driver pass the warnings to it + # (most useful when using LTO or warnings as errors) + if(CMAKE_CXX_LINK_EXECUTABLE MATCHES "^") + _set_project_warnings_add_target_link_option( + ${_project_name} "$<$:${PROJECT_WARNINGS_CXX}>" + ) + endif() + + if(CMAKE_C_LINK_EXECUTABLE MATCHES "^") + _set_project_warnings_add_target_link_option( + ${_project_name} "$<$:${PROJECT_WARNINGS_C}>" + ) + endif() + + endfunction() diff --git a/cmake/MacOSXBundleInfo.plist.in b/cmake/MacOSXBundleInfo.plist.in index 400e482fe..d36ac3e8f 100644 --- a/cmake/MacOSXBundleInfo.plist.in +++ b/cmake/MacOSXBundleInfo.plist.in @@ -67,5 +67,16 @@ Alternate + CFBundleURLTypes + + + CFBundleURLName + Curseforge + CFBundleURLSchemes + + curseforge + + + diff --git a/flake.lock b/flake.lock index 3f1d7d25c..f96336551 100644 --- a/flake.lock +++ b/flake.lock @@ -21,11 +21,11 @@ "nixpkgs-lib": "nixpkgs-lib" }, "locked": { - "lastModified": 1690933134, - "narHash": "sha256-ab989mN63fQZBFrkk4Q8bYxQCktuHmBIBqUG1jl6/FQ=", + "lastModified": 1693611461, + "narHash": "sha256-aPODl8vAgGQ0ZYFIRisxYG5MOGSkIczvu2Cd8Gb9+1Y=", "owner": "hercules-ci", "repo": "flake-parts", - "rev": "59cf3f1447cfc75087e7273b04b31e689a8599fb", + "rev": "7f53fdb7bdc5bb237da7fefef12d099e4fd611ca", "type": "github" }, "original": { @@ -89,13 +89,28 @@ "type": "github" } }, + "nix-filter": { + "locked": { + "lastModified": 1694857738, + "narHash": "sha256-bxxNyLHjhu0N8T3REINXQ2ZkJco0ABFPn6PIe2QUfqo=", + "owner": "numtide", + "repo": "nix-filter", + "rev": "41fd48e00c22b4ced525af521ead8792402de0ea", + "type": "github" + }, + "original": { + "owner": "numtide", + "repo": "nix-filter", + "type": "github" + } + }, "nixpkgs": { "locked": { - "lastModified": 1691218994, - "narHash": "sha256-46GJ5vLf9H+Oh7Jii2gJI9GATJHGbx2iQpon5nUSFPI=", + "lastModified": 1695318763, + "narHash": "sha256-FHVPDRP2AfvsxAdc+AsgFJevMz5VBmnZglFUMlxBkcY=", "owner": "nixos", "repo": "nixpkgs", - "rev": "0d2fb29f5071a12d7983319c2c2576be6a130582", + "rev": "e12483116b3b51a185a33a272bf351e357ba9a99", "type": "github" }, "original": { @@ -108,11 +123,11 @@ "nixpkgs-lib": { "locked": { "dir": "lib", - "lastModified": 1690881714, - "narHash": "sha256-h/nXluEqdiQHs1oSgkOOWF+j8gcJMWhwnZ9PFabN6q0=", + "lastModified": 1693471703, + "narHash": "sha256-0l03ZBL8P1P6z8MaSDS/MvuU8E75rVxe5eE1N6gxeTo=", "owner": "NixOS", "repo": "nixpkgs", - "rev": "9e1960bc196baf6881340d53dccb203a951745a2", + "rev": "3e52e76b70d5508f3cec70b882a29199f4d1ee85", "type": "github" }, "original": { @@ -138,11 +153,11 @@ ] }, "locked": { - "lastModified": 1691256628, - "narHash": "sha256-M0YXHemR3zbyhM7PvJa5lzGhWVf6kM/fpZ4cWe/VIhI=", + "lastModified": 1694364351, + "narHash": "sha256-oadhSCqopYXxURwIA6/Anpe5IAG11q2LhvTJNP5zE6o=", "owner": "cachix", "repo": "pre-commit-hooks.nix", - "rev": "3139c4d1f7732cab89f06492bdd4677b877e3785", + "rev": "4f883a76282bc28eb952570afc3d8a1bf6f481d7", "type": "github" }, "original": { @@ -156,6 +171,7 @@ "flake-compat": "flake-compat", "flake-parts": "flake-parts", "libnbtplusplus": "libnbtplusplus", + "nix-filter": "nix-filter", "nixpkgs": "nixpkgs", "pre-commit-hooks": "pre-commit-hooks" } diff --git a/flake.nix b/flake.nix index c3148fe03..afb0ec63a 100644 --- a/flake.nix +++ b/flake.nix @@ -4,6 +4,7 @@ inputs = { nixpkgs.url = "github:nixos/nixpkgs/nixpkgs-unstable"; flake-parts.url = "github:hercules-ci/flake-parts"; + nix-filter.url = "github:numtide/nix-filter"; pre-commit-hooks = { url = "github:cachix/pre-commit-hooks.nix"; inputs.nixpkgs.follows = "nixpkgs"; @@ -20,8 +21,24 @@ }; }; - outputs = inputs: - inputs.flake-parts.lib.mkFlake - {inherit inputs;} - {imports = [./nix];}; + outputs = { + flake-parts, + pre-commit-hooks, + ... + } @ inputs: + flake-parts.lib.mkFlake {inherit inputs;} { + imports = [ + pre-commit-hooks.flakeModule + + ./nix/dev.nix + ./nix/distribution.nix + ]; + + systems = [ + "x86_64-linux" + "aarch64-linux" + "x86_64-darwin" + "aarch64-darwin" + ]; + }; } diff --git a/launcher/Application.cpp b/launcher/Application.cpp index 7c7fc7465..66044d9ac 100644 --- a/launcher/Application.cpp +++ b/launcher/Application.cpp @@ -9,7 +9,6 @@ * Copyright (C) 2022 Tayou * Copyright (C) 2023 TheKodeToad * Copyright (C) 2023 Rachel Powers <508861+Ryex@users.noreply.github.com> - * Copyright (C) 2023 seth * * 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 @@ -136,11 +135,7 @@ #endif #if defined Q_OS_WIN32 -#ifndef WIN32_LEAN_AND_MEAN -#define WIN32_LEAN_AND_MEAN -#endif -#include -#include +#include "WindowsConsole.h" #endif #define STRINGIFY(x) #x @@ -172,22 +167,8 @@ void appDebugOutput(QtMsgType type, const QMessageLogContext& context, const QSt Application::Application(int& argc, char** argv) : QApplication(argc, argv) { #if defined Q_OS_WIN32 - // attach the parent console - if (AttachConsole(ATTACH_PARENT_PROCESS)) { - // if attach succeeds, reopen and sync all the i/o - if (freopen("CON", "w", stdout)) { - std::cout.sync_with_stdio(); - } - if (freopen("CON", "w", stderr)) { - std::cerr.sync_with_stdio(); - } - if (freopen("CON", "r", stdin)) { - std::cin.sync_with_stdio(); - } - auto out = GetStdHandle(STD_OUTPUT_HANDLE); - DWORD written; - const char* endline = "\n"; - WriteConsole(out, endline, strlen(endline), &written, NULL); + // attach the parent console if stdout not already captured + if (AttachWindowsConsole()) { consoleAttached = true; } #endif @@ -212,8 +193,11 @@ Application::Application(int& argc, char** argv) : QApplication(argc, argv) { { "s", "server" }, "Join the specified server on launch (only valid in combination with --launch)", "address" }, { { "a", "profile" }, "Use the account specified by its profile name (only valid in combination with --launch)", "profile" }, { "alive", "Write a small '" + liveCheckFile + "' file after the launcher starts" }, - { { "I", "import" }, "Import instance from specified zip (local path or URL)", "file" }, + { { "I", "import" }, "Import instance or resource from specified local path or URL", "url" }, { "show", "Opens the window for the specified instance (by instance ID)", "show" } }); + // Has to be positional for some OS to handle that properly + parser.addPositionalArgument("URL", "Import the resource(s) at the given URL(s) (same as -I / --import)", "[URL...]"); + parser.addHelpOption(); parser.addVersionOption(); @@ -226,13 +210,13 @@ Application::Application(int& argc, char** argv) : QApplication(argc, argv) m_instanceIdToShowWindowOf = parser.value("show"); - for (auto zip_path : parser.values("import")) { - m_zipsToImport.append(QUrl::fromLocalFile(QFileInfo(zip_path).absoluteFilePath())); + for (auto url : parser.values("import")) { + m_urlsToImport.append(normalizeImportUrl(url)); } // treat unspecified positional arguments as import urls - for (auto zip_path : parser.positionalArguments()) { - m_zipsToImport.append(QUrl::fromLocalFile(QFileInfo(zip_path).absoluteFilePath())); + for (auto url : parser.positionalArguments()) { + m_urlsToImport.append(normalizeImportUrl(url)); } // error if --launch is missing with --server or --profile @@ -331,11 +315,11 @@ Application::Application(int& argc, char** argv) : QApplication(argc, argv) activate.command = "activate"; m_peerInstance->sendMessage(activate.serialize(), timeout); - if (!m_zipsToImport.isEmpty()) { - for (auto zip_url : m_zipsToImport) { + if (!m_urlsToImport.isEmpty()) { + for (auto url : m_urlsToImport) { ApplicationMessage import; import.command = "import"; - import.args.insert("path", zip_url.toString()); + import.args.insert("url", url.toString()); m_peerInstance->sendMessage(import.serialize(), timeout); } } @@ -510,7 +494,7 @@ Application::Application(int& argc, char** argv) : QApplication(argc, argv) m_settings.reset(new INISettingsObject({ BuildConfig.LAUNCHER_CONFIGFILE, "polymc.cfg", "multimc.cfg" }, this)); // Theming - m_settings->registerSetting("IconTheme", QString("pe_colored")); + m_settings->registerSetting("IconTheme", QString()); m_settings->registerSetting("ApplicationTheme", QString()); m_settings->registerSetting("BackgroundCat", QString("kitteh")); @@ -595,12 +579,11 @@ Application::Application(int& argc, char** argv) : QApplication(argc, argv) m_settings->registerSetting("IgnoreJavaCompatibility", false); m_settings->registerSetting("IgnoreJavaWizard", false); - // Mod loader settings - m_settings->registerSetting("DisableQuiltBeacon", false); - // Native library workarounds m_settings->registerSetting("UseNativeOpenAL", false); + m_settings->registerSetting("CustomOpenALPath", ""); m_settings->registerSetting("UseNativeGLFW", false); + m_settings->registerSetting("CustomGLFWPath", ""); // Peformance related options m_settings->registerSetting("EnableFeralGamemode", false); @@ -611,6 +594,7 @@ Application::Application(int& argc, char** argv) : QApplication(argc, argv) m_settings->registerSetting("ShowGameTime", true); m_settings->registerSetting("ShowGlobalGameTime", true); m_settings->registerSetting("RecordGameTime", true); + m_settings->registerSetting("ShowGameTimeWithoutDays", false); // Minecraft mods m_settings->registerSetting("ModMetadataDisabled", false); @@ -772,7 +756,7 @@ Application::Application(int& argc, char** argv) : QApplication(argc, argv) } // Themes - m_themeManager = std::make_unique(m_mainWindow); + m_themeManager = std::make_unique(); // initialize and load all instances { @@ -858,14 +842,15 @@ Application::Application(int& argc, char** argv) : QApplication(argc, argv) } }); - applyCurrentlySelectedTheme(true); - updateCapabilities(); + detectLibraries(); + if (createSetupWizard()) { return; } + m_themeManager->applyCurrentlySelectedTheme(true); performMainStartupAction(); } @@ -891,10 +876,20 @@ bool Application::createSetupWizard() }(); bool languageRequired = settings()->get("Language").toString().isEmpty(); bool pasteInterventionRequired = settings()->get("PastebinURL") != ""; - bool themeInterventionRequired = settings()->get("ApplicationTheme") == ""; + bool validWidgets = m_themeManager->isValidApplicationTheme(settings()->get("ApplicationTheme").toString()); + bool validIcons = m_themeManager->isValidIconTheme(settings()->get("IconTheme").toString()); + bool themeInterventionRequired = !validWidgets || !validIcons; bool wizardRequired = javaRequired || languageRequired || pasteInterventionRequired || themeInterventionRequired; if (wizardRequired) { + // set default theme after going into theme wizard + if (!validIcons) + settings()->set("IconTheme", QString("pe_colored")); + if (!validWidgets) + settings()->set("ApplicationTheme", QString("system")); + + m_themeManager->applyCurrentlySelectedTheme(true); + m_setupWizard = new SetupWizard(nullptr); if (languageRequired) { m_setupWizard->addPage(new LanguageWizardPage(m_setupWizard)); @@ -909,9 +904,9 @@ bool Application::createSetupWizard() } if (themeInterventionRequired) { - settings()->set("ApplicationTheme", QString("system")); // set default theme after going into theme wizard m_setupWizard->addPage(new ThemeWizardPage(m_setupWizard)); } + connect(m_setupWizard, &QDialog::finished, this, &Application::setupWizardFinished); m_setupWizard->show(); return true; @@ -970,7 +965,7 @@ void Application::performMainStartupAction() qDebug() << " Launching with account" << m_profileToUse; } - launch(inst, true, false, nullptr, serverToJoin, accountToUse); + launch(inst, true, false, serverToJoin, accountToUse); return; } } @@ -987,9 +982,9 @@ void Application::performMainStartupAction() showMainWindow(false); qDebug() << "<> Main window shown."; } - if (!m_zipsToImport.isEmpty()) { - qDebug() << "<> Importing from zip:" << m_zipsToImport; - m_mainWindow->processURLs(m_zipsToImport); + if (!m_urlsToImport.isEmpty()) { + qDebug() << "<> Importing from url:" << m_urlsToImport; + m_mainWindow->processURLs(m_urlsToImport); } } @@ -1031,12 +1026,12 @@ void Application::messageReceived(const QByteArray& message) if (command == "activate") { showMainWindow(); } else if (command == "import") { - QString path = received.args["path"]; - if (path.isEmpty()) { + QString url = received.args["url"]; + if (url.isEmpty()) { qWarning() << "Received" << command << "message without a zip path/URL."; return; } - m_mainWindow->processURLs({ QUrl::fromLocalFile(QFileInfo(path).absoluteFilePath()) }); + m_mainWindow->processURLs({ normalizeImportUrl(url) }); } else if (command == "launch") { QString id = received.args["id"]; QString server = received.args["server"]; @@ -1069,7 +1064,7 @@ void Application::messageReceived(const QByteArray& message) } } - launch(instance, true, false, nullptr, serverObject, accountObject); + launch(instance, true, false, serverObject, accountObject); } else { qWarning() << "Received invalid message" << message; } @@ -1088,26 +1083,6 @@ std::shared_ptr Application::javalist() return m_javalist; } -QList Application::getValidApplicationThemes() -{ - return m_themeManager->getValidApplicationThemes(); -} - -void Application::applyCurrentlySelectedTheme(bool initial) -{ - m_themeManager->applyCurrentlySelectedTheme(initial); -} - -void Application::setApplicationTheme(const QString& name) -{ - m_themeManager->setApplicationTheme(name); -} - -void Application::setIconTheme(const QString& name) -{ - m_themeManager->setIconTheme(name); -} - QIcon Application::getThemedIcon(const QString& name) { if (name == "logo") { @@ -1116,16 +1091,6 @@ QIcon Application::getThemedIcon(const QString& name) return QIcon::fromTheme(name); } -QList Application::getValidCatPacks() -{ - return m_themeManager->getValidCatPacks(); -} - -QString Application::getCatPack(QString catName) -{ - return m_themeManager->getCatPack(catName); -} - bool Application::openJsonEditor(const QString& filename) { const QString file = QDir::current().absoluteFilePath(filename); @@ -1140,7 +1105,6 @@ bool Application::openJsonEditor(const QString& filename) bool Application::launch(InstancePtr instance, bool online, bool demo, - BaseProfilerFactory* profiler, MinecraftServerTargetPtr serverToJoin, MinecraftAccountPtr accountToUse) { @@ -1148,7 +1112,7 @@ bool Application::launch(InstancePtr instance, qDebug() << "Cannot launch instances while an update is running. Please try again when updates are completed."; } else if (instance->canLaunch()) { auto& extras = m_instanceExtras[instance->id()]; - auto& window = extras.window; + auto window = extras.window; if (window) { if (!window->saveAll()) { return false; @@ -1159,7 +1123,7 @@ bool Application::launch(InstancePtr instance, controller->setInstance(instance); controller->setOnline(online); controller->setDemo(demo); - controller->setProfiler(profiler); + controller->setProfiler(profilers().value(instance->settings()->get("Profiler").toString(), nullptr).get()); controller->setServerToJoin(serverToJoin); controller->setAccountToUse(accountToUse); if (window) { @@ -1451,6 +1415,15 @@ void Application::updateCapabilities() #endif } +void Application::detectLibraries() +{ +#ifdef Q_OS_LINUX + m_detectedGLFWPath = MangoHud::findLibrary(BuildConfig.GLFW_LIBRARY_NAME); + m_detectedOpenALPath = MangoHud::findLibrary(BuildConfig.OPENAL_LIBRARY_NAME); + qDebug() << "Detected native libraries:" << m_detectedGLFWPath << m_detectedOpenALPath; +#endif +} + QString Application::getJarPath(QString jarFile) { QStringList potentialPaths = { @@ -1629,3 +1602,13 @@ void Application::triggerUpdateCheck() qDebug() << "Updater not available."; } } + +QUrl Application::normalizeImportUrl(QString const& url) +{ + auto local_file = QFileInfo(url); + if (local_file.exists()) { + return QUrl::fromLocalFile(local_file.absoluteFilePath()); + } else { + return QUrl::fromUserInput(url); + } +} diff --git a/launcher/Application.h b/launcher/Application.h index cf7967a56..b227bb813 100644 --- a/launcher/Application.h +++ b/launcher/Application.h @@ -71,6 +71,7 @@ class TranslationsModel; class ITheme; class MCEditTool; class ThemeManager; +class IconTheme; namespace Meta { class Index; @@ -109,17 +110,7 @@ class Application : public QApplication { QIcon getThemedIcon(const QString& name); - void setIconTheme(const QString& name); - - void applyCurrentlySelectedTheme(bool initial = false); - - QList getValidApplicationThemes(); - - void setApplicationTheme(const QString& name); - - QList getValidCatPacks(); - - QString getCatPack(QString catName = ""); + ThemeManager* themeManager() { return m_themeManager.get(); } shared_qobject_ptr updater() { return m_updater; } @@ -151,6 +142,8 @@ class Application : public QApplication { void updateCapabilities(); + void detectLibraries(); + /*! * Finds and returns the full path to a jar file. * Returns a null-string if it could not be found. @@ -186,6 +179,8 @@ class Application : public QApplication { int suitableMaxMem(); + QUrl normalizeImportUrl(QString const& url); + signals: void updateAllowedChanged(bool status); void globalSettingsAboutToOpen(); @@ -200,7 +195,6 @@ class Application : public QApplication { bool launch(InstancePtr instance, bool online = true, bool demo = false, - BaseProfilerFactory* profiler = nullptr, MinecraftServerTargetPtr serverToJoin = nullptr, MinecraftAccountPtr accountToUse = nullptr); bool kill(InstancePtr instance); @@ -284,11 +278,13 @@ class Application : public QApplication { SetupWizard* m_setupWizard = nullptr; public: + QString m_detectedGLFWPath; + QString m_detectedOpenALPath; QString m_instanceIdToLaunch; QString m_serverToJoin; QString m_profileToUse; bool m_liveCheck = false; - QList m_zipsToImport; + QList m_urlsToImport; QString m_instanceIdToShowWindowOf; std::unique_ptr logFile; }; diff --git a/launcher/BaseInstance.cpp b/launcher/BaseInstance.cpp index 70c6da6b3..725036395 100644 --- a/launcher/BaseInstance.cpp +++ b/launcher/BaseInstance.cpp @@ -3,6 +3,7 @@ * Prism Launcher - Minecraft Launcher * Copyright (C) 2022 Sefa Eyeoglu * Copyright (c) 2022 Jamie Mansfield + * Copyright (C) 2023 TheKodeToad * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by @@ -100,6 +101,8 @@ BaseInstance::BaseInstance(SettingsObjectPtr globalSettings, SettingsObjectPtr s m_settings->registerSetting("ManagedPackName", ""); m_settings->registerSetting("ManagedPackVersionID", ""); m_settings->registerSetting("ManagedPackVersionName", ""); + + m_settings->registerSetting("Profiler", ""); } QString BaseInstance::getPreLaunchCommand() diff --git a/launcher/BaseInstance.h b/launcher/BaseInstance.h index 4357125f3..47fff5957 100644 --- a/launcher/BaseInstance.h +++ b/launcher/BaseInstance.h @@ -3,6 +3,7 @@ * Prism Launcher - Minecraft Launcher * Copyright (C) 2022 Sefa Eyeoglu * Copyright (c) 2022 Jamie Mansfield + * Copyright (C) 2023 TheKodeToad * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by @@ -38,6 +39,7 @@ #include #include +#include #include #include #include @@ -86,7 +88,7 @@ class BaseInstance : public QObject, public std::enable_shared_from_this(BaseVersion& a) { return name() > a.name(); }; + virtual bool operator<(BaseVersion& a) { return name() < a.name(); } + virtual bool operator>(BaseVersion& a) { return name() > a.name(); } }; Q_DECLARE_METATYPE(BaseVersion::Ptr) diff --git a/launcher/CMakeLists.txt b/launcher/CMakeLists.txt index 824cf80eb..18e0acab1 100644 --- a/launcher/CMakeLists.txt +++ b/launcher/CMakeLists.txt @@ -136,6 +136,15 @@ set(NET_SOURCES net/Validator.h net/Upload.cpp net/Upload.h + net/HeaderProxy.h + net/RawHeaderProxy.h + net/ApiHeaderProxy.h + net/ApiDownload.h + net/ApiDownload.cpp + net/ApiUpload.cpp + net/ApiUpload.h + net/NetRequest.cpp + net/NetRequest.h ) # Game launch logic @@ -566,6 +575,9 @@ set(ATLAUNCHER_SOURCES ) set(LINKEXE_SOURCES + WindowsConsole.cpp + WindowsConsole.h + filelink/FileLink.h filelink/FileLink.cpp FileSystem.h @@ -762,6 +774,8 @@ SET(LAUNCHER_SOURCES ui/themes/ITheme.h ui/themes/SystemTheme.cpp ui/themes/SystemTheme.h + ui/themes/IconTheme.cpp + ui/themes/IconTheme.h ui/themes/ThemeManager.cpp ui/themes/ThemeManager.h ui/themes/CatPack.cpp @@ -962,6 +976,8 @@ SET(LAUNCHER_SOURCES ui/dialogs/ChooseProviderDialog.cpp ui/dialogs/ModUpdateDialog.cpp ui/dialogs/ModUpdateDialog.h + ui/dialogs/InstallLoaderDialog.cpp + ui/dialogs/InstallLoaderDialog.h # GUI - widgets ui/widgets/Common.cpp @@ -1026,6 +1042,14 @@ SET(LAUNCHER_SOURCES ui/instanceview/VisualGroup.h ) +if(WIN32) + set(LAUNCHER_SOURCES + WindowsConsole.cpp + WindowsConsole.h + ${LAUNCHER_SOURCES} + ) +endif() + qt_wrap_ui(LAUNCHER_UI ui/MainWindow.ui ui/setupwizard/PasteWizardPage.ui @@ -1109,8 +1133,15 @@ if(WIN32) set(LAUNCHER_RCS ${CMAKE_CURRENT_BINARY_DIR}/../${Launcher_Branding_WindowsRC}) endif() +include(CompilerWarnings) + # Add executable add_library(Launcher_logic STATIC ${LOGIC_SOURCES} ${LAUNCHER_SOURCES} ${LAUNCHER_UI} ${LAUNCHER_RESOURCES}) +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_link_libraries(Launcher_logic systeminfo @@ -1195,6 +1226,11 @@ install(TARGETS ${Launcher_Name} if(WIN32) add_library(filelink_logic STATIC ${LINKEXE_SOURCES}) + set_project_warnings(filelink_logic + "${Launcher_MSVC_WARNINGS}" + "${Launcher_CLANG_WARNINGS}" + "${Launcher_GCC_WARNINGS}") + target_include_directories(filelink_logic PUBLIC ${CMAKE_CURRENT_SOURCE_DIR}) target_link_libraries(filelink_logic systeminfo diff --git a/launcher/DesktopServices.cpp b/launcher/DesktopServices.cpp index 710487349..004e5e085 100644 --- a/launcher/DesktopServices.cpp +++ b/launcher/DesktopServices.cpp @@ -96,12 +96,12 @@ bool IndirectOpen(T callable, qint64* pid_forked = nullptr) #endif namespace DesktopServices { -bool openDirectory(const QString& path, bool ensureExists) +bool openDirectory(const QString& path, [[maybe_unused]] bool ensureExists) { qDebug() << "Opening directory" << path; QDir parentPath; QDir dir(path); - if (!dir.exists()) { + if (ensureExists && !dir.exists()) { parentPath.mkpath(dir.absolutePath()); } auto f = [&]() { return QDesktopServices::openUrl(QUrl::fromLocalFile(dir.absolutePath())); }; diff --git a/launcher/FileIgnoreProxy.cpp b/launcher/FileIgnoreProxy.cpp index 4c8c64c72..df06c3c75 100644 --- a/launcher/FileIgnoreProxy.cpp +++ b/launcher/FileIgnoreProxy.cpp @@ -267,10 +267,7 @@ bool FileIgnoreProxy::filterAcceptsRow(int sourceRow, const QModelIndex& sourceP bool FileIgnoreProxy::ignoreFile(QFileInfo fileInfo) const { - auto fileName = fileInfo.fileName(); - auto path = relPath(fileInfo.absoluteFilePath()); - return std::any_of(m_ignoreFiles.cbegin(), m_ignoreFiles.cend(), [fileName](auto iFileName) { return fileName == iFileName; }) || - m_ignoreFilePaths.covers(path); + return m_ignoreFiles.contains(fileInfo.fileName()) || m_ignoreFilePaths.covers(relPath(fileInfo.absoluteFilePath())); } bool FileIgnoreProxy::filterFile(const QString& fileName) const diff --git a/launcher/FileSystem.h b/launcher/FileSystem.h index 85e2cbe00..bfed576c1 100644 --- a/launcher/FileSystem.h +++ b/launcher/FileSystem.h @@ -112,8 +112,8 @@ class copy : public QObject { bool operator()(bool dryRun = false) { return operator()(QString(), dryRun); } - int totalCopied() { return m_copied; } - int totalFailed() { return m_failedPaths.length(); } + qsizetype totalCopied() { return m_copied; } + qsizetype totalFailed() { return m_failedPaths.length(); } QStringList failed() { return m_failedPaths; } signals: @@ -130,7 +130,7 @@ class copy : public QObject { bool m_whitelist = false; QDir m_src; QDir m_dst; - int m_copied; + qsizetype m_copied; QStringList m_failedPaths; }; @@ -474,8 +474,8 @@ class clone : public QObject { bool operator()(bool dryRun = false) { return operator()(QString(), dryRun); } - int totalCloned() { return m_cloned; } - int totalFailed() { return m_failedClones.length(); } + qsizetype totalCloned() { return m_cloned; } + qsizetype totalFailed() { return m_failedClones.length(); } QList> failed() { return m_failedClones; } @@ -491,7 +491,7 @@ class clone : public QObject { bool m_whitelist = false; QDir m_src; QDir m_dst; - int m_cloned; + qsizetype m_cloned; QList> m_failedClones; }; diff --git a/launcher/Filter.cpp b/launcher/Filter.cpp index 9d9044734..fc1c42344 100644 --- a/launcher/Filter.cpp +++ b/launcher/Filter.cpp @@ -16,6 +16,12 @@ bool ExactFilter::accepts(const QString& value) return value == pattern; } +ExactIfPresentFilter::ExactIfPresentFilter(const QString& pattern) : pattern(pattern) {} +bool ExactIfPresentFilter::accepts(const QString& value) +{ + return value.isEmpty() || value == pattern; +} + RegexpFilter::RegexpFilter(const QString& regexp, bool invert) : invert(invert) { pattern.setPattern(regexp); diff --git a/launcher/Filter.h b/launcher/Filter.h index f58521cf2..089c844d4 100644 --- a/launcher/Filter.h +++ b/launcher/Filter.h @@ -29,6 +29,16 @@ class ExactFilter : public Filter { QString pattern; }; +class ExactIfPresentFilter : public Filter { + public: + ExactIfPresentFilter(const QString& pattern); + ~ExactIfPresentFilter() override = default; + bool accepts(const QString& value) override; + + private: + QString pattern; +}; + class RegexpFilter : public Filter { public: RegexpFilter(const QString& regexp, bool invert); diff --git a/launcher/InstanceImportTask.cpp b/launcher/InstanceImportTask.cpp index 97e106ba6..bc139e4f1 100644 --- a/launcher/InstanceImportTask.cpp +++ b/launcher/InstanceImportTask.cpp @@ -50,6 +50,9 @@ #include "modplatform/technic/TechnicPackProcessor.h" #include "settings/INISettingsObject.h" +#include "tasks/Task.h" + +#include "net/ApiDownload.h" #include #include @@ -88,25 +91,27 @@ void InstanceImportTask::executeTask() setStatus(tr("Downloading modpack:\n%1").arg(m_sourceUrl.toString())); m_downloadRequired = true; - const QString path(m_sourceUrl.host() + '/' + m_sourceUrl.path()); - - auto entry = APPLICATION->metacache()->resolveEntry("general", path); - entry->setStale(true); - m_archivePath = entry->getFullPath(); - - m_filesNetJob.reset(new NetJob(tr("Modpack download"), APPLICATION->network())); - m_filesNetJob->addNetAction(Net::Download::makeCached(m_sourceUrl, entry)); - - connect(m_filesNetJob.get(), &NetJob::succeeded, this, &InstanceImportTask::downloadSucceeded); - connect(m_filesNetJob.get(), &NetJob::progress, this, &InstanceImportTask::downloadProgressChanged); - connect(m_filesNetJob.get(), &NetJob::stepProgress, this, &InstanceImportTask::propagateStepProgress); - connect(m_filesNetJob.get(), &NetJob::failed, this, &InstanceImportTask::downloadFailed); - connect(m_filesNetJob.get(), &NetJob::aborted, this, &InstanceImportTask::downloadAborted); - - m_filesNetJob->start(); + downloadFromUrl(); } } +void InstanceImportTask::downloadFromUrl() +{ + const QString path = m_sourceUrl.host() + '/' + m_sourceUrl.path(); + auto entry = APPLICATION->metacache()->resolveEntry("general", path); + entry->setStale(true); + m_filesNetJob.reset(new NetJob(tr("Modpack download"), APPLICATION->network())); + m_filesNetJob->addNetAction(Net::ApiDownload::makeCached(m_sourceUrl, entry)); + m_archivePath = entry->getFullPath(); + + connect(m_filesNetJob.get(), &NetJob::succeeded, this, &InstanceImportTask::downloadSucceeded); + connect(m_filesNetJob.get(), &NetJob::progress, this, &InstanceImportTask::downloadProgressChanged); + connect(m_filesNetJob.get(), &NetJob::stepProgress, this, &InstanceImportTask::propagateStepProgress); + connect(m_filesNetJob.get(), &NetJob::failed, this, &InstanceImportTask::downloadFailed); + connect(m_filesNetJob.get(), &NetJob::aborted, this, &InstanceImportTask::downloadAborted); + m_filesNetJob->start(); +} + void InstanceImportTask::downloadSucceeded() { processZipPack(); diff --git a/launcher/InstanceImportTask.h b/launcher/InstanceImportTask.h index 4459e440c..ca3d30ad6 100644 --- a/launcher/InstanceImportTask.h +++ b/launcher/InstanceImportTask.h @@ -101,4 +101,5 @@ class InstanceImportTask : public InstanceTask { // FIXME: nuke QWidget* m_parent; + void downloadFromUrl(); }; diff --git a/launcher/InstanceList.cpp b/launcher/InstanceList.cpp index 029384091..856eee816 100644 --- a/launcher/InstanceList.cpp +++ b/launcher/InstanceList.cpp @@ -96,7 +96,11 @@ Qt::DropActions InstanceList::supportedDropActions() const return Qt::MoveAction; } -bool InstanceList::canDropMimeData(const QMimeData* data, Qt::DropAction action, int row, int column, const QModelIndex& parent) const +bool InstanceList::canDropMimeData(const QMimeData* data, + [[maybe_unused]] Qt::DropAction action, + [[maybe_unused]] int row, + [[maybe_unused]] int column, + [[maybe_unused]] const QModelIndex& parent) const { if (data && data->hasFormat("application/x-instanceid")) { return true; @@ -104,7 +108,11 @@ bool InstanceList::canDropMimeData(const QMimeData* data, Qt::DropAction action, return false; } -bool InstanceList::dropMimeData(const QMimeData* data, Qt::DropAction action, int row, int column, const QModelIndex& parent) +bool InstanceList::dropMimeData(const QMimeData* data, + [[maybe_unused]] Qt::DropAction action, + [[maybe_unused]] int row, + [[maybe_unused]] int column, + [[maybe_unused]] const QModelIndex& parent) { if (data && data->hasFormat("application/x-instanceid")) { return true; @@ -751,7 +759,7 @@ void InstanceList::instanceDirContentsChanged(const QString& path) emit instancesChanged(); } -void InstanceList::on_InstFolderChanged(const Setting& setting, QVariant value) +void InstanceList::on_InstFolderChanged([[maybe_unused]] const Setting& setting, QVariant value) { QString newInstDir = QDir(value.toString()).canonicalPath(); if (newInstDir != m_instDir) { @@ -789,7 +797,7 @@ class InstanceStaging : public Task { , m_groupName(std::move(groupName)) { m_child.reset(child); - connect(child, &Task::succeeded, this, &InstanceStaging::childSucceded); + connect(child, &Task::succeeded, this, &InstanceStaging::childSucceeded); connect(child, &Task::failed, this, &InstanceStaging::childFailed); connect(child, &Task::aborted, this, &InstanceStaging::childAborted); connect(child, &Task::abortStatusChanged, this, &InstanceStaging::setAbortable); @@ -797,7 +805,7 @@ class InstanceStaging : public Task { connect(child, &Task::details, this, &InstanceStaging::setDetails); connect(child, &Task::progress, this, &InstanceStaging::setProgress); connect(child, &Task::stepProgress, this, &InstanceStaging::propagateStepProgress); - connect(&m_backoffTimer, &QTimer::timeout, this, &InstanceStaging::childSucceded); + connect(&m_backoffTimer, &QTimer::timeout, this, &InstanceStaging::childSucceeded); } virtual ~InstanceStaging(){}; @@ -819,7 +827,7 @@ class InstanceStaging : public Task { QStringList warnings() const override { return m_child->warnings(); } private slots: - void childSucceded() + void childSucceeded() { unsigned sleepTime = backoff(); if (m_parent->commitStagedInstance(m_stagingPath, m_instance_name, m_groupName, *m_child.get())) { diff --git a/launcher/LaunchController.cpp b/launcher/LaunchController.cpp index 380489e06..b21eb281f 100644 --- a/launcher/LaunchController.cpp +++ b/launcher/LaunchController.cpp @@ -2,6 +2,7 @@ /* * Prism Launcher - Minecraft Launcher * Copyright (C) 2022 Sefa Eyeoglu + * Copyright (C) 2023 TheKodeToad * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by @@ -361,22 +362,21 @@ void LaunchController::readyForLaunch() QString error; if (!m_profiler->check(&error)) { m_launcher->abort(); - QMessageBox::critical(m_parentWidget, tr("Error!"), tr("Couldn't start profiler: %1").arg(error)); emitFailed("Profiler startup failed!"); + QMessageBox::critical(m_parentWidget, tr("Error!"), tr("Profiler check for %1 failed: %2").arg(m_profiler->name(), error)); return; } BaseProfiler* profilerInstance = m_profiler->createProfiler(m_launcher->instance(), this); connect(profilerInstance, &BaseProfiler::readyToLaunch, [this](const QString& message) { - QMessageBox msg; + QMessageBox msg(m_parentWidget); msg.setText(tr("The game launch is delayed until you press the " "button. This is the right time to setup the profiler, as the " "profiler server is running now.\n\n%1") .arg(message)); msg.setWindowTitle(tr("Waiting.")); msg.setIcon(QMessageBox::Information); - msg.addButton(tr("Launch"), QMessageBox::AcceptRole); - msg.setModal(true); + msg.addButton(tr("&Launch"), QMessageBox::AcceptRole); msg.exec(); m_launcher->proceed(); }); diff --git a/launcher/LoggedProcess.cpp b/launcher/LoggedProcess.cpp index 6978777e1..fadd64e68 100644 --- a/launcher/LoggedProcess.cpp +++ b/launcher/LoggedProcess.cpp @@ -65,14 +65,9 @@ QStringList LoggedProcess::reprocess(const QByteArray& data, QTextDecoder& decod m_leftover_line = ""; } -#if QT_VERSION < QT_VERSION_CHECK(5, 14, 0) - auto lines = str.remove(QChar::CarriageReturn).split(QChar::LineFeed, QString::SkipEmptyParts); -#else - auto lines = str.remove(QChar::CarriageReturn).split(QChar::LineFeed, Qt::SkipEmptyParts); -#endif + auto lines = str.remove(QChar::CarriageReturn).split(QChar::LineFeed); - if (!str.endsWith(QChar::LineFeed)) - m_leftover_line = lines.takeLast(); + m_leftover_line = lines.takeLast(); return lines; } diff --git a/launcher/MMCTime.cpp b/launcher/MMCTime.cpp index 3972dbd53..1765fd844 100644 --- a/launcher/MMCTime.cpp +++ b/launcher/MMCTime.cpp @@ -16,19 +16,20 @@ */ #include +#include #include #include #include -QString Time::prettifyDuration(int64_t duration) +QString Time::prettifyDuration(int64_t duration, bool noDays) { int seconds = (int)(duration % 60); duration /= 60; int minutes = (int)(duration % 60); duration /= 60; - int hours = (int)(duration % 24); - int days = (int)(duration / 24); + int hours = (int)(noDays ? duration : (duration % 24)); + int days = (int)(noDays ? 0 : (duration / 24)); if ((hours == 0) && (days == 0)) { return QObject::tr("%1min %2s").arg(minutes).arg(seconds); } diff --git a/launcher/MMCTime.h b/launcher/MMCTime.h index b7d34b5d8..ea6d37e7e 100644 --- a/launcher/MMCTime.h +++ b/launcher/MMCTime.h @@ -20,7 +20,7 @@ namespace Time { -QString prettifyDuration(int64_t duration); +QString prettifyDuration(int64_t duration, bool noDays = false); /** * @brief Returns a string with short form time duration ie. `2days 1h3m4s56.0ms`. diff --git a/launcher/MangoHud.cpp b/launcher/MangoHud.cpp index 5758da3aa..ab79f418b 100644 --- a/launcher/MangoHud.cpp +++ b/launcher/MangoHud.cpp @@ -16,6 +16,7 @@ * along with this program. If not, see . */ +#include #include #include #include @@ -26,6 +27,15 @@ #include "Json.h" #include "MangoHud.h" +#ifdef __GLIBC__ +#ifndef _GNU_SOURCE +#define _GNU_SOURCE +#define UNDEF_GNU_SOURCE +#endif +#include +#include +#endif + namespace MangoHud { QString getLibraryString() @@ -106,4 +116,37 @@ QString getLibraryString() return QString(); } + +QString findLibrary(QString libName) +{ +#ifdef __GLIBC__ + const char* library = libName.toLocal8Bit().constData(); + + void* handle = dlopen(library, RTLD_NOW); + if (!handle) { + qCritical() << "dlopen() failed:" << dlerror(); + return {}; + } + + char path[PATH_MAX]; + if (dlinfo(handle, RTLD_DI_ORIGIN, path) == -1) { + qCritical() << "dlinfo() failed:" << dlerror(); + dlclose(handle); + return {}; + } + + auto fullPath = FS::PathCombine(QString(path), libName); + + dlclose(handle); + return fullPath; +#else + qWarning() << "MangoHud::findLibrary is not implemented on this platform"; + return {}; +#endif +} } // namespace MangoHud + +#ifdef UNDEF_GNU_SOURCE +#undef _GNU_SOURCE +#undef UNDEF_GNU_SOURCE +#endif diff --git a/launcher/MangoHud.h b/launcher/MangoHud.h index 7b7c2849c..5361999b4 100644 --- a/launcher/MangoHud.h +++ b/launcher/MangoHud.h @@ -24,4 +24,6 @@ namespace MangoHud { QString getLibraryString(); -} + +QString findLibrary(QString libName); +} // namespace MangoHud diff --git a/launcher/NullInstance.h b/launcher/NullInstance.h index a332d4c49..c79600e7d 100644 --- a/launcher/NullInstance.h +++ b/launcher/NullInstance.h @@ -2,6 +2,7 @@ /* * Prism Launcher - Minecraft Launcher * Copyright (C) 2022 Sefa Eyeoglu + * Copyright (C) 2023 TheKodeToad * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by @@ -52,7 +53,7 @@ class NullInstance : public BaseInstance { QSet traits() const override { return {}; }; QString instanceConfigFolder() const override { return instanceRoot(); }; shared_qobject_ptr createLaunchTask(AuthSessionPtr, MinecraftServerTargetPtr) override { return nullptr; } - shared_qobject_ptr createUpdateTask(Net::Mode mode) override { return nullptr; } + shared_qobject_ptr createUpdateTask([[maybe_unused]] Net::Mode mode) override { return nullptr; } QProcessEnvironment createEnvironment() override { return QProcessEnvironment(); } QProcessEnvironment createLaunchEnvironment() override { return QProcessEnvironment(); } QMap getVariables() override { return QMap(); } @@ -62,6 +63,7 @@ class NullInstance : public BaseInstance { bool canExport() const override { return false; } bool canEdit() const override { return false; } bool canLaunch() const override { return false; } + void populateLaunchMenu(QMenu* menu) override {} QStringList verboseDescription(AuthSessionPtr session, MinecraftServerTargetPtr serverToJoin) override { QStringList out; diff --git a/launcher/ProblemProvider.h b/launcher/ProblemProvider.h index 246a8b5c6..9d1b0004e 100644 --- a/launcher/ProblemProvider.h +++ b/launcher/ProblemProvider.h @@ -12,7 +12,7 @@ struct PatchProblem { class ProblemProvider { public: - virtual ~ProblemProvider(){}; + virtual ~ProblemProvider() {} virtual const QList getProblems() const = 0; virtual ProblemSeverity getProblemSeverity() const = 0; }; diff --git a/launcher/RecursiveFileSystemWatcher.cpp b/launcher/RecursiveFileSystemWatcher.cpp index f3be1bd8f..8b28a03f1 100644 --- a/launcher/RecursiveFileSystemWatcher.cpp +++ b/launcher/RecursiveFileSystemWatcher.cpp @@ -90,7 +90,7 @@ void RecursiveFileSystemWatcher::fileChange(const QString& path) { emit fileChanged(path); } -void RecursiveFileSystemWatcher::directoryChange(const QString& path) +void RecursiveFileSystemWatcher::directoryChange([[maybe_unused]] const QString& path) { setFiles(scanRecursive(m_root)); } diff --git a/launcher/ResourceDownloadTask.cpp b/launcher/ResourceDownloadTask.cpp index 8bb9b64e9..a02151ca1 100644 --- a/launcher/ResourceDownloadTask.cpp +++ b/launcher/ResourceDownloadTask.cpp @@ -24,6 +24,8 @@ #include "minecraft/mod/ModFolderModel.h" #include "minecraft/mod/ResourceFolderModel.h" +#include "net/ApiDownload.h" + ResourceDownloadTask::ResourceDownloadTask(ModPlatform::IndexedPack::Ptr pack, ModPlatform::IndexedVersion version, const std::shared_ptr packs, @@ -51,7 +53,7 @@ ResourceDownloadTask::ResourceDownloadTask(ModPlatform::IndexedPack::Ptr pack, } } - m_filesNetJob->addNetAction(Net::Download::makeFile(m_pack_version.downloadUrl, dir.absoluteFilePath(getFilename()))); + m_filesNetJob->addNetAction(Net::ApiDownload::makeFile(m_pack_version.downloadUrl, dir.absoluteFilePath(getFilename()))); connect(m_filesNetJob.get(), &NetJob::succeeded, this, &ResourceDownloadTask::downloadSucceeded); connect(m_filesNetJob.get(), &NetJob::progress, this, &ResourceDownloadTask::downloadProgressChanged); connect(m_filesNetJob.get(), &NetJob::stepProgress, this, &ResourceDownloadTask::propagateStepProgress); diff --git a/launcher/SkinUtils.cpp b/launcher/SkinUtils.cpp index 20d3b52e7..989114ad5 100644 --- a/launcher/SkinUtils.cpp +++ b/launcher/SkinUtils.cpp @@ -35,6 +35,11 @@ QPixmap getFaceFromCache(QString username, int height, int width) QPixmap skinTexture(fskin.fileName()); if (!skinTexture.isNull()) { QPixmap skin = QPixmap(8, 8); +#if QT_VERSION >= QT_VERSION_CHECK(5, 14, 0) + skin.fill(QColorConstants::Transparent); +#else + skin.fill(QColor(0, 0, 0, 0)); +#endif QPainter painter(&skin); painter.drawPixmap(0, 0, skinTexture.copy(8, 8, 8, 8)); painter.drawPixmap(0, 0, skinTexture.copy(40, 8, 8, 8)); diff --git a/launcher/Usable.h b/launcher/Usable.h index a75415aa6..b0ecd4018 100644 --- a/launcher/Usable.h +++ b/launcher/Usable.h @@ -16,6 +16,8 @@ class Usable { friend class UseLock; public: + virtual ~Usable() {} + std::size_t useCount() const { return m_useCount; } bool isInUse() const { return m_useCount > 0; } diff --git a/launcher/Version.h b/launcher/Version.h index 101a907cb..9c043b013 100644 --- a/launcher/Version.h +++ b/launcher/Version.h @@ -63,7 +63,7 @@ class Version { struct Section { explicit Section(QString fullString) : m_fullString(std::move(fullString)) { - int cutoff = m_fullString.size(); + qsizetype cutoff = m_fullString.size(); for (int i = 0; i < m_fullString.size(); i++) { if (!m_fullString[i].isDigit()) { cutoff = i; @@ -103,14 +103,8 @@ class Version { QString m_fullString; - [[nodiscard]] inline bool isAppendix() const - { - return m_stringPart.startsWith('+'); - } - [[nodiscard]] inline bool isPreRelease() const - { - return m_stringPart.startsWith('-') && m_stringPart.length() > 1; - } + [[nodiscard]] inline bool isAppendix() const { return m_stringPart.startsWith('+'); } + [[nodiscard]] inline bool isPreRelease() const { return m_stringPart.startsWith('-') && m_stringPart.length() > 1; } inline bool operator==(const Section& other) const { @@ -156,14 +150,8 @@ class Version { return m_fullString < other.m_fullString; } - inline bool operator!=(const Section& other) const - { - return !(*this == other); - } - inline bool operator>(const Section& other) const - { - return !(*this < other || *this == other); - } + inline bool operator!=(const Section& other) const { return !(*this == other); } + inline bool operator>(const Section& other) const { return !(*this < other || *this == other); } }; private: diff --git a/launcher/VersionProxyModel.cpp b/launcher/VersionProxyModel.cpp index 6e0333e46..0ab9ae2c3 100644 --- a/launcher/VersionProxyModel.cpp +++ b/launcher/VersionProxyModel.cpp @@ -194,12 +194,12 @@ QVariant VersionProxyModel::data(const QModelIndex& index, int role) const switch (column) { case Name: { if (hasRecommended) { - auto value = sourceModel()->data(parentIndex, BaseVersionList::RecommendedRole); - if (value.toBool()) { + auto recommenced = sourceModel()->data(parentIndex, BaseVersionList::RecommendedRole); + if (recommenced.toBool()) { return APPLICATION->getThemedIcon("star"); } else if (hasLatest) { - auto value = sourceModel()->data(parentIndex, BaseVersionList::LatestRole); - if (value.toBool()) { + auto latest = sourceModel()->data(parentIndex, BaseVersionList::LatestRole); + if (latest.toBool()) { return APPLICATION->getThemedIcon("bug"); } } @@ -228,7 +228,7 @@ QVariant VersionProxyModel::data(const QModelIndex& index, int role) const } } -QModelIndex VersionProxyModel::parent(const QModelIndex& child) const +QModelIndex VersionProxyModel::parent([[maybe_unused]] const QModelIndex& child) const { return QModelIndex(); } @@ -408,7 +408,9 @@ void VersionProxyModel::sourceRowsAboutToBeInserted(const QModelIndex& parent, i beginInsertRows(parent, first, last); } -void VersionProxyModel::sourceRowsInserted(const QModelIndex& parent, int first, int last) +void VersionProxyModel::sourceRowsInserted([[maybe_unused]] const QModelIndex& parent, + [[maybe_unused]] int first, + [[maybe_unused]] int last) { endInsertRows(); } @@ -418,7 +420,7 @@ void VersionProxyModel::sourceRowsAboutToBeRemoved(const QModelIndex& parent, in beginRemoveRows(parent, first, last); } -void VersionProxyModel::sourceRowsRemoved(const QModelIndex& parent, int first, int last) +void VersionProxyModel::sourceRowsRemoved([[maybe_unused]] const QModelIndex& parent, [[maybe_unused]] int first, [[maybe_unused]] int last) { endRemoveRows(); } diff --git a/launcher/WindowsConsole.cpp b/launcher/WindowsConsole.cpp new file mode 100644 index 000000000..83cad5afa --- /dev/null +++ b/launcher/WindowsConsole.cpp @@ -0,0 +1,128 @@ +/* + * Prism Launcher - Minecraft Launcher + * Copyright (C) 2022 Rachel Powers <508861+Ryex@users.noreply.github.com> + * + * 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 . + * + */ + +#ifndef WIN32_LEAN_AND_MEAN +#define WIN32_LEAN_AND_MEAN +#endif +#include +#include +#include +#include +#include + +void RedirectHandle(DWORD handle, FILE* stream, const char* mode) +{ + HANDLE stdHandle = GetStdHandle(handle); + if (stdHandle != INVALID_HANDLE_VALUE) { + int fileDescriptor = _open_osfhandle((intptr_t)stdHandle, _O_TEXT); + if (fileDescriptor != -1) { + FILE* file = _fdopen(fileDescriptor, mode); + if (file != NULL) { + int dup2Result = _dup2(_fileno(file), _fileno(stream)); + if (dup2Result == 0) { + setvbuf(stream, NULL, _IONBF, 0); + } + } + } + } +} + +// taken from https://stackoverflow.com/a/25927081 +// getting a proper output to console with redirection support on windows is apparently hell +void BindCrtHandlesToStdHandles(bool bindStdIn, bool bindStdOut, bool bindStdErr) +{ + // Re-initialize the C runtime "FILE" handles with clean handles bound to "nul". We do this because it has been + // observed that the file number of our standard handle file objects can be assigned internally to a value of -2 + // when not bound to a valid target, which represents some kind of unknown internal invalid state. In this state our + // call to "_dup2" fails, as it specifically tests to ensure that the target file number isn't equal to this value + // before allowing the operation to continue. We can resolve this issue by first "re-opening" the target files to + // use the "nul" device, which will place them into a valid state, after which we can redirect them to our target + // using the "_dup2" function. + if (bindStdIn) { + FILE* dummyFile; + freopen_s(&dummyFile, "nul", "r", stdin); + } + if (bindStdOut) { + FILE* dummyFile; + freopen_s(&dummyFile, "nul", "w", stdout); + } + if (bindStdErr) { + FILE* dummyFile; + freopen_s(&dummyFile, "nul", "w", stderr); + } + + // Redirect unbuffered stdin from the current standard input handle + if (bindStdIn) { + RedirectHandle(STD_INPUT_HANDLE, stdin, "r"); + } + + // Redirect unbuffered stdout to the current standard output handle + if (bindStdOut) { + RedirectHandle(STD_OUTPUT_HANDLE, stdout, "w"); + } + + // Redirect unbuffered stderr to the current standard error handle + if (bindStdErr) { + RedirectHandle(STD_ERROR_HANDLE, stderr, "w"); + } + + // Clear the error state for each of the C++ standard stream objects. We need to do this, as attempts to access the + // standard streams before they refer to a valid target will cause the iostream objects to enter an error state. In + // versions of Visual Studio after 2005, this seems to always occur during startup regardless of whether anything + // has been read from or written to the targets or not. + if (bindStdIn) { + std::wcin.clear(); + std::cin.clear(); + } + if (bindStdOut) { + std::wcout.clear(); + std::cout.clear(); + } + if (bindStdErr) { + std::wcerr.clear(); + std::cerr.clear(); + } +} + +bool AttachWindowsConsole() +{ + auto stdinType = GetFileType(GetStdHandle(STD_INPUT_HANDLE)); + auto stdoutType = GetFileType(GetStdHandle(STD_OUTPUT_HANDLE)); + auto stderrType = GetFileType(GetStdHandle(STD_ERROR_HANDLE)); + + bool bindStdIn = false; + bool bindStdOut = false; + bool bindStdErr = false; + + if (stdinType == FILE_TYPE_CHAR || stdinType == FILE_TYPE_UNKNOWN) { + bindStdIn = true; + } + if (stdoutType == FILE_TYPE_CHAR || stdoutType == FILE_TYPE_UNKNOWN) { + bindStdOut = true; + } + if (stderrType == FILE_TYPE_CHAR || stderrType == FILE_TYPE_UNKNOWN) { + bindStdErr = true; + } + + if (AttachConsole(ATTACH_PARENT_PROCESS)) { + BindCrtHandlesToStdHandles(bindStdIn, bindStdOut, bindStdErr); + return true; + } + + return false; +} diff --git a/launcher/WindowsConsole.h b/launcher/WindowsConsole.h new file mode 100644 index 000000000..ab53864b4 --- /dev/null +++ b/launcher/WindowsConsole.h @@ -0,0 +1,25 @@ +// +// SPDX-License-Identifier: GPL-3.0-only + +/* + * Prism Launcher - Minecraft Launcher + * Copyright (C) 2022 Rachel Powers <508861+Ryex@users.noreply.github.com> + * + * 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 + +void BindCrtHandlesToStdHandles(bool bindStdIn, bool bindStdOut, bool bindStdErr); +bool AttachWindowsConsole(); diff --git a/launcher/filelink/FileLink.cpp b/launcher/filelink/FileLink.cpp index c9599b820..1ea0e382f 100644 --- a/launcher/filelink/FileLink.cpp +++ b/launcher/filelink/FileLink.cpp @@ -37,11 +37,7 @@ #include #if defined Q_OS_WIN32 -#ifndef WIN32_LEAN_AND_MEAN -#define WIN32_LEAN_AND_MEAN -#endif -#include -#include +#include "WindowsConsole.h" #endif // Snippet from https://github.com/gulrak/filesystem#using-it-as-single-file-header @@ -67,21 +63,7 @@ FileLinkApp::FileLinkApp(int& argc, char** argv) : QCoreApplication(argc, argv), { #if defined Q_OS_WIN32 // attach the parent console - if (AttachConsole(ATTACH_PARENT_PROCESS)) { - // if attach succeeds, reopen and sync all the i/o - if (freopen("CON", "w", stdout)) { - std::cout.sync_with_stdio(); - } - if (freopen("CON", "w", stderr)) { - std::cerr.sync_with_stdio(); - } - if (freopen("CON", "r", stdin)) { - std::cin.sync_with_stdio(); - } - auto out = GetStdHandle(STD_OUTPUT_HANDLE); - DWORD written; - const char* endline = "\n"; - WriteConsole(out, endline, strlen(endline), &written, NULL); + if (AttachWindowsConsole()) { consoleAttached = true; } #endif @@ -188,7 +170,7 @@ void FileLinkApp::runLink() FS::LinkResult result = { src_path, dst_path, QString::fromStdString(os_err.message()), os_err.value() }; m_path_results.append(result); } else { - FS::LinkResult result = { src_path, dst_path }; + FS::LinkResult result = { src_path, dst_path, "", 0 }; m_path_results.append(result); } } @@ -248,7 +230,7 @@ void FileLinkApp::readPathPairs() in >> numLinks; qDebug() << "numLinks" << numLinks; - for (int i = 0; i < numLinks; i++) { + for (quint32 i = 0; i < numLinks; i++) { FS::LinkPair pair; in >> pair.src; in >> pair.dst; @@ -271,7 +253,6 @@ FileLinkApp::~FileLinkApp() fclose(stdout); fclose(stdin); fclose(stderr); - FreeConsole(); } #endif } diff --git a/launcher/icons/IconList.cpp b/launcher/icons/IconList.cpp index 30c51caf9..5576b9745 100644 --- a/launcher/icons/IconList.cpp +++ b/launcher/icons/IconList.cpp @@ -2,6 +2,7 @@ /* * Prism Launcher - Minecraft Launcher * Copyright (C) 2022 Sefa Eyeoglu + * Copyright (c) 2023 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 @@ -42,6 +43,7 @@ #include #include #include +#include "icons/IconUtils.h" #define MAX_SIZE 1024 @@ -128,7 +130,7 @@ void IconList::directoryChanged(const QString& path) QString suffix = rmfile.suffix(); // The icon doesnt have a suffix, but it can have other .s in the name, so we account for those as well - if (suffix != "jpeg" && suffix != "png" && suffix != "jpg" && suffix != "ico" && suffix != "svg" && suffix != "gif") + if (!IconUtils::isIconSuffix(suffix)) key = rmfile.fileName(); int idx = getIconIndex(key); @@ -155,7 +157,7 @@ void IconList::directoryChanged(const QString& path) QString suffix = addfile.suffix(); // The icon doesnt have a suffix, but it can have other .s in the name, so we account for those as well - if (suffix != "jpeg" && suffix != "png" && suffix != "jpg" && suffix != "ico" && suffix != "svg" && suffix != "gif") + if (!IconUtils::isIconSuffix(suffix)) key = addfile.fileName(); if (addIcon(key, QString(), addfile.filePath(), IconType::FileBased)) { @@ -255,10 +257,7 @@ bool IconList::dropMimeData(const QMimeData* data, Qt::ItemFlags IconList::flags(const QModelIndex& index) const { Qt::ItemFlags defaultFlags = QAbstractListModel::flags(index); - if (index.isValid()) - return Qt::ItemIsDropEnabled | defaultFlags; - else - return Qt::ItemIsDropEnabled | defaultFlags; + return Qt::ItemIsDropEnabled | defaultFlags; } QVariant IconList::data(const QModelIndex& index, int role) const @@ -290,19 +289,8 @@ int IconList::rowCount(const QModelIndex& parent) const void IconList::installIcons(const QStringList& iconFiles) { - for (QString file : iconFiles) { - QFileInfo fileinfo(file); - if (!fileinfo.isReadable() || !fileinfo.isFile()) - continue; - QString target = FS::PathCombine(getDirectory(), fileinfo.fileName()); - - QString suffix = fileinfo.suffix(); - if (suffix != "jpeg" && suffix != "png" && suffix != "jpg" && suffix != "ico" && suffix != "svg" && suffix != "gif") - continue; - - if (!QFile::copy(file, target)) - continue; - } + for (QString file : iconFiles) + installIcon(file, {}); } void IconList::installIcon(const QString& file, const QString& name) @@ -311,18 +299,17 @@ void IconList::installIcon(const QString& file, const QString& name) if (!fileinfo.isReadable() || !fileinfo.isFile()) return; - QString target = FS::PathCombine(getDirectory(), name); + if (!IconUtils::isIconSuffix(fileinfo.suffix())) + return; + QString target = FS::PathCombine(getDirectory(), name.isEmpty() ? fileinfo.fileName() : name); QFile::copy(file, target); } bool IconList::iconFileExists(const QString& key) const { auto iconEntry = icon(key); - if (!iconEntry) { - return false; - } - return iconEntry->has(IconType::FileBased); + return iconEntry && iconEntry->has(IconType::FileBased); } const MMCIcon* IconList::icon(const QString& key) const @@ -335,18 +322,12 @@ const MMCIcon* IconList::icon(const QString& key) const bool IconList::deleteIcon(const QString& key) { - if (!iconFileExists(key)) - return false; - - return QFile::remove(icon(key)->getFilePath()); + return iconFileExists(key) && QFile::remove(icon(key)->getFilePath()); } bool IconList::trashIcon(const QString& key) { - if (!iconFileExists(key)) - return false; - - return FS::trash(icon(key)->getFilePath(), nullptr); + return iconFileExists(key) && FS::trash(icon(key)->getFilePath(), nullptr); } bool IconList::addThemeIcon(const QString& key) @@ -357,20 +338,19 @@ bool IconList::addThemeIcon(const QString& key) oldOne.replace(Builtin, key); dataChanged(index(*iter), index(*iter)); return true; - } else { - // add a new icon - beginInsertRows(QModelIndex(), icons.size(), icons.size()); - { - MMCIcon mmc_icon; - mmc_icon.m_name = key; - mmc_icon.m_key = key; - mmc_icon.replace(Builtin, key); - icons.push_back(mmc_icon); - name_index[key] = icons.size() - 1; - } - endInsertRows(); - return true; } + // add a new icon + beginInsertRows(QModelIndex(), icons.size(), icons.size()); + { + MMCIcon mmc_icon; + mmc_icon.m_name = key; + mmc_icon.m_key = key; + mmc_icon.replace(Builtin, key); + icons.push_back(mmc_icon); + name_index[key] = icons.size() - 1; + } + endInsertRows(); + return true; } bool IconList::addIcon(const QString& key, const QString& name, const QString& path, const IconType type) @@ -385,20 +365,19 @@ bool IconList::addIcon(const QString& key, const QString& name, const QString& p oldOne.replace(type, icon, path); dataChanged(index(*iter), index(*iter)); return true; - } else { - // add a new icon - beginInsertRows(QModelIndex(), icons.size(), icons.size()); - { - MMCIcon mmc_icon; - mmc_icon.m_name = name; - mmc_icon.m_key = key; - mmc_icon.replace(type, icon, path); - icons.push_back(mmc_icon); - name_index[key] = icons.size() - 1; - } - endInsertRows(); - return true; } + // add a new icon + beginInsertRows(QModelIndex(), icons.size(), icons.size()); + { + MMCIcon mmc_icon; + mmc_icon.m_name = name; + mmc_icon.m_key = key; + mmc_icon.replace(type, icon, path); + icons.push_back(mmc_icon); + name_index[key] = icons.size() - 1; + } + endInsertRows(); + return true; } void IconList::saveIcon(const QString& key, const QString& path, const char* format) const @@ -446,5 +425,3 @@ QString IconList::getDirectory() const { return m_dir.absolutePath(); } - -//#include "IconList.moc" diff --git a/launcher/icons/IconList.h b/launcher/icons/IconList.h index bc1dd3b9b..8afd05574 100644 --- a/launcher/icons/IconList.h +++ b/launcher/icons/IconList.h @@ -1,18 +1,37 @@ -/* Copyright 2013-2021 MultiMC Contributors +// SPDX-License-Identifier: GPL-3.0-only +/* + * Prism Launcher - Minecraft Launcher + * Copyright (c) 2023 Trial97 * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at + * 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. * - * http://www.apache.org/licenses/LICENSE-2.0 + * 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. * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + * + * This file incorporates work covered by the following copyright and + * permission notice: + * + * Copyright 2013-2021 MultiMC Contributors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. */ - #pragma once #include diff --git a/launcher/icons/IconUtils.cpp b/launcher/icons/IconUtils.cpp index 0b06639e7..99c38f47a 100644 --- a/launcher/icons/IconUtils.cpp +++ b/launcher/icons/IconUtils.cpp @@ -1,19 +1,51 @@ +// SPDX-License-Identifier: GPL-3.0-only +/* + * Prism Launcher - Minecraft Launcher + * Copyright (c) 2023 Trial97 + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, version 3. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + * + * This file incorporates work covered by the following copyright and + * permission notice: + * + * Copyright 2013-2021 MultiMC Contributors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + #include "IconUtils.h" #include #include "FileSystem.h" -#include - namespace { -std::array validIconExtensions = { { "svg", "png", "ico", "gif", "jpg", "jpeg" } }; +static const QStringList validIconExtensions = { { "svg", "png", "ico", "gif", "jpg", "jpeg" } }; } namespace IconUtils { QString findBestIconIn(const QString& folder, const QString& iconKey) { - int best_found = validIconExtensions.size(); QString best_filename; QDirIterator it(folder, QDir::NoDotAndDotDot | QDir::Files, QDirIterator::NoIteratorFlags); @@ -21,36 +53,20 @@ QString findBestIconIn(const QString& folder, const QString& iconKey) it.next(); auto fileInfo = it.fileInfo(); - if (fileInfo.completeBaseName() != iconKey) - continue; - - auto extension = fileInfo.suffix(); - - for (int i = 0; i < best_found; i++) { - if (extension == validIconExtensions[i]) { - best_found = i; - qDebug() << i << " : " << fileInfo.fileName(); - best_filename = fileInfo.fileName(); - } - } + if (fileInfo.completeBaseName() == iconKey && isIconSuffix(fileInfo.suffix())) + return fileInfo.absoluteFilePath(); } - return FS::PathCombine(folder, best_filename); + return {}; } QString getIconFilter() { - QString out; - QTextStream stream(&out); - stream << '('; - for (size_t i = 0; i < validIconExtensions.size() - 1; i++) { - if (i > 0) { - stream << " "; - } - stream << "*." << validIconExtensions[i]; - } - stream << " *." << validIconExtensions[validIconExtensions.size() - 1]; - stream << ')'; - return out; + return "(*." + validIconExtensions.join(" *.") + ")"; +} + +bool isIconSuffix(QString suffix) +{ + return validIconExtensions.contains(suffix); } } // namespace IconUtils diff --git a/launcher/icons/IconUtils.h b/launcher/icons/IconUtils.h index 8c6f677dd..90cdfe5ab 100644 --- a/launcher/icons/IconUtils.h +++ b/launcher/icons/IconUtils.h @@ -1,3 +1,38 @@ +// SPDX-License-Identifier: GPL-3.0-only +/* + * Prism Launcher - Minecraft Launcher + * Copyright (c) 2023 Trial97 + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, version 3. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + * + * This file incorporates work covered by the following copyright and + * permission notice: + * + * Copyright 2013-2021 MultiMC Contributors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + #pragma once #include @@ -10,4 +45,5 @@ QString findBestIconIn(const QString& folder, const QString& iconKey); // Get icon file type filter for file browser dialogs QString getIconFilter(); +bool isIconSuffix(QString suffix); } // namespace IconUtils diff --git a/launcher/icons/MMCIcon.cpp b/launcher/icons/MMCIcon.cpp index 8301b1ba9..991b470c0 100644 --- a/launcher/icons/MMCIcon.cpp +++ b/launcher/icons/MMCIcon.cpp @@ -2,6 +2,7 @@ /* * Prism Launcher - Minecraft Launcher * Copyright (C) 2022 Sefa Eyeoglu + * Copyright (c) 2023 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 @@ -50,8 +51,8 @@ IconType operator--(IconType& t, int) case IconType::FileBased: t = IconType::Transient; break; - default: { - } + default: + break; } return temp; } diff --git a/launcher/icons/MMCIcon.h b/launcher/icons/MMCIcon.h index c968f306a..a6e305668 100644 --- a/launcher/icons/MMCIcon.h +++ b/launcher/icons/MMCIcon.h @@ -1,18 +1,37 @@ -/* Copyright 2013-2021 MultiMC Contributors +// SPDX-License-Identifier: GPL-3.0-only +/* + * Prism Launcher - Minecraft Launcher + * Copyright (c) 2023 Trial97 * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at + * 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. * - * http://www.apache.org/licenses/LICENSE-2.0 + * 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. * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + * + * This file incorporates work covered by the following copyright and + * permission notice: + * + * Copyright 2013-2021 MultiMC Contributors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. */ - #pragma once #include #include diff --git a/launcher/java/JavaVersion.h b/launcher/java/JavaVersion.h index 694732033..7e66269cb 100644 --- a/launcher/java/JavaVersion.h +++ b/launcher/java/JavaVersion.h @@ -14,7 +14,7 @@ class JavaVersion { friend class JavaVersionTest; public: - JavaVersion(){}; + JavaVersion() {} JavaVersion(const QString& rhs); JavaVersion& operator=(const QString& rhs); diff --git a/launcher/launch/LogModel.h b/launcher/launch/LogModel.h index ba2e4054a..18e51d7e3 100644 --- a/launcher/launch/LogModel.h +++ b/launcher/launch/LogModel.h @@ -30,7 +30,7 @@ class LogModel : public QAbstractListModel { enum Roles { LevelRole = Qt::UserRole }; - private /* types */: + private /* types */: struct entry { MessageLevel::Enum level; QString line; diff --git a/launcher/meta/BaseEntity.cpp b/launcher/meta/BaseEntity.cpp index d8003b55a..5f9804e48 100644 --- a/launcher/meta/BaseEntity.cpp +++ b/launcher/meta/BaseEntity.cpp @@ -16,7 +16,7 @@ #include "BaseEntity.h" #include "Json.h" -#include "net/Download.h" +#include "net/ApiDownload.h" #include "net/HttpMetaCache.h" #include "net/NetJob.h" @@ -32,7 +32,7 @@ class ParsingValidator : public Net::Validator { bool init(QNetworkRequest&) override { return true; } bool write(QByteArray& data) override { - this->data.append(data); + this->m_data.append(data); return true; } bool abort() override { return true; } @@ -40,7 +40,7 @@ class ParsingValidator : public Net::Validator { { auto fname = m_entity->localFilename(); try { - auto doc = Json::requireDocument(data, fname); + auto doc = Json::requireDocument(m_data, fname); auto obj = Json::requireObject(doc, fname); m_entity->parse(obj); return true; @@ -51,7 +51,7 @@ class ParsingValidator : public Net::Validator { } private: /* data */ - QByteArray data; + QByteArray m_data; Meta::BaseEntity* m_entity; }; @@ -104,7 +104,7 @@ void Meta::BaseEntity::load(Net::Mode loadType) auto url = this->url(); auto entry = APPLICATION->metacache()->resolveEntry("meta", localFilename()); entry->setStale(true); - auto dl = Net::Download::makeCached(url, entry); + auto dl = Net::ApiDownload::makeCached(url, entry); /* * The validator parses the file and loads it into the object. * If that fails, the file is not written to storage. diff --git a/launcher/minecraft/AssetsUtils.cpp b/launcher/minecraft/AssetsUtils.cpp index 4923f0efd..4ef007075 100644 --- a/launcher/minecraft/AssetsUtils.cpp +++ b/launcher/minecraft/AssetsUtils.cpp @@ -46,8 +46,8 @@ #include "AssetsUtils.h" #include "BuildConfig.h" #include "FileSystem.h" +#include "net/ApiDownload.h" #include "net/ChecksumValidator.h" -#include "net/Download.h" #include "Application.h" @@ -279,7 +279,7 @@ NetAction::Ptr AssetObject::getDownloadAction() { QFileInfo objectFile(getLocalPath()); if ((!objectFile.isFile()) || (objectFile.size() != size)) { - auto objectDL = Net::Download::makeFile(getUrl(), objectFile.filePath()); + auto objectDL = Net::ApiDownload::makeFile(getUrl(), objectFile.filePath()); if (hash.size()) { auto rawHash = QByteArray::fromHex(hash.toLatin1()); objectDL->addValidator(new Net::ChecksumValidator(QCryptographicHash::Sha1, rawHash)); diff --git a/launcher/minecraft/Component.h b/launcher/minecraft/Component.h index 00a912d65..3474a22e0 100644 --- a/launcher/minecraft/Component.h +++ b/launcher/minecraft/Component.h @@ -25,7 +25,8 @@ class Component : public QObject, public ProblemProvider { Component(PackProfile* parent, std::shared_ptr version); Component(PackProfile* parent, const QString& uid, std::shared_ptr file); - virtual ~Component(){}; + virtual ~Component() {} + void applyTo(LaunchProfile* profile); bool isEnabled(); diff --git a/launcher/minecraft/ComponentUpdateTask.cpp b/launcher/minecraft/ComponentUpdateTask.cpp index 0b85b81aa..bb838043a 100644 --- a/launcher/minecraft/ComponentUpdateTask.cpp +++ b/launcher/minecraft/ComponentUpdateTask.cpp @@ -2,14 +2,14 @@ #include "Component.h" #include "ComponentUpdateTask_p.h" -#include "OneSixVersionFormat.h" #include "PackProfile.h" #include "PackProfile_p.h" #include "Version.h" #include "cassert" #include "meta/Index.h" #include "meta/Version.h" -#include "meta/VersionList.h" +#include "minecraft/OneSixVersionFormat.h" +#include "minecraft/ProfileUtils.h" #include "net/Mode.h" #include "Application.h" diff --git a/launcher/minecraft/LaunchProfile.h b/launcher/minecraft/LaunchProfile.h index 3acc1f192..12b312383 100644 --- a/launcher/minecraft/LaunchProfile.h +++ b/launcher/minecraft/LaunchProfile.h @@ -41,7 +41,7 @@ class LaunchProfile : public ProblemProvider { public: - virtual ~LaunchProfile(){}; + virtual ~LaunchProfile() {} public: /* application of profile variables from patches */ void applyMinecraftVersion(const QString& id); diff --git a/launcher/minecraft/Library.cpp b/launcher/minecraft/Library.cpp index a9c208450..0e8ddf03d 100644 --- a/launcher/minecraft/Library.cpp +++ b/launcher/minecraft/Library.cpp @@ -38,8 +38,8 @@ #include #include +#include #include -#include void Library::getApplicableFiles(const RuntimeContext& runtimeContext, QStringList& jar, @@ -115,12 +115,12 @@ QList Library::getDownloads(const RuntimeContext& runtimeContext if (sha1.size()) { auto rawSha1 = QByteArray::fromHex(sha1.toLatin1()); - auto dl = Net::Download::makeCached(url, entry, options); + auto dl = Net::ApiDownload::makeCached(url, entry, options); dl->addValidator(new Net::ChecksumValidator(QCryptographicHash::Sha1, rawSha1)); qDebug() << "Checksummed Download for:" << rawName().serialize() << "storage:" << storage << "url:" << url; out.append(dl); } else { - out.append(Net::Download::makeCached(url, entry, options)); + out.append(Net::ApiDownload::makeCached(url, entry, options)); qDebug() << "Download for:" << rawName().serialize() << "storage:" << storage << "url:" << url; } return true; diff --git a/launcher/minecraft/MinecraftInstance.cpp b/launcher/minecraft/MinecraftInstance.cpp index 305bff67b..86ef3b30e 100644 --- a/launcher/minecraft/MinecraftInstance.cpp +++ b/launcher/minecraft/MinecraftInstance.cpp @@ -3,8 +3,7 @@ * Prism Launcher - Minecraft Launcher * Copyright (C) 2022 Sefa Eyeoglu * Copyright (C) 2022 Jamie Mansfield - * Copyright (C) 2022 TheKodeToad - * Copyright (c) 2023 seth + * Copyright (C) 2023 TheKodeToad * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by @@ -88,6 +87,10 @@ #include "minecraft/gameoptions/GameOptions.h" #include "minecraft/update/FoldersTask.h" +#include "tools/BaseProfiler.h" + +#include + #ifdef Q_OS_LINUX #include "MangoHud.h" #endif @@ -166,7 +169,9 @@ void MinecraftInstance::loadSpecificSettings() // Native library workarounds auto nativeLibraryWorkaroundsOverride = m_settings->registerSetting("OverrideNativeWorkarounds", false); m_settings->registerOverride(global_settings->getSetting("UseNativeOpenAL"), nativeLibraryWorkaroundsOverride); + m_settings->registerOverride(global_settings->getSetting("CustomOpenALPath"), nativeLibraryWorkaroundsOverride); m_settings->registerOverride(global_settings->getSetting("UseNativeGLFW"), nativeLibraryWorkaroundsOverride); + m_settings->registerOverride(global_settings->getSetting("CustomGLFWPath"), nativeLibraryWorkaroundsOverride); // Peformance related options auto performanceOverride = m_settings->registerSetting("OverridePerformance", false); @@ -179,10 +184,6 @@ void MinecraftInstance::loadSpecificSettings() m_settings->registerOverride(global_settings->getSetting("CloseAfterLaunch"), miscellaneousOverride); m_settings->registerOverride(global_settings->getSetting("QuitAfterGameStop"), miscellaneousOverride); - // Mod loader specific options - auto modLoaderSettings = m_settings->registerSetting("OverrideModLoaderSettings", false); - m_settings->registerOverride(global_settings->getSetting("DisableQuiltBeacon"), modLoaderSettings); - m_settings->set("InstanceType", "OneSix"); } @@ -194,6 +195,12 @@ void MinecraftInstance::loadSpecificSettings() m_settings->registerSetting("UseAccountForInstance", false); m_settings->registerSetting("InstanceAccountId", ""); + m_settings->registerSetting("ExportName", ""); + m_settings->registerSetting("ExportVersion", "1.0.0"); + m_settings->registerSetting("ExportSummary", ""); + m_settings->registerSetting("ExportAuthor", ""); + m_settings->registerSetting("ExportOptionalFiles", true); + qDebug() << "Instance-type specific settings were loaded!"; setSpecificSettingsLoaded(true); @@ -229,6 +236,50 @@ QSet MinecraftInstance::traits() const return profile->getTraits(); } +// FIXME: move UI code out of MinecraftInstance +void MinecraftInstance::populateLaunchMenu(QMenu* menu) +{ + QAction* normalLaunch = menu->addAction(tr("&Launch")); + normalLaunch->setShortcut(QKeySequence::Open); + QAction* normalLaunchOffline = menu->addAction(tr("Launch &Offline")); + normalLaunchOffline->setShortcut(QKeySequence(tr("Ctrl+Shift+O"))); + QAction* normalLaunchDemo = menu->addAction(tr("Launch &Demo")); + normalLaunchDemo->setShortcut(QKeySequence(tr("Ctrl+Alt+O"))); + + normalLaunchDemo->setEnabled(supportsDemo()); + + connect(normalLaunch, &QAction::triggered, [this] { APPLICATION->launch(shared_from_this()); }); + connect(normalLaunchOffline, &QAction::triggered, [this] { APPLICATION->launch(shared_from_this(), false, false); }); + connect(normalLaunchDemo, &QAction::triggered, [this] { APPLICATION->launch(shared_from_this(), false, true); }); + + QString profilersTitle = tr("Profilers"); + menu->addSeparator()->setText(profilersTitle); + + auto profilers = new QActionGroup(menu); + profilers->setExclusive(true); + connect(profilers, &QActionGroup::triggered, [this](QAction* action) { + settings()->set("Profiler", action->data()); + emit profilerChanged(); + }); + + QAction* noProfilerAction = menu->addAction(tr("&No Profiler")); + noProfilerAction->setData(""); + noProfilerAction->setCheckable(true); + noProfilerAction->setChecked(true); + profilers->addAction(noProfilerAction); + + for (auto profiler = APPLICATION->profilers().begin(); profiler != APPLICATION->profilers().end(); profiler++) { + QAction* profilerAction = menu->addAction(profiler.value()->name()); + profilers->addAction(profilerAction); + profilerAction->setData(profiler.key()); + profilerAction->setCheckable(true); + profilerAction->setChecked(settings()->get("Profiler").toString() == profiler.key()); + + QString error; + profilerAction->setEnabled(profiler.value()->check(&error)); + } +} + QString MinecraftInstance::gameRoot() const { QFileInfo mcDir(FS::PathCombine(instanceRoot(), "minecraft")); @@ -260,7 +311,7 @@ QString MinecraftInstance::getLocalLibraryPath() const bool MinecraftInstance::supportsDemo() const { Version instance_ver{ getPackProfile()->getComponentVersion("net.minecraft") }; - // Demo mode was introduced in 1.3.1: https://minecraft.fandom.com/wiki/Demo_mode#History + // Demo mode was introduced in 1.3.1: https://minecraft.wiki/w/Demo_mode#History // FIXME: Due to Version constraints atm, this can't handle well non-release versions return instance_ver >= Version("1.3.1"); } @@ -385,10 +436,31 @@ QStringList MinecraftInstance::extraArguments() } { - const auto loaders = version->getModLoaders(); - if (loaders.has_value() && loaders.value() & ResourceAPI::Quilt && settings()->get("DisableQuiltBeacon").toBool()) - list.append("-Dloader.disable_beacon=true"); + QString openALPath; + QString glfwPath; + + if (settings()->get("UseNativeOpenAL").toBool()) { + openALPath = APPLICATION->m_detectedOpenALPath; + auto customPath = settings()->get("CustomOpenALPath").toString(); + if (!customPath.isEmpty()) + openALPath = customPath; + } + if (settings()->get("UseNativeGLFW").toBool()) { + glfwPath = APPLICATION->m_detectedGLFWPath; + auto customPath = settings()->get("CustomGLFWPath").toString(); + if (!customPath.isEmpty()) + glfwPath = customPath; + } + + QFileInfo openALInfo(openALPath); + QFileInfo glfwInfo(glfwPath); + + if (!openALPath.isEmpty() && openALInfo.exists()) + list.append("-Dorg.lwjgl.openal.libname=" + openALInfo.absoluteFilePath()); + if (!glfwPath.isEmpty() && glfwInfo.exists()) + list.append("-Dorg.lwjgl.glfw.libname=" + glfwInfo.absoluteFilePath()); } + return list; } @@ -868,13 +940,16 @@ QString MinecraftInstance::getStatusbarDescription() if (m_settings->get("ShowGameTime").toBool()) { if (lastTimePlayed() > 0) { QDateTime lastLaunchTime = QDateTime::fromMSecsSinceEpoch(lastLaunch()); - description.append(tr(", last played on %1 for %2") - .arg(QLocale().toString(lastLaunchTime, QLocale::ShortFormat)) - .arg(Time::prettifyDuration(lastTimePlayed()))); + description.append( + tr(", last played on %1 for %2") + .arg(QLocale().toString(lastLaunchTime, QLocale::ShortFormat)) + .arg(Time::prettifyDuration(lastTimePlayed(), APPLICATION->settings()->get("ShowGameTimeWithoutDays").toBool()))); } if (totalTimePlayed() > 0) { - description.append(tr(", total played for %1").arg(Time::prettifyDuration(totalTimePlayed()))); + description.append( + tr(", total played for %1") + .arg(Time::prettifyDuration(totalTimePlayed(), APPLICATION->settings()->get("ShowGameTimeWithoutDays").toBool()))); } } if (hasCrashed()) { diff --git a/launcher/minecraft/MinecraftInstance.h b/launcher/minecraft/MinecraftInstance.h index c331cc6f5..dabd44ba7 100644 --- a/launcher/minecraft/MinecraftInstance.h +++ b/launcher/minecraft/MinecraftInstance.h @@ -2,7 +2,7 @@ /* * Prism Launcher - Minecraft Launcher * Copyright (C) 2022 Sefa Eyeoglu - * Copyright (C) 2022 TheKodeToad + * Copyright (C) 2023 TheKodeToad * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by @@ -70,6 +70,8 @@ class MinecraftInstance : public BaseInstance { bool canExport() const override { return true; } + void populateLaunchMenu(QMenu* menu) override; + ////// Directories and files ////// QString jarModsDir() const; QString resourcePacksDir() const; diff --git a/launcher/minecraft/MojangDownloadInfo.h b/launcher/minecraft/MojangDownloadInfo.h index b98a0168d..855dbe005 100644 --- a/launcher/minecraft/MojangDownloadInfo.h +++ b/launcher/minecraft/MojangDownloadInfo.h @@ -19,8 +19,8 @@ struct MojangDownloadInfo { }; struct MojangLibraryDownloadInfo { - MojangLibraryDownloadInfo(MojangDownloadInfo::Ptr artifact) : artifact(artifact){}; - MojangLibraryDownloadInfo(){}; + MojangLibraryDownloadInfo(MojangDownloadInfo::Ptr artifact_) : artifact(artifact_) {} + MojangLibraryDownloadInfo() {} // types typedef std::shared_ptr Ptr; @@ -47,18 +47,18 @@ struct MojangAssetIndexInfo : public MojangDownloadInfo { // methods MojangAssetIndexInfo() {} - MojangAssetIndexInfo(QString id) + MojangAssetIndexInfo(QString id_) { - this->id = id; + this->id = id_; // HACK: ignore assets from other version files than Minecraft // workaround for stupid assets issue caused by amazon: // https://www.theregister.co.uk/2017/02/28/aws_is_awol_as_s3_goes_haywire/ - if (id == "legacy") { + if (id_ == "legacy") { url = "https://piston-meta.mojang.com/mc/assets/legacy/c0fd82e8ce9fbc93119e40d96d5a4e62cfa3f729/legacy.json"; } // HACK else { - url = "https://s3.amazonaws.com/Minecraft.Download/indexes/" + id + ".json"; + url = "https://s3.amazonaws.com/Minecraft.Download/indexes/" + id_ + ".json"; } known = false; } diff --git a/launcher/minecraft/OneSixVersionFormat.cpp b/launcher/minecraft/OneSixVersionFormat.cpp index e24553252..306c95a6a 100644 --- a/launcher/minecraft/OneSixVersionFormat.cpp +++ b/launcher/minecraft/OneSixVersionFormat.cpp @@ -350,7 +350,7 @@ QJsonDocument OneSixVersionFormat::versionFileToJson(const VersionFilePtr& patch } } -LibraryPtr OneSixVersionFormat::plusJarModFromJson(ProblemContainer& problems, +LibraryPtr OneSixVersionFormat::plusJarModFromJson([[maybe_unused]] ProblemContainer& problems, const QJsonObject& libObj, const QString& filename, const QString& originalName) diff --git a/launcher/minecraft/PackProfile.cpp b/launcher/minecraft/PackProfile.cpp index 82fc779eb..9e42c5dd6 100644 --- a/launcher/minecraft/PackProfile.cpp +++ b/launcher/minecraft/PackProfile.cpp @@ -58,14 +58,14 @@ #include "ComponentUpdateTask.h" #include "PackProfile.h" #include "PackProfile_p.h" +#include "minecraft/mod/Mod.h" +#include "modplatform/ModIndex.h" -#include "Application.h" -#include "modplatform/ResourceAPI.h" - -static const QMap modloaderMapping{ { "net.minecraftforge", ResourceAPI::Forge }, - { "net.fabricmc.fabric-loader", ResourceAPI::Fabric }, - { "org.quiltmc.quilt-loader", ResourceAPI::Quilt }, - { "com.mumfrey.liteloader", ResourceAPI::LiteLoader } }; +static const QMap modloaderMapping{ { "net.neoforged", ModPlatform::NeoForge }, + { "net.minecraftforge", ModPlatform::Forge }, + { "net.fabricmc.fabric-loader", ModPlatform::Fabric }, + { "org.quiltmc.quilt-loader", ModPlatform::Quilt }, + { "com.mumfrey.liteloader", ModPlatform::LiteLoader } }; PackProfile::PackProfile(MinecraftInstance* instance) : QAbstractListModel() { @@ -204,10 +204,10 @@ static bool loadPackProfile(PackProfile* parent, } auto orderArray = Json::requireArray(obj.value("components")); for (auto item : orderArray) { - auto obj = Json::requireObject(item, "Component must be an object."); - container.append(componentFromJsonV1(parent, componentJsonPattern, obj)); + auto comp_obj = Json::requireObject(item, "Component must be an object."); + container.append(componentFromJsonV1(parent, componentJsonPattern, comp_obj)); } - } catch (const JSONValidationError& err) { + } catch ([[maybe_unused]] const JSONValidationError& err) { qCritical() << "Couldn't parse" << componentsFile.fileName() << ": bad file format"; container.clear(); return false; @@ -377,7 +377,7 @@ void PackProfile::insertComponent(size_t index, ComponentPtr component) qWarning() << "Attempt to add a component that is already present!"; return; } - beginInsertRows(QModelIndex(), index, index); + beginInsertRows(QModelIndex(), static_cast(index), static_cast(index)); d->components.insert(index, component); d->componentIndex[id] = component; endInsertRows(); @@ -389,7 +389,7 @@ void PackProfile::componentDataChanged() { auto objPtr = qobject_cast(sender()); if (!objPtr) { - qWarning() << "PackProfile got dataChenged signal from a non-Component!"; + qWarning() << "PackProfile got dataChanged signal from a non-Component!"; return; } if (objPtr->getID() == "net.minecraft") { @@ -405,7 +405,7 @@ void PackProfile::componentDataChanged() } index++; } - qWarning() << "PackProfile got dataChenged signal from a Component which does not belong to it!"; + qWarning() << "PackProfile got dataChanged signal from a Component which does not belong to it!"; } bool PackProfile::remove(const int index) @@ -483,9 +483,9 @@ ComponentPtr PackProfile::getComponent(const QString& id) return (*iter); } -ComponentPtr PackProfile::getComponent(int index) +ComponentPtr PackProfile::getComponent(size_t index) { - if (index < 0 || index >= d->components.size()) { + if (index >= static_cast(d->components.size())) { return nullptr; } return d->components[index]; @@ -547,7 +547,7 @@ QVariant PackProfile::data(const QModelIndex& index, int role) const return QVariant(); } -bool PackProfile::setData(const QModelIndex& index, const QVariant& value, int role) +bool PackProfile::setData(const QModelIndex& index, [[maybe_unused]] const QVariant& value, int role) { if (!index.isValid() || index.row() < 0 || index.row() >= rowCount(index.parent())) { return false; @@ -989,12 +989,12 @@ void PackProfile::disableInteraction(bool disable) } } -std::optional PackProfile::getModLoaders() +std::optional PackProfile::getModLoaders() { - ResourceAPI::ModLoaderTypes result; + ModPlatform::ModLoaderTypes result; bool has_any_loader = false; - QMapIterator i(modloaderMapping); + QMapIterator i(modloaderMapping); while (i.hasNext()) { i.next(); @@ -1008,3 +1008,18 @@ std::optional PackProfile::getModLoaders() return {}; return result; } + +std::optional PackProfile::getSupportedModLoaders() +{ + auto loadersOpt = getModLoaders(); + if (!loadersOpt.has_value()) + return loadersOpt; + auto loaders = loadersOpt.value(); + // TODO: remove this or add version condition once Quilt drops official Fabric support + if (loaders & ModPlatform::Quilt) + loaders |= ModPlatform::Fabric; + // TODO: remove this or add version condition once NeoForge drops official Forge support + if (loaders & ModPlatform::NeoForge) + loaders |= ModPlatform::Forge; + return loaders; +} diff --git a/launcher/minecraft/PackProfile.h b/launcher/minecraft/PackProfile.h index a5e5cb1a9..e72b6ebfe 100644 --- a/launcher/minecraft/PackProfile.h +++ b/launcher/minecraft/PackProfile.h @@ -44,14 +44,11 @@ #include #include #include +#include -#include "BaseVersion.h" #include "Component.h" #include "LaunchProfile.h" -#include "Library.h" -#include "MojangDownloadInfo.h" -#include "ProfileUtils.h" -#include "modplatform/ResourceAPI.h" +#include "modplatform/ModIndex.h" #include "net/Mode.h" class MinecraftInstance; @@ -140,13 +137,15 @@ class PackProfile : public QAbstractListModel { ComponentPtr getComponent(const QString& id); /// get the profile component by index - ComponentPtr getComponent(int index); + ComponentPtr getComponent(size_t index); /// Add the component to the internal list of patches // todo(merged): is this the best approach void appendComponent(ComponentPtr component); - std::optional getModLoaders(); + std::optional getModLoaders(); + // this returns aditional loaders(Quilt supports fabric and NeoForge supports Forge) + std::optional getSupportedModLoaders(); private: void scheduleSave(); diff --git a/launcher/minecraft/ProfileUtils.cpp b/launcher/minecraft/ProfileUtils.cpp index 18a4b7d00..d56ed14bd 100644 --- a/launcher/minecraft/ProfileUtils.cpp +++ b/launcher/minecraft/ProfileUtils.cpp @@ -82,7 +82,7 @@ bool readOverrideOrders(QString path, PatchOrder& order) for (auto item : orderArray) { order.append(Json::requireString(item)); } - } catch (const JSONValidationError& err) { + } catch ([[maybe_unused]] const JSONValidationError& err) { qCritical() << "Couldn't parse" << orderFile.fileName() << ": bad file format"; qWarning() << "Ignoring overriden order"; order.clear(); diff --git a/launcher/minecraft/Rule.h b/launcher/minecraft/Rule.h index 483cd936f..c6cdbc43f 100644 --- a/launcher/minecraft/Rule.h +++ b/launcher/minecraft/Rule.h @@ -55,7 +55,7 @@ class Rule { public: Rule(RuleAction result) : m_result(result) {} - virtual ~Rule(){}; + virtual ~Rule() {} virtual QJsonObject toJson() = 0; RuleAction apply(const Library* parent, const RuntimeContext& runtimeContext) { diff --git a/launcher/minecraft/World.cpp b/launcher/minecraft/World.cpp index 62e0279a1..1a680ac56 100644 --- a/launcher/minecraft/World.cpp +++ b/launcher/minecraft/World.cpp @@ -368,11 +368,11 @@ optional read_string(nbt::value& parent, const char* name) } auto& tag_str = namedValue.as(); return QString::fromStdString(tag_str.get()); - } catch (const std::out_of_range& e) { + } catch ([[maybe_unused]] const std::out_of_range& e) { // fallback for old world formats qWarning() << "String NBT tag" << name << "could not be found."; return nullopt; - } catch (const std::bad_cast& e) { + } catch ([[maybe_unused]] const std::bad_cast& e) { // type mismatch qWarning() << "NBT tag" << name << "could not be converted to string."; return nullopt; @@ -388,11 +388,11 @@ optional read_long(nbt::value& parent, const char* name) } auto& tag_str = namedValue.as(); return tag_str.get(); - } catch (const std::out_of_range& e) { + } catch ([[maybe_unused]] const std::out_of_range& e) { // fallback for old world formats qWarning() << "Long NBT tag" << name << "could not be found."; return nullopt; - } catch (const std::bad_cast& e) { + } catch ([[maybe_unused]] const std::bad_cast& e) { // type mismatch qWarning() << "NBT tag" << name << "could not be converted to long."; return nullopt; @@ -408,11 +408,11 @@ optional read_int(nbt::value& parent, const char* name) } auto& tag_str = namedValue.as(); return tag_str.get(); - } catch (const std::out_of_range& e) { + } catch ([[maybe_unused]] const std::out_of_range& e) { // fallback for old world formats qWarning() << "Int NBT tag" << name << "could not be found."; return nullopt; - } catch (const std::bad_cast& e) { + } catch ([[maybe_unused]] const std::bad_cast& e) { // type mismatch qWarning() << "NBT tag" << name << "could not be converted to int."; return nullopt; diff --git a/launcher/minecraft/WorldList.cpp b/launcher/minecraft/WorldList.cpp index a7348e570..812b13c71 100644 --- a/launcher/minecraft/WorldList.cpp +++ b/launcher/minecraft/WorldList.cpp @@ -255,7 +255,7 @@ QVariant WorldList::data(const QModelIndex& index, int role) const } } -QVariant WorldList::headerData(int section, Qt::Orientation orientation, int role) const +QVariant WorldList::headerData(int section, [[maybe_unused]] Qt::Orientation orientation, int role) const { switch (role) { case Qt::DisplayRole: @@ -294,7 +294,6 @@ QVariant WorldList::headerData(int section, Qt::Orientation orientation, int rol default: return QVariant(); } - return QVariant(); } QStringList WorldList::mimeTypes() const @@ -339,19 +338,19 @@ QMimeData* WorldList::mimeData(const QModelIndexList& indexes) const if (indexes.size() == 0) return new QMimeData(); - QList worlds; + QList worlds_; for (auto idx : indexes) { if (idx.column() != 0) continue; int row = idx.row(); if (row < 0 || row >= this->worlds.size()) continue; - worlds.append(this->worlds[row]); + worlds_.append(this->worlds[row]); } - if (!worlds.size()) { + if (!worlds_.size()) { return new QMimeData(); } - return new WorldMimeData(worlds); + return new WorldMimeData(worlds_); } Qt::ItemFlags WorldList::flags(const QModelIndex& index) const diff --git a/launcher/minecraft/auth/AccountData.cpp b/launcher/minecraft/auth/AccountData.cpp index b56f49cbd..474bf7c6e 100644 --- a/launcher/minecraft/auth/AccountData.cpp +++ b/launcher/minecraft/auth/AccountData.cpp @@ -311,7 +311,7 @@ bool AccountData::resumeStateFromV2(QJsonObject data) QJsonObject profileObject = profileVal.toObject(); QString id = profileObject.value("id").toString(""); QString name = profileObject.value("name").toString(""); - bool legacy = profileObject.value("legacy").toBool(false); + bool legacy_ = profileObject.value("legacy").toBool(false); if (id.isEmpty() || name.isEmpty()) { qWarning() << "Unable to load a profile" << name << "because it was missing an ID or a name."; continue; @@ -319,7 +319,7 @@ bool AccountData::resumeStateFromV2(QJsonObject data) if (id == currentProfile) { currentProfileIndex = index; } - profiles.append({ id, name, legacy }); + profiles.append({ id, name, legacy_ }); } auto& profile = profiles[currentProfileIndex]; diff --git a/launcher/minecraft/auth/AccountList.cpp b/launcher/minecraft/auth/AccountList.cpp index 317af714b..84dbd8416 100644 --- a/launcher/minecraft/auth/AccountList.cpp +++ b/launcher/minecraft/auth/AccountList.cpp @@ -353,7 +353,7 @@ QVariant AccountList::data(const QModelIndex& index, int role) const } } -QVariant AccountList::headerData(int section, Qt::Orientation orientation, int role) const +QVariant AccountList::headerData(int section, [[maybe_unused]] Qt::Orientation orientation, int role) const { switch (role) { case Qt::DisplayRole: @@ -415,7 +415,7 @@ Qt::ItemFlags AccountList::flags(const QModelIndex& index) const bool AccountList::setData(const QModelIndex& idx, const QVariant& value, int role) { - if (idx.row() < 0 || idx.row() >= rowCount(idx) || !idx.isValid()) { + if (idx.row() < 0 || idx.row() >= rowCount(idx.parent()) || !idx.isValid()) { return false; } @@ -423,7 +423,8 @@ bool AccountList::setData(const QModelIndex& idx, const QVariant& value, int rol if (value == Qt::Checked) { MinecraftAccountPtr account = at(idx.row()); setDefaultAccount(account); - } + } else if (m_defaultAccount == at(idx.row())) + setDefaultAccount(nullptr); } emit dataChanged(idx, index(idx.row(), columnCount(QModelIndex()) - 1)); diff --git a/launcher/minecraft/auth/MinecraftAccount.cpp b/launcher/minecraft/auth/MinecraftAccount.cpp index 1dcea2072..6c2f0805f 100644 --- a/launcher/minecraft/auth/MinecraftAccount.cpp +++ b/launcher/minecraft/auth/MinecraftAccount.cpp @@ -37,6 +37,7 @@ #include "MinecraftAccount.h" +#include #include #include #include @@ -126,6 +127,11 @@ QPixmap MinecraftAccount::getFace() const return QPixmap(); } QPixmap skin = QPixmap(8, 8); +#if QT_VERSION >= QT_VERSION_CHECK(5, 14, 0) + skin.fill(QColorConstants::Transparent); +#else + skin.fill(QColor(0, 0, 0, 0)); +#endif QPainter painter(&skin); painter.drawPixmap(0, 0, skinTexture.copy(8, 8, 8, 8)); painter.drawPixmap(0, 0, skinTexture.copy(40, 8, 8, 8)); diff --git a/launcher/minecraft/auth/Parsers.cpp b/launcher/minecraft/auth/Parsers.cpp index 8dbe446ab..f6179a93e 100644 --- a/launcher/minecraft/auth/Parsers.cpp +++ b/launcher/minecraft/auth/Parsers.cpp @@ -113,16 +113,16 @@ bool parseXTokenResponse(QByteArray& data, Katabasis::Token& output, QString nam if (!item.isObject()) { continue; } - auto obj = item.toObject(); - if (obj.contains("uhs")) { + auto obj_ = item.toObject(); + if (obj_.contains("uhs")) { foundUHS = true; } else { continue; } // consume all 'display claims' ... whatever that means - for (auto iter = obj.begin(); iter != obj.end(); iter++) { + for (auto iter = obj_.begin(); iter != obj_.end(); iter++) { QString claim; - if (!getString(obj.value(iter.key()), claim)) { + if (!getString(obj_.value(iter.key()), claim)) { qWarning() << "display claim " << iter.key() << " is not a string..."; return false; } diff --git a/launcher/minecraft/auth/steps/EntitlementsStep.cpp b/launcher/minecraft/auth/steps/EntitlementsStep.cpp index e942db526..0573dcb6e 100644 --- a/launcher/minecraft/auth/steps/EntitlementsStep.cpp +++ b/launcher/minecraft/auth/steps/EntitlementsStep.cpp @@ -36,7 +36,9 @@ void EntitlementsStep::rehydrate() // NOOP, for now. We only save bools and there's nothing to check. } -void EntitlementsStep::onRequestDone(QNetworkReply::NetworkError error, QByteArray data, QList headers) +void EntitlementsStep::onRequestDone([[maybe_unused]] QNetworkReply::NetworkError error, + QByteArray data, + [[maybe_unused]] QList headers) { auto requestor = qobject_cast(QObject::sender()); requestor->deleteLater(); diff --git a/launcher/minecraft/gameoptions/GameOptions.cpp b/launcher/minecraft/gameoptions/GameOptions.cpp index 443525ae4..4f4fb99a7 100644 --- a/launcher/minecraft/gameoptions/GameOptions.cpp +++ b/launcher/minecraft/gameoptions/GameOptions.cpp @@ -97,12 +97,11 @@ QVariant GameOptions::data(const QModelIndex& index, int role) const default: return QVariant(); } - return QVariant(); } int GameOptions::rowCount(const QModelIndex&) const { - return contents.size(); + return static_cast(contents.size()); } int GameOptions::columnCount(const QModelIndex&) const diff --git a/launcher/minecraft/launch/ExtractNatives.cpp b/launcher/minecraft/launch/ExtractNatives.cpp index cebeaedd4..8f3cac4d1 100644 --- a/launcher/minecraft/launch/ExtractNatives.cpp +++ b/launcher/minecraft/launch/ExtractNatives.cpp @@ -39,7 +39,7 @@ static QString replaceSuffix(QString target, const QString& suffix, const QStrin return target + replacement; } -static bool unzipNatives(QString source, QString targetFolder, bool applyJnilibHack, bool nativeOpenAL, bool nativeGLFW) +static bool unzipNatives(QString source, QString targetFolder, bool applyJnilibHack) { QuaZip zip(source); if (!zip.open(QuaZip::mdUnzip)) { @@ -52,12 +52,6 @@ static bool unzipNatives(QString source, QString targetFolder, bool applyJnilibH do { QString name = zip.getCurrentFileName(); auto lowercase = name.toLower(); - if (nativeGLFW && name.contains("glfw")) { - continue; - } - if (nativeOpenAL && name.contains("openal")) { - continue; - } if (applyJnilibHack) { name = replaceSuffix(name, ".jnilib", ".dylib"); } @@ -83,14 +77,12 @@ void ExtractNatives::executeTask() return; } auto settings = minecraftInstance->settings(); - bool nativeOpenAL = settings->get("UseNativeOpenAL").toBool(); - bool nativeGLFW = settings->get("UseNativeGLFW").toBool(); auto outputPath = minecraftInstance->getNativePath(); auto javaVersion = minecraftInstance->getJavaVersion(); bool jniHackEnabled = javaVersion.major() >= 8; for (const auto& source : toExtract) { - if (!unzipNatives(source, outputPath, jniHackEnabled, nativeOpenAL, nativeGLFW)) { + if (!unzipNatives(source, outputPath, jniHackEnabled)) { const char* reason = QT_TR_NOOP("Couldn't extract native jar '%1' to destination '%2'"); emit logLine(QString(reason).arg(source, outputPath), MessageLevel::Fatal); emitFailed(tr(reason).arg(source, outputPath)); diff --git a/launcher/minecraft/launch/LauncherPartLaunch.cpp b/launcher/minecraft/launch/LauncherPartLaunch.cpp index 05562ef2f..44e5d0a63 100644 --- a/launcher/minecraft/launch/LauncherPartLaunch.cpp +++ b/launcher/minecraft/launch/LauncherPartLaunch.cpp @@ -53,7 +53,7 @@ LauncherPartLaunch::LauncherPartLaunch(LaunchTask* parent) : LaunchStep(parent) auto instance = parent->instance(); if (instance->settings()->get("CloseAfterLaunch").toBool()) { std::shared_ptr connection{ new QMetaObject::Connection }; - *connection = connect(&m_process, &LoggedProcess::log, this, [=](QStringList lines, MessageLevel::Enum level) { + *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(); diff --git a/launcher/minecraft/mod/DataPack.cpp b/launcher/minecraft/mod/DataPack.cpp index 7bf5a3112..fc2d3f68b 100644 --- a/launcher/minecraft/mod/DataPack.cpp +++ b/launcher/minecraft/mod/DataPack.cpp @@ -28,7 +28,7 @@ #include "Version.h" // Values taken from: -// https://minecraft.fandom.com/wiki/Tutorials/Creating_a_data_pack#%22pack_format%22 +// https://minecraft.wiki/w/Tutorials/Creating_a_data_pack#%22pack_format%22 static const QMap> s_pack_format_versions = { { 4, { Version("1.13"), Version("1.14.4") } }, { 5, { Version("1.15"), Version("1.16.1") } }, { 6, { Version("1.16.2"), Version("1.16.5") } }, { 7, { Version("1.17"), Version("1.17.1") } }, diff --git a/launcher/minecraft/mod/DataPack.h b/launcher/minecraft/mod/DataPack.h index fc2703c7a..b3787b238 100644 --- a/launcher/minecraft/mod/DataPack.h +++ b/launcher/minecraft/mod/DataPack.h @@ -63,7 +63,7 @@ class DataPack : public Resource { mutable QMutex m_data_lock; /* The 'version' of a data pack, as defined in the pack.mcmeta file. - * See https://minecraft.fandom.com/wiki/Data_pack#pack.mcmeta + * See https://minecraft.wiki/w/Data_pack#pack.mcmeta */ int m_pack_format = 0; diff --git a/launcher/minecraft/mod/Mod.cpp b/launcher/minecraft/mod/Mod.cpp index ae3dea8d8..310946379 100644 --- a/launcher/minecraft/mod/Mod.cpp +++ b/launcher/minecraft/mod/Mod.cpp @@ -132,17 +132,23 @@ auto Mod::destroy(QDir& index_dir, bool preserve_metadata, bool attempt_trash) - if (!preserve_metadata) { qDebug() << QString("Destroying metadata for '%1' on purpose").arg(name()); - if (metadata()) { - Metadata::remove(index_dir, metadata()->slug); - } else { - auto n = name(); - Metadata::remove(index_dir, n); - } + destroyMetadata(index_dir); } return Resource::destroy(attempt_trash); } +void Mod::destroyMetadata(QDir& index_dir) +{ + if (metadata()) { + Metadata::remove(index_dir, metadata()->slug); + } else { + auto n = name(); + Metadata::remove(index_dir, n); + } + m_local_details.metadata = nullptr; +} + auto Mod::details() const -> const ModDetails& { return m_local_details; @@ -246,7 +252,8 @@ void Mod::setIcon(QImage new_image) const PixmapCache::remove(m_pack_image_cache_key.key); // scale the image to avoid flooding the pixmapcache - auto pixmap = QPixmap::fromImage(new_image.scaled({ 64, 64 }, Qt::AspectRatioMode::KeepAspectRatioByExpanding)); + 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; @@ -259,7 +266,7 @@ QPixmap Mod::icon(QSize size, Qt::AspectRatioMode mode) const if (PixmapCache::find(m_pack_image_cache_key.key, &cached_image)) { if (size.isNull()) return cached_image; - return cached_image.scaled(size, mode); + return cached_image.scaled(size, mode, Qt::SmoothTransformation); } // No valid image we can get diff --git a/launcher/minecraft/mod/Mod.h b/launcher/minecraft/mod/Mod.h index ca3ee11fa..e97ee9d3b 100644 --- a/launcher/minecraft/mod/Mod.h +++ b/launcher/minecraft/mod/Mod.h @@ -72,7 +72,7 @@ class Mod : public Resource { auto metaurl() const -> QString; /** Get the intneral path to the mod's icon file*/ - QString iconPath() const { return m_local_details.icon_file; }; + QString iconPath() const { return m_local_details.icon_file; } /** 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. */ @@ -93,6 +93,8 @@ class Mod : public Resource { // Delete all the files of this mod auto destroy(QDir& index_dir, bool preserve_metadata = false, bool attempt_trash = true) -> bool; + // Delete the metadata only + void destroyMetadata(QDir& index_dir); void finishResolvingWithDetails(ModDetails&& details); diff --git a/launcher/minecraft/mod/ModDetails.h b/launcher/minecraft/mod/ModDetails.h index 6a15219a2..a00d5a24b 100644 --- a/launcher/minecraft/mod/ModDetails.h +++ b/launcher/minecraft/mod/ModDetails.h @@ -60,17 +60,17 @@ struct ModLicense { ModLicense(const QString license) { - // FIXME: come up with a better license parseing. + // FIXME: come up with a better license parsing. // handle SPDX identifiers? https://spdx.org/licenses/ auto parts = license.split(' '); QStringList notNameParts = {}; for (auto part : parts) { - auto url = QUrl(part); + auto _url = QUrl(part); if (part.startsWith("(") && part.endsWith(")")) - url = QUrl(part.mid(1, part.size() - 2)); + _url = QUrl(part.mid(1, part.size() - 2)); - if (url.isValid() && !url.scheme().isEmpty() && !url.host().isEmpty()) { - this->url = url.toString(); + if (_url.isValid() && !_url.scheme().isEmpty() && !_url.host().isEmpty()) { + this->url = _url.toString(); notNameParts.append(part); continue; } @@ -89,13 +89,9 @@ struct ModLicense { } } - ModLicense(const QString name, const QString id, const QString url, const QString description) - { - this->name = name; - this->id = id; - this->url = url; - this->description = description; - } + ModLicense(const QString& name_, const QString& id_, const QString& url_, const QString& description_) + : name(name_), id(id_), url(url_), description(description_) + {} ModLicense(const ModLicense& other) : name(other.name), id(other.id), url(other.url), description(other.description) {} diff --git a/launcher/minecraft/mod/ModFolderModel.cpp b/launcher/minecraft/mod/ModFolderModel.cpp index 8dbe583da..a5f1489dd 100644 --- a/launcher/minecraft/mod/ModFolderModel.cpp +++ b/launcher/minecraft/mod/ModFolderModel.cpp @@ -51,8 +51,13 @@ #include "Application.h" +#include "Json.h" #include "minecraft/mod/tasks/LocalModParseTask.h" +#include "minecraft/mod/tasks/LocalModUpdateTask.h" #include "minecraft/mod/tasks/ModFolderLoadTask.h" +#include "modplatform/ModIndex.h" +#include "modplatform/flame/FlameAPI.h" +#include "modplatform/flame/FlameModIndex.h" ModFolderModel::ModFolderModel(const QString& dir, BaseInstance* instance, bool is_indexed, bool create_dir) : ResourceFolderModel(QDir(dir), instance, nullptr, create_dir), m_is_indexed(is_indexed) @@ -62,6 +67,7 @@ ModFolderModel::ModFolderModel(const QString& dir, BaseInstance* instance, bool m_column_sort_keys = { SortType::ENABLED, SortType::NAME, SortType::NAME, SortType::VERSION, SortType::DATE, SortType::PROVIDER }; m_column_resize_modes = { QHeaderView::ResizeToContents, QHeaderView::Interactive, QHeaderView::Stretch, QHeaderView::ResizeToContents, QHeaderView::ResizeToContents, QHeaderView::ResizeToContents }; + m_columnsHideable = { false, true, false, true, true, true }; } QVariant ModFolderModel::data(const QModelIndex& index, int role) const @@ -137,7 +143,7 @@ QVariant ModFolderModel::data(const QModelIndex& index, int role) const } } -QVariant ModFolderModel::headerData(int section, Qt::Orientation orientation, int role) const +QVariant ModFolderModel::headerData(int section, [[maybe_unused]] Qt::Orientation orientation, int role) const { switch (role) { case Qt::DisplayRole: @@ -227,6 +233,25 @@ bool ModFolderModel::deleteMods(const QModelIndexList& indexes) return true; } +bool ModFolderModel::deleteModsMetadata(const QModelIndexList& indexes) +{ + if (indexes.isEmpty()) + return true; + + for (auto i : indexes) { + if (i.column() != 0) { + continue; + } + auto m = at(i.row()); + auto index_dir = indexDir(); + m->destroyMetadata(index_dir); + } + + update(); + + return true; +} + bool ModFolderModel::isValid() { return m_dir.exists() && m_dir.isReadable(); @@ -308,3 +333,47 @@ void ModFolderModel::onParseSucceeded(int ticket, QString mod_id) emit dataChanged(index(row), index(row, columnCount(QModelIndex()) - 1)); } + +static const FlameAPI flameAPI; +bool ModFolderModel::installMod(QString file_path, ModPlatform::IndexedVersion& vers) +{ + if (vers.addonId.isValid()) { + ModPlatform::IndexedPack pack{ + vers.addonId, + ModPlatform::ResourceProvider::FLAME, + }; + + QEventLoop loop; + + auto response = std::make_shared(); + auto job = flameAPI.getProject(vers.addonId.toString(), response); + + QObject::connect(job.get(), &Task::failed, [&loop] { loop.quit(); }); + QObject::connect(job.get(), &Task::aborted, &loop, &QEventLoop::quit); + QObject::connect(job.get(), &Task::succeeded, [response, this, &vers, &loop, &pack] { + QJsonParseError parse_error{}; + QJsonDocument doc = QJsonDocument::fromJson(*response, &parse_error); + if (parse_error.error != QJsonParseError::NoError) { + qWarning() << "Error while parsing JSON response for mod info at " << parse_error.offset + << " reason: " << parse_error.errorString(); + qDebug() << *response; + return; + } + try { + auto obj = Json::requireObject(Json::requireObject(doc), "data"); + FlameMod::loadIndexedPack(pack, obj); + } catch (const JSONValidationError& e) { + qDebug() << doc; + qWarning() << "Error while reading mod info: " << e.cause(); + } + LocalModUpdateTask update_metadata(indexDir(), pack, vers); + QObject::connect(&update_metadata, &Task::finished, &loop, &QEventLoop::quit); + update_metadata.start(); + }); + + job->start(); + + loop.exec(); + } + return ResourceFolderModel::installResource(file_path); +} diff --git a/launcher/minecraft/mod/ModFolderModel.h b/launcher/minecraft/mod/ModFolderModel.h index 06fd78149..61d840f9b 100644 --- a/launcher/minecraft/mod/ModFolderModel.h +++ b/launcher/minecraft/mod/ModFolderModel.h @@ -48,6 +48,7 @@ #include "minecraft/mod/tasks/LocalModParseTask.h" #include "minecraft/mod/tasks/ModFolderLoadTask.h" +#include "modplatform/ModIndex.h" class LegacyInstance; class BaseInstance; @@ -75,10 +76,12 @@ class ModFolderModel : public ResourceFolderModel { [[nodiscard]] Task* createParseTask(Resource&) override; bool installMod(QString file_path) { return ResourceFolderModel::installResource(file_path); } + bool installMod(QString file_path, ModPlatform::IndexedVersion& vers); bool uninstallMod(const QString& filename, bool preserve_metadata = false); /// Deletes all the selected mods bool deleteMods(const QModelIndexList& indexes); + bool deleteModsMetadata(const QModelIndexList& indexes); bool isValid(); diff --git a/launcher/minecraft/mod/ResourceFolderModel.cpp b/launcher/minecraft/mod/ResourceFolderModel.cpp index 0106d5c9b..d3237b34b 100644 --- a/launcher/minecraft/mod/ResourceFolderModel.cpp +++ b/launcher/minecraft/mod/ResourceFolderModel.cpp @@ -447,7 +447,7 @@ QVariant ResourceFolderModel::data(const QModelIndex& index, int role) const } } -bool ResourceFolderModel::setData(const QModelIndex& index, const QVariant& value, int role) +bool ResourceFolderModel::setData(const QModelIndex& index, [[maybe_unused]] const QVariant& value, int role) { int row = index.row(); if (row < 0 || row >= rowCount(index.parent()) || !index.isValid()) @@ -471,7 +471,7 @@ bool ResourceFolderModel::setData(const QModelIndex& index, const QVariant& valu return false; } -QVariant ResourceFolderModel::headerData(int section, Qt::Orientation orientation, int role) const +QVariant ResourceFolderModel::headerData(int section, [[maybe_unused]] Qt::Orientation orientation, int role) const { switch (role) { case Qt::DisplayRole: @@ -551,6 +551,9 @@ QMenu* ResourceFolderModel::createHeaderContextMenu(QTreeView* tree) menu->addSeparator()->setText(tr("Show / Hide Columns")); for (int col = 0; col < columnCount(); ++col) { + // Skip creating actions for columns that should not be hidden + if (!m_columnsHideable.at(col)) + continue; auto act = new QAction(menu); setupHeaderAction(act, col); @@ -584,7 +587,8 @@ SortType ResourceFolderModel::columnToSortKey(size_t column) const } /* Standard Proxy Model for createFilterProxyModel */ -[[nodiscard]] bool ResourceFolderModel::ProxyModel::filterAcceptsRow(int source_row, const QModelIndex& source_parent) const +[[nodiscard]] bool ResourceFolderModel::ProxyModel::filterAcceptsRow(int source_row, + [[maybe_unused]] const QModelIndex& source_parent) const { auto* model = qobject_cast(sourceModel()); if (!model) diff --git a/launcher/minecraft/mod/ResourceFolderModel.h b/launcher/minecraft/mod/ResourceFolderModel.h index 595b9762e..80c31e456 100644 --- a/launcher/minecraft/mod/ResourceFolderModel.h +++ b/launcher/minecraft/mod/ResourceFolderModel.h @@ -49,8 +49,8 @@ class ResourceFolderModel : public QAbstractListModel { bool stopWatching(const QStringList paths); /* Helper methods for subclasses, using a predetermined list of paths. */ - virtual bool startWatching() { return startWatching({ m_dir.absolutePath() }); }; - virtual bool stopWatching() { return stopWatching({ m_dir.absolutePath() }); }; + virtual bool startWatching() { return startWatching({ m_dir.absolutePath() }); } + virtual bool stopWatching() { return stopWatching({ m_dir.absolutePath() }); } /** Given a path in the system, install that resource, moving it to its place in the * instance file hierarchy. @@ -78,7 +78,7 @@ class ResourceFolderModel : public QAbstractListModel { /** Creates a new parse task, if needed, for 'res' and start it.*/ virtual void resolveResource(Resource* res); - [[nodiscard]] size_t size() const { return m_resources.size(); }; + [[nodiscard]] qsizetype size() const { return m_resources.size(); } [[nodiscard]] bool empty() const { return size() == 0; } [[nodiscard]] Resource& at(int index) { return *m_resources.at(index); } [[nodiscard]] Resource const& at(int index) const { return *m_resources.at(index); } @@ -97,10 +97,10 @@ class ResourceFolderModel : public QAbstractListModel { /* Basic columns */ enum Columns { ACTIVE_COLUMN = 0, NAME_COLUMN, DATE_COLUMN, NUM_COLUMNS }; - QStringList columnNames(bool translated = true) const { return translated ? m_column_names_translated : m_column_names; }; + QStringList columnNames(bool translated = true) const { return translated ? m_column_names_translated : m_column_names; } [[nodiscard]] int rowCount(const QModelIndex& parent = {}) const override { return parent.isValid() ? 0 : static_cast(size()); } - [[nodiscard]] int columnCount(const QModelIndex& parent = {}) const override { return parent.isValid() ? 0 : NUM_COLUMNS; }; + [[nodiscard]] int columnCount(const QModelIndex& parent = {}) const override { return parent.isValid() ? 0 : NUM_COLUMNS; } [[nodiscard]] Qt::DropActions supportedDropActions() const override; @@ -159,7 +159,7 @@ class ResourceFolderModel : public QAbstractListModel { * This task should load and parse all heavy info needed by a resource, such as parsing a manifest. It gets executed * in the background, so it slowly updates the UI as tasks get done. */ - [[nodiscard]] virtual Task* createParseTask(Resource&) { return nullptr; }; + [[nodiscard]] virtual Task* createParseTask(Resource&) { return nullptr; } /** Standard implementation of the model update logic. * @@ -203,6 +203,7 @@ class ResourceFolderModel : public QAbstractListModel { QStringList m_column_names_translated = { tr("Enable"), tr("Name"), tr("Last Modified") }; QList m_column_resize_modes = { QHeaderView::ResizeToContents, QHeaderView::Stretch, QHeaderView::ResizeToContents }; + QList m_columnsHideable = { false, false, true }; QDir m_dir; BaseInstance* m_instance; @@ -224,15 +225,15 @@ class ResourceFolderModel : public QAbstractListModel { /* A macro to define useful functions to handle Resource* -> T* more easily on derived classes */ #define RESOURCE_HELPERS(T) \ - [[nodiscard]] T* operator[](size_t index) \ + [[nodiscard]] T* operator[](int index) \ { \ return static_cast(m_resources[index].get()); \ } \ - [[nodiscard]] T* at(size_t index) \ + [[nodiscard]] T* at(int index) \ { \ return static_cast(m_resources[index].get()); \ } \ - [[nodiscard]] const T* at(size_t index) const \ + [[nodiscard]] const T* at(int index) const \ { \ return static_cast(m_resources.at(index).get()); \ } \ diff --git a/launcher/minecraft/mod/ResourcePack.cpp b/launcher/minecraft/mod/ResourcePack.cpp index dab0f6d67..074534405 100644 --- a/launcher/minecraft/mod/ResourcePack.cpp +++ b/launcher/minecraft/mod/ResourcePack.cpp @@ -11,7 +11,7 @@ #include "minecraft/mod/tasks/LocalResourcePackParseTask.h" // Values taken from: -// https://minecraft.fandom.com/wiki/Tutorials/Creating_a_resource_pack#Formatting_pack.mcmeta +// https://minecraft.wiki/w/Tutorials/Creating_a_resource_pack#Formatting_pack.mcmeta static const QMap> s_pack_format_versions = { { 1, { Version("1.6.1"), Version("1.8.9") } }, { 2, { Version("1.9"), Version("1.10.2") } }, { 3, { Version("1.11"), Version("1.12.2") } }, { 4, { Version("1.13"), Version("1.14.4") } }, @@ -50,7 +50,8 @@ void ResourcePack::setImage(QImage new_image) const PixmapCache::instance().remove(m_pack_image_cache_key.key); // scale the image to avoid flooding the pixmapcache - auto pixmap = QPixmap::fromImage(new_image.scaled({ 64, 64 }, Qt::AspectRatioMode::KeepAspectRatioByExpanding)); + auto pixmap = + QPixmap::fromImage(new_image.scaled({ 64, 64 }, Qt::AspectRatioMode::KeepAspectRatioByExpanding, Qt::SmoothTransformation)); m_pack_image_cache_key.key = PixmapCache::instance().insert(pixmap); m_pack_image_cache_key.was_ever_used = true; @@ -68,7 +69,7 @@ QPixmap ResourcePack::image(QSize size, Qt::AspectRatioMode mode) const if (PixmapCache::instance().find(m_pack_image_cache_key.key, &cached_image)) { if (size.isNull()) return cached_image; - return cached_image.scaled(size, mode); + return cached_image.scaled(size, mode, Qt::SmoothTransformation); } // No valid image we can get diff --git a/launcher/minecraft/mod/ResourcePack.h b/launcher/minecraft/mod/ResourcePack.h index da354bc1c..c06f3793d 100644 --- a/launcher/minecraft/mod/ResourcePack.h +++ b/launcher/minecraft/mod/ResourcePack.h @@ -51,7 +51,7 @@ class ResourcePack : public Resource { mutable QMutex m_data_lock; /* The 'version' of a resource pack, as defined in the pack.mcmeta file. - * See https://minecraft.fandom.com/wiki/Tutorials/Creating_a_resource_pack#Formatting_pack.mcmeta + * See https://minecraft.wiki/w/Tutorials/Creating_a_resource_pack#Formatting_pack.mcmeta */ int m_pack_format = 0; diff --git a/launcher/minecraft/mod/ResourcePackFolderModel.cpp b/launcher/minecraft/mod/ResourcePackFolderModel.cpp index 70206cb00..f27431576 100644 --- a/launcher/minecraft/mod/ResourcePackFolderModel.cpp +++ b/launcher/minecraft/mod/ResourcePackFolderModel.cpp @@ -54,6 +54,7 @@ ResourcePackFolderModel::ResourcePackFolderModel(const QString& dir, BaseInstanc m_column_sort_keys = { SortType::ENABLED, SortType::NAME, SortType::NAME, SortType::PACK_FORMAT, SortType::DATE }; m_column_resize_modes = { QHeaderView::ResizeToContents, QHeaderView::Interactive, QHeaderView::Stretch, QHeaderView::ResizeToContents, QHeaderView::ResizeToContents }; + m_columnsHideable = { false, true, false, true, true }; } QVariant ResourcePackFolderModel::data(const QModelIndex& index, int role) const @@ -128,7 +129,7 @@ QVariant ResourcePackFolderModel::data(const QModelIndex& index, int role) const } } -QVariant ResourcePackFolderModel::headerData(int section, Qt::Orientation orientation, int role) const +QVariant ResourcePackFolderModel::headerData(int section, [[maybe_unused]] Qt::Orientation orientation, int role) const { switch (role) { case Qt::DisplayRole: @@ -165,7 +166,6 @@ QVariant ResourcePackFolderModel::headerData(int section, Qt::Orientation orient default: return {}; } - return {}; } int ResourcePackFolderModel::columnCount(const QModelIndex& parent) const diff --git a/launcher/minecraft/mod/TexturePack.cpp b/launcher/minecraft/mod/TexturePack.cpp index 7d8c67137..04cc36310 100644 --- a/launcher/minecraft/mod/TexturePack.cpp +++ b/launcher/minecraft/mod/TexturePack.cpp @@ -44,7 +44,8 @@ void TexturePack::setImage(QImage new_image) const PixmapCache::remove(m_pack_image_cache_key.key); // scale the image to avoid flooding the pixmapcache - auto pixmap = QPixmap::fromImage(new_image.scaled({ 64, 64 }, Qt::AspectRatioMode::KeepAspectRatioByExpanding)); + 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; @@ -56,7 +57,7 @@ QPixmap TexturePack::image(QSize size, Qt::AspectRatioMode mode) const if (PixmapCache::find(m_pack_image_cache_key.key, &cached_image)) { if (size.isNull()) return cached_image; - return cached_image.scaled(size, mode); + return cached_image.scaled(size, mode, Qt::SmoothTransformation); } // No valid image we can get diff --git a/launcher/minecraft/mod/TexturePackFolderModel.cpp b/launcher/minecraft/mod/TexturePackFolderModel.cpp index 9a9910faf..5c5f2b7c1 100644 --- a/launcher/minecraft/mod/TexturePackFolderModel.cpp +++ b/launcher/minecraft/mod/TexturePackFolderModel.cpp @@ -49,6 +49,7 @@ TexturePackFolderModel::TexturePackFolderModel(const QString& dir, BaseInstance* m_column_sort_keys = { SortType::ENABLED, SortType::NAME, SortType::NAME, SortType::DATE }; m_column_resize_modes = { QHeaderView::ResizeToContents, QHeaderView::Interactive, QHeaderView::Stretch, QHeaderView::ResizeToContents }; + m_columnsHideable = { false, true, false, true }; } Task* TexturePackFolderModel::createUpdateTask() @@ -113,7 +114,7 @@ QVariant TexturePackFolderModel::data(const QModelIndex& index, int role) const } } -QVariant TexturePackFolderModel::headerData(int section, Qt::Orientation orientation, int role) const +QVariant TexturePackFolderModel::headerData(int section, [[maybe_unused]] Qt::Orientation orientation, int role) const { switch (role) { case Qt::DisplayRole: diff --git a/launcher/minecraft/mod/tasks/GetModDependenciesTask.cpp b/launcher/minecraft/mod/tasks/GetModDependenciesTask.cpp index f8ecdb33e..ee84b1f5e 100644 --- a/launcher/minecraft/mod/tasks/GetModDependenciesTask.cpp +++ b/launcher/minecraft/mod/tasks/GetModDependenciesTask.cpp @@ -39,9 +39,9 @@ static Version mcVersion(BaseInstance* inst) return static_cast(inst)->getPackProfile()->getComponent("net.minecraft")->getVersion(); } -static ResourceAPI::ModLoaderTypes mcLoaders(BaseInstance* inst) +static ModPlatform::ModLoaderTypes mcLoaders(BaseInstance* inst) { - return static_cast(inst)->getPackProfile()->getModLoaders().value(); + return static_cast(inst)->getPackProfile()->getSupportedModLoaders().value(); } GetModDependenciesTask::GetModDependenciesTask(QObject* parent, @@ -61,7 +61,7 @@ GetModDependenciesTask::GetModDependenciesTask(QObject* parent, if (auto meta = mod->metadata(); meta) m_mods.append(meta); prepare(); -}; +} void GetModDependenciesTask::prepare() { @@ -75,7 +75,7 @@ void GetModDependenciesTask::prepare() ModPlatform::Dependency GetModDependenciesTask::getOverride(const ModPlatform::Dependency& dep, const ModPlatform::ResourceProvider providerName) { - if (auto isQuilt = m_loaderType & ResourceAPI::Quilt; isQuilt || m_loaderType & ResourceAPI::Fabric) { + if (auto isQuilt = m_loaderType & ModPlatform::Quilt; isQuilt || m_loaderType & ModPlatform::Fabric) { auto overide = ModPlatform::getOverrideDeps(); auto over = std::find_if(overide.cbegin(), overide.cend(), [dep, providerName, isQuilt](auto o) { return o.provider == providerName && dep.addonId == (isQuilt ? o.fabric : o.quilt); @@ -130,7 +130,7 @@ QList GetModDependenciesTask::getDependenciesForVersion c_dependencies.append(getOverride(ver_dep, providerName)); } return c_dependencies; -}; +} Task::Ptr GetModDependenciesTask::getProjectInfoTask(std::shared_ptr pDep) { @@ -181,7 +181,7 @@ Task::Ptr GetModDependenciesTask::prepareDependencyTask(const ModPlatform::Depen ResourceAPI::DependencySearchArgs args = { dep, m_version, m_loaderType }; ResourceAPI::DependencySearchCallbacks callbacks; - callbacks.on_succeed = [dep, provider, pDep, level, this](auto& doc, auto& pack) { + callbacks.on_succeed = [dep, provider, pDep, level, this](auto& doc, [[maybe_unused]] auto& pack) { try { QJsonArray arr; if (dep.version.length() != 0 && doc.isObject()) { @@ -191,7 +191,7 @@ Task::Ptr GetModDependenciesTask::prepareDependencyTask(const ModPlatform::Depen } pDep->version = provider.mod->loadDependencyVersions(dep, arr); if (!pDep->version.addonId.isValid()) { - if (m_loaderType & ResourceAPI::Quilt) { // falback for quilt + if (m_loaderType & ModPlatform::Quilt) { // falback for quilt auto overide = ModPlatform::getOverrideDeps(); auto over = std::find_if(overide.cbegin(), overide.cend(), [dep, provider](auto o) { return o.provider == provider.name && dep.addonId == o.quilt; }); @@ -201,6 +201,7 @@ Task::Ptr GetModDependenciesTask::prepareDependencyTask(const ModPlatform::Depen return; } } + removePack(dep.addonId); qWarning() << "Error while reading mod version empty "; qDebug() << doc; return; @@ -215,27 +216,27 @@ Task::Ptr GetModDependenciesTask::prepareDependencyTask(const ModPlatform::Depen return; } if (level == 0) { - qWarning() << "Dependency cycle exeeded"; + qWarning() << "Dependency cycle exceeded"; return; } if (dep.addonId.toString().isEmpty() && !pDep->version.addonId.toString().isEmpty()) { pDep->pack->addonId = pDep->version.addonId; - auto dep = getOverride({ pDep->version.addonId, pDep->dependency.type }, provider.name); - if (dep.addonId != pDep->version.addonId) { + auto dep_ = getOverride({ pDep->version.addonId, pDep->dependency.type }, provider.name); + if (dep_.addonId != pDep->version.addonId) { removePack(pDep->version.addonId); - addTask(prepareDependencyTask(dep, provider.name, level)); + addTask(prepareDependencyTask(dep_, provider.name, level)); } else addTask(getProjectInfoTask(pDep)); } - for (auto dep : getDependenciesForVersion(pDep->version, provider.name)) { - addTask(prepareDependencyTask(dep, provider.name, level - 1)); + for (auto dep_ : getDependenciesForVersion(pDep->version, provider.name)) { + addTask(prepareDependencyTask(dep_, provider.name, level - 1)); } }; auto version = provider.api->getDependencyVersion(std::move(args), std::move(callbacks)); tasks->addTask(version); return tasks; -}; +} void GetModDependenciesTask::removePack(const QVariant addonId) { diff --git a/launcher/minecraft/mod/tasks/GetModDependenciesTask.h b/launcher/minecraft/mod/tasks/GetModDependenciesTask.h index 50eba6afc..a8b9953d3 100644 --- a/launcher/minecraft/mod/tasks/GetModDependenciesTask.h +++ b/launcher/minecraft/mod/tasks/GetModDependenciesTask.h @@ -80,5 +80,5 @@ class GetModDependenciesTask : public SequentialTask { Provider m_modrinth_provider; Version m_version; - ResourceAPI::ModLoaderTypes m_loaderType; + ModPlatform::ModLoaderTypes m_loaderType; }; diff --git a/launcher/minecraft/mod/tasks/LocalDataPackParseTask.cpp b/launcher/minecraft/mod/tasks/LocalDataPackParseTask.cpp index 5bb448778..82f6b9df9 100644 --- a/launcher/minecraft/mod/tasks/LocalDataPackParseTask.cpp +++ b/launcher/minecraft/mod/tasks/LocalDataPackParseTask.cpp @@ -133,7 +133,7 @@ bool processZIP(DataPack& pack, ProcessingLevel level) return true; } -// https://minecraft.fandom.com/wiki/Data_pack#pack.mcmeta +// https://minecraft.wiki/w/Data_pack#pack.mcmeta bool processMCMeta(DataPack& pack, QByteArray&& raw_data) { try { diff --git a/launcher/minecraft/mod/tasks/LocalModParseTask.cpp b/launcher/minecraft/mod/tasks/LocalModParseTask.cpp index 75bb6d25c..7ec00c0c6 100644 --- a/launcher/minecraft/mod/tasks/LocalModParseTask.cpp +++ b/launcher/minecraft/mod/tasks/LocalModParseTask.cpp @@ -104,14 +104,15 @@ ModDetails ReadMCModTOML(QByteArray contents) #if TOML_EXCEPTIONS try { tomlData = toml::parse(contents.toStdString()); - } catch (const toml::parse_error& err) { + } catch ([[maybe_unused]] const toml::parse_error& err) { return {}; } #else - tomlData = toml::parse(contents.toStdString()); - if (!tomlData) { + toml::parse_result result = toml::parse(contents.toStdString()); + if (!result) { return {}; } + tomlData = result.table(); #endif // array defined by [[mods]] @@ -151,8 +152,8 @@ ModDetails ReadMCModTOML(QByteArray contents) QString authors = ""; if (auto authorsDatum = tomlData["authors"].as_string()) { authors = QString::fromStdString(authorsDatum->get()); - } else if (auto authorsDatum = (*modsTable)["authors"].as_string()) { - authors = QString::fromStdString(authorsDatum->get()); + } else if (auto authorsDatumMods = (*modsTable)["authors"].as_string()) { + authors = QString::fromStdString(authorsDatumMods->get()); } if (!authors.isEmpty()) { details.authors.append(authors); @@ -161,8 +162,8 @@ ModDetails ReadMCModTOML(QByteArray contents) QString homeurl = ""; if (auto homeurlDatum = tomlData["displayURL"].as_string()) { homeurl = QString::fromStdString(homeurlDatum->get()); - } else if (auto homeurlDatum = (*modsTable)["displayURL"].as_string()) { - homeurl = QString::fromStdString(homeurlDatum->get()); + } else if (auto homeurlDatumMods = (*modsTable)["displayURL"].as_string()) { + homeurl = QString::fromStdString(homeurlDatumMods->get()); } // fix up url. if (!homeurl.isEmpty() && !homeurl.startsWith("http://") && !homeurl.startsWith("https://") && !homeurl.startsWith("ftp://")) { @@ -173,16 +174,16 @@ ModDetails ReadMCModTOML(QByteArray contents) QString issueTrackerURL = ""; if (auto issueTrackerURLDatum = tomlData["issueTrackerURL"].as_string()) { issueTrackerURL = QString::fromStdString(issueTrackerURLDatum->get()); - } else if (auto issueTrackerURLDatum = (*modsTable)["issueTrackerURL"].as_string()) { - issueTrackerURL = QString::fromStdString(issueTrackerURLDatum->get()); + } else if (auto issueTrackerURLDatumMods = (*modsTable)["issueTrackerURL"].as_string()) { + issueTrackerURL = QString::fromStdString(issueTrackerURLDatumMods->get()); } details.issue_tracker = issueTrackerURL; QString license = ""; if (auto licenseDatum = tomlData["license"].as_string()) { license = QString::fromStdString(licenseDatum->get()); - } else if (auto licenseDatum = (*modsTable)["license"].as_string()) { - license = QString::fromStdString(licenseDatum->get()); + } else if (auto licenseDatumMods = (*modsTable)["license"].as_string()) { + license = QString::fromStdString(licenseDatumMods->get()); } if (!license.isEmpty()) details.licenses.append(ModLicense(license)); @@ -190,8 +191,8 @@ ModDetails ReadMCModTOML(QByteArray contents) QString logoFile = ""; if (auto logoFileDatum = tomlData["logoFile"].as_string()) { logoFile = QString::fromStdString(logoFileDatum->get()); - } else if (auto logoFileDatum = (*modsTable)["logoFile"].as_string()) { - logoFile = QString::fromStdString(logoFileDatum->get()); + } else if (auto logoFileDatumMods = (*modsTable)["logoFile"].as_string()) { + logoFile = QString::fromStdString(logoFileDatumMods->get()); } details.icon_file = logoFile; @@ -458,7 +459,7 @@ bool process(Mod& mod, ProcessingLevel level) } } -bool processZIP(Mod& mod, ProcessingLevel level) +bool processZIP(Mod& mod, [[maybe_unused]] ProcessingLevel level) { ModDetails details; @@ -591,7 +592,7 @@ bool processZIP(Mod& mod, ProcessingLevel level) return false; // no valid mod found in archive } -bool processFolder(Mod& mod, ProcessingLevel level) +bool processFolder(Mod& mod, [[maybe_unused]] ProcessingLevel level) { ModDetails details; @@ -612,7 +613,7 @@ bool processFolder(Mod& mod, ProcessingLevel level) return false; // no valid mcmod.info file found } -bool processLitemod(Mod& mod, ProcessingLevel level) +bool processLitemod(Mod& mod, [[maybe_unused]] ProcessingLevel level) { ModDetails details; diff --git a/launcher/minecraft/mod/tasks/LocalResourcePackParseTask.cpp b/launcher/minecraft/mod/tasks/LocalResourcePackParseTask.cpp index 73cbf891c..7b9f4f594 100644 --- a/launcher/minecraft/mod/tasks/LocalResourcePackParseTask.cpp +++ b/launcher/minecraft/mod/tasks/LocalResourcePackParseTask.cpp @@ -178,7 +178,7 @@ bool processZIP(ResourcePack& pack, ProcessingLevel level) return true; } -// https://minecraft.fandom.com/wiki/Tutorials/Creating_a_resource_pack#Formatting_pack.mcmeta +// https://minecraft.wiki/w/Tutorials/Creating_a_resource_pack#Formatting_pack.mcmeta bool processMCMeta(ResourcePack& pack, QByteArray&& raw_data) { try { diff --git a/launcher/minecraft/services/CapeChange.cpp b/launcher/minecraft/services/CapeChange.cpp index f87da13e8..2ba38a6af 100644 --- a/launcher/minecraft/services/CapeChange.cpp +++ b/launcher/minecraft/services/CapeChange.cpp @@ -42,7 +42,7 @@ CapeChange::CapeChange(QObject* parent, QString token, QString cape) : Task(parent), m_capeId(cape), m_token(token) {} -void CapeChange::setCape(QString& cape) +void CapeChange::setCape([[maybe_unused]] QString& cape) { QNetworkRequest request(QUrl("https://api.minecraftservices.com/minecraft/profile/capes/active")); auto requestString = QString("{\"capeId\":\"%1\"}").arg(m_capeId); diff --git a/launcher/minecraft/update/AssetUpdateTask.cpp b/launcher/minecraft/update/AssetUpdateTask.cpp index 7043d9878..8af014996 100644 --- a/launcher/minecraft/update/AssetUpdateTask.cpp +++ b/launcher/minecraft/update/AssetUpdateTask.cpp @@ -7,6 +7,8 @@ #include "Application.h" +#include "net/ApiDownload.h" + AssetUpdateTask::AssetUpdateTask(MinecraftInstance* inst) { m_inst = inst; @@ -29,7 +31,7 @@ void AssetUpdateTask::executeTask() entry->setStale(true); auto hexSha1 = assets->sha1.toLatin1(); qDebug() << "Asset index SHA1:" << hexSha1; - auto dl = Net::Download::makeCached(indexUrl, entry); + auto dl = Net::ApiDownload::makeCached(indexUrl, entry); auto rawSha1 = QByteArray::fromHex(assets->sha1.toLatin1()); dl->addValidator(new Net::ChecksumValidator(QCryptographicHash::Sha1, rawSha1)); job->addNetAction(dl); diff --git a/launcher/minecraft/update/FMLLibrariesTask.cpp b/launcher/minecraft/update/FMLLibrariesTask.cpp index a6ca21021..ce0c9a723 100644 --- a/launcher/minecraft/update/FMLLibrariesTask.cpp +++ b/launcher/minecraft/update/FMLLibrariesTask.cpp @@ -8,6 +8,8 @@ #include "Application.h" #include "BuildConfig.h" +#include "net/ApiDownload.h" + FMLLibrariesTask::FMLLibrariesTask(MinecraftInstance* inst) { m_inst = inst; @@ -62,7 +64,7 @@ void FMLLibrariesTask::executeTask() for (auto& lib : fmlLibsToProcess) { auto entry = metacache->resolveEntry("fmllibs", lib.filename); QString urlString = BuildConfig.FMLLIBS_BASE_URL + lib.filename; - dljob->addNetAction(Net::Download::makeCached(QUrl(urlString), entry, options)); + dljob->addNetAction(Net::ApiDownload::makeCached(QUrl(urlString), entry, options)); } connect(dljob.get(), &NetJob::succeeded, this, &FMLLibrariesTask::fmllibsFinished); diff --git a/launcher/modplatform/CheckUpdateTask.h b/launcher/modplatform/CheckUpdateTask.h index 65c80d268..a4c03cb75 100644 --- a/launcher/modplatform/CheckUpdateTask.h +++ b/launcher/modplatform/CheckUpdateTask.h @@ -14,7 +14,7 @@ class CheckUpdateTask : public Task { public: CheckUpdateTask(QList& mods, std::list& mcVersions, - std::optional loaders, + std::optional loaders, std::shared_ptr mods_folder) : Task(nullptr), m_mods(mods), m_game_versions(mcVersions), m_loaders(loaders), m_mods_folder(mods_folder){}; @@ -62,7 +62,7 @@ class CheckUpdateTask : public Task { protected: QList& m_mods; std::list& m_game_versions; - std::optional m_loaders; + std::optional m_loaders; std::shared_ptr m_mods_folder; std::vector m_updatable; diff --git a/launcher/modplatform/ModIndex.cpp b/launcher/modplatform/ModIndex.cpp index 17edcdb5d..72ba88d90 100644 --- a/launcher/modplatform/ModIndex.cpp +++ b/launcher/modplatform/ModIndex.cpp @@ -134,4 +134,25 @@ QString getMetaURL(ResourceProvider provider, QVariant projectID) projectID.toString(); } +auto getModLoaderString(ModLoaderType type) -> const QString +{ + switch (type) { + case NeoForge: + return "neoforge"; + case Forge: + return "forge"; + case Cauldron: + return "cauldron"; + case LiteLoader: + return "liteloader"; + case Fabric: + return "fabric"; + case Quilt: + return "quilt"; + default: + break; + } + return ""; +} + } // namespace ModPlatform diff --git a/launcher/modplatform/ModIndex.h b/launcher/modplatform/ModIndex.h index e56c282a2..4ed8bcd86 100644 --- a/launcher/modplatform/ModIndex.h +++ b/launcher/modplatform/ModIndex.h @@ -31,6 +31,9 @@ class QIODevice; namespace ModPlatform { +enum ModLoaderType { NeoForge = 1 << 0, Forge = 1 << 1, Cauldron = 1 << 2, LiteLoader = 1 << 3, Fabric = 1 << 4, Quilt = 1 << 5 }; +Q_DECLARE_FLAGS(ModLoaderTypes, ModLoaderType) + enum class ResourceProvider { MODRINTH, FLAME }; enum class ResourceType { MOD, RESOURCE_PACK, SHADER_PACK }; @@ -101,7 +104,7 @@ struct IndexedVersion { QString downloadUrl; QString date; QString fileName; - QStringList loaders = {}; + ModLoaderTypes loaders = {}; QString hash_type; QString hash; bool is_preferred = true; @@ -144,7 +147,7 @@ struct IndexedPack { ExtraPackData extraData; // For internal use, not provided by APIs - [[nodiscard]] bool isVersionSelected(size_t index) const + [[nodiscard]] bool isVersionSelected(int index) const { if (!versionsLoaded) return false; @@ -174,9 +177,18 @@ inline auto getOverrideDeps() -> QList { "qvIfYCYJ", "P7dR8mSH", "API", ModPlatform::ResourceProvider::MODRINTH }, { "lwVhp9o5", "Ha28R6CL", "KotlinLibraries", ModPlatform::ResourceProvider::MODRINTH } }; -}; +} + QString getMetaURL(ResourceProvider provider, QVariant projectID); +auto getModLoaderString(ModLoaderType type) -> const QString; + +constexpr bool hasSingleModLoaderSelected(ModLoaderTypes l) noexcept +{ + auto x = static_cast(l); + return x && !(x & (x - 1)); +} + } // namespace ModPlatform Q_DECLARE_METATYPE(ModPlatform::IndexedPack) diff --git a/launcher/modplatform/ResourceAPI.h b/launcher/modplatform/ResourceAPI.h index ef220977e..3b1959384 100644 --- a/launcher/modplatform/ResourceAPI.h +++ b/launcher/modplatform/ResourceAPI.h @@ -54,9 +54,6 @@ class ResourceAPI { public: virtual ~ResourceAPI() = default; - enum ModLoaderType { Forge = 1 << 0, Cauldron = 1 << 1, LiteLoader = 1 << 2, Fabric = 1 << 3, Quilt = 1 << 4 }; - Q_DECLARE_FLAGS(ModLoaderTypes, ModLoaderType) - struct SortingMethod { // The index of the sorting method. Used to allow for arbitrary ordering in the list of methods. // Used by Flame in the API request. @@ -74,7 +71,7 @@ class ResourceAPI { std::optional search; std::optional sorting; - std::optional loaders; + std::optional loaders; std::optional > versions; }; struct SearchCallbacks { @@ -87,7 +84,7 @@ class ResourceAPI { ModPlatform::IndexedPack pack; std::optional > mcVersions; - std::optional loaders; + std::optional loaders; VersionSearchArgs(VersionSearchArgs const&) = default; void operator=(VersionSearchArgs other) @@ -108,13 +105,15 @@ class ResourceAPI { void operator=(ProjectInfoArgs other) { pack = other.pack; } }; struct ProjectInfoCallbacks { - std::function on_succeed; + std::function on_succeed; + std::function on_fail; + std::function on_abort; }; struct DependencySearchArgs { ModPlatform::Dependency dependency; Version mcVersion; - ModLoaderTypes loader; + ModPlatform::ModLoaderTypes loader; }; struct DependencySearchCallbacks { @@ -128,28 +127,30 @@ class ResourceAPI { public slots: [[nodiscard]] virtual Task::Ptr searchProjects(SearchArgs&&, SearchCallbacks&&) const { - qWarning() << "TODO"; + qWarning() << "TODO: ResourceAPI::searchProjects"; return nullptr; } - [[nodiscard]] virtual Task::Ptr getProject(QString addonId, std::shared_ptr response) const + [[nodiscard]] virtual Task::Ptr getProject([[maybe_unused]] QString addonId, + [[maybe_unused]] std::shared_ptr response) const { - qWarning() << "TODO"; + qWarning() << "TODO: ResourceAPI::getProject"; return nullptr; } - [[nodiscard]] virtual Task::Ptr getProjects(QStringList addonIds, std::shared_ptr response) const + [[nodiscard]] virtual Task::Ptr getProjects([[maybe_unused]] QStringList addonIds, + [[maybe_unused]] std::shared_ptr response) const { - qWarning() << "TODO"; + qWarning() << "TODO: ResourceAPI::getProjects"; return nullptr; } [[nodiscard]] virtual Task::Ptr getProjectInfo(ProjectInfoArgs&&, ProjectInfoCallbacks&&) const { - qWarning() << "TODO"; + qWarning() << "TODO: ResourceAPI::getProjectInfo"; return nullptr; } [[nodiscard]] virtual Task::Ptr getProjectVersions(VersionSearchArgs&&, VersionSearchCallbacks&&) const { - qWarning() << "TODO"; + qWarning() << "TODO: ResourceAPI::getProjectVersions"; return nullptr; } @@ -159,25 +160,6 @@ class ResourceAPI { return nullptr; } - static auto getModLoaderString(ModLoaderType type) -> const QString - { - switch (type) { - case Forge: - return "forge"; - case Cauldron: - return "cauldron"; - case LiteLoader: - return "liteloader"; - case Fabric: - return "fabric"; - case Quilt: - return "quilt"; - default: - break; - } - return ""; - } - protected: [[nodiscard]] inline QString debugName() const { return "External resource API"; } diff --git a/launcher/modplatform/atlauncher/ATLPackInstallTask.cpp b/launcher/modplatform/atlauncher/ATLPackInstallTask.cpp index f01984812..e5771b7cd 100644 --- a/launcher/modplatform/atlauncher/ATLPackInstallTask.cpp +++ b/launcher/modplatform/atlauncher/ATLPackInstallTask.cpp @@ -53,6 +53,8 @@ #include "net/ChecksumValidator.h" #include "settings/INISettingsObject.h" +#include "net/ApiDownload.h" + #include "Application.h" #include "BuildConfig.h" @@ -83,7 +85,7 @@ void PackInstallTask::executeTask() NetJob::Ptr netJob{ new NetJob("ATLauncher::VersionFetch", APPLICATION->network()) }; auto searchUrl = QString(BuildConfig.ATL_DOWNLOAD_SERVER_URL + "packs/%1/versions/%2/Configs.json").arg(m_pack_safe_name).arg(m_version_name); - netJob->addNetAction(Net::Download::makeByteArray(QUrl(searchUrl), response)); + netJob->addNetAction(Net::ApiDownload::makeByteArray(QUrl(searchUrl), response)); QObject::connect(netJob.get(), &NetJob::succeeded, this, &PackInstallTask::onDownloadSucceeded); QObject::connect(netJob.get(), &NetJob::failed, this, &PackInstallTask::onDownloadFailed); @@ -631,7 +633,7 @@ void PackInstallTask::installConfigs() auto entry = APPLICATION->metacache()->resolveEntry("ATLauncherPacks", path); entry->setStale(true); - auto dl = Net::Download::makeCached(url, entry); + auto dl = Net::ApiDownload::makeCached(url, entry); if (!m_version.configs.sha1.isEmpty()) { auto rawSha1 = QByteArray::fromHex(m_version.configs.sha1.toLatin1()); dl->addValidator(new Net::ChecksumValidator(QCryptographicHash::Sha1, rawSha1)); @@ -748,7 +750,7 @@ void PackInstallTask::downloadMods() entry->setStale(true); modsToExtract.insert(entry->getFullPath(), mod); - auto dl = Net::Download::makeCached(url, entry); + auto dl = Net::ApiDownload::makeCached(url, entry); if (!mod.md5.isEmpty()) { auto rawMd5 = QByteArray::fromHex(mod.md5.toLatin1()); dl->addValidator(new Net::ChecksumValidator(QCryptographicHash::Md5, rawMd5)); @@ -759,7 +761,7 @@ void PackInstallTask::downloadMods() entry->setStale(true); modsToDecomp.insert(entry->getFullPath(), mod); - auto dl = Net::Download::makeCached(url, entry); + auto dl = Net::ApiDownload::makeCached(url, entry); if (!mod.md5.isEmpty()) { auto rawMd5 = QByteArray::fromHex(mod.md5.toLatin1()); dl->addValidator(new Net::ChecksumValidator(QCryptographicHash::Md5, rawMd5)); @@ -773,7 +775,7 @@ void PackInstallTask::downloadMods() auto entry = APPLICATION->metacache()->resolveEntry("ATLauncherPacks", cacheName); entry->setStale(true); - auto dl = Net::Download::makeCached(url, entry); + auto dl = Net::ApiDownload::makeCached(url, entry); if (!mod.md5.isEmpty()) { auto rawMd5 = QByteArray::fromHex(mod.md5.toLatin1()); dl->addValidator(new Net::ChecksumValidator(QCryptographicHash::Md5, rawMd5)); @@ -1003,15 +1005,30 @@ static Meta::Version::Ptr getComponentVersion(const QString& uid, const QString& if (!vlist) return {}; - if (!vlist->isLoaded()) - vlist->load(Net::Mode::Online); + if (!vlist->isLoaded()) { + QEventLoop loadVersionLoop; + auto task = vlist->getLoadTask(); + QObject::connect(task.get(), &Task::finished, &loadVersionLoop, &QEventLoop::quit); + if (!task->isRunning()) + task->start(); + + loadVersionLoop.exec(); + } auto ver = vlist->getVersion(version); if (!ver) return {}; - if (!ver->isLoaded()) + if (!ver->isLoaded()) { + QEventLoop loadVersionLoop; ver->load(Net::Mode::Online); + auto task = ver->getCurrentTask(); + QObject::connect(task.get(), &Task::finished, &loadVersionLoop, &QEventLoop::quit); + if (!task->isRunning()) + task->start(); + + loadVersionLoop.exec(); + } return ver; } diff --git a/launcher/modplatform/flame/FileResolvingTask.cpp b/launcher/modplatform/flame/FileResolvingTask.cpp index ae168bbd6..5865bee91 100644 --- a/launcher/modplatform/flame/FileResolvingTask.cpp +++ b/launcher/modplatform/flame/FileResolvingTask.cpp @@ -1,6 +1,9 @@ #include "FileResolvingTask.h" #include "Json.h" +#include "modplatform/ModIndex.h" +#include "net/ApiDownload.h" +#include "net/ApiUpload.h" #include "net/Upload.h" #include "modplatform/modrinth/ModrinthPackIndex.h" @@ -38,7 +41,7 @@ void Flame::FileResolvingTask::executeTask() return l; })); QByteArray data = Json::toText(object); - auto dl = Net::Upload::makeByteArray(QUrl("https://api.curseforge.com/v1/mods/files"), result, data); + auto dl = Net::ApiUpload::makeByteArray(QUrl("https://api.curseforge.com/v1/mods/files"), result, data); m_dljob->addNetAction(dl); auto step_progress = std::make_shared(); @@ -93,14 +96,14 @@ void Flame::FileResolvingTask::netJobFinished() auto& out = m_toProcess.files[fileid]; try { out.parseFromObject(Json::requireObject(file)); - } catch (const JSONValidationError& e) { + } catch ([[maybe_unused]] const JSONValidationError& e) { qDebug() << "Blocked mod on curseforge" << out.fileName; auto hash = out.hash; if (!hash.isEmpty()) { auto url = QString("https://api.modrinth.com/v2/version_file/%1?algorithm=sha1").arg(hash); auto output = std::make_shared(); - auto dl = Net::Download::makeByteArray(QUrl(url), output); - QObject::connect(dl.get(), &Net::Download::succeeded, [&out]() { out.resolved = true; }); + auto dl = Net::ApiDownload::makeByteArray(QUrl(url), output); + QObject::connect(dl.get(), &Net::ApiDownload::succeeded, [&out]() { out.resolved = true; }); m_checkJob->addNetAction(dl); blockedProjects.insert(&out, output); @@ -151,7 +154,7 @@ void Flame::FileResolvingTask::modrinthCheckFinished() // If there's more than one mod loader for this version, we can't know for sure // which file is relative to each loader, so it's best to not use any one and // let the user download it manually. - if (file.loaders.size() <= 1) { + if (!file.loaders || hasSingleModLoaderSelected(file.loaders)) { out->url = file.downloadUrl; qDebug() << "Found alternative on modrinth " << out->fileName; } else { @@ -171,9 +174,9 @@ void Flame::FileResolvingTask::modrinthCheckFinished() auto projectId = mod->projectId; auto output = std::make_shared(); auto url = QString("https://api.curseforge.com/v1/mods/%1").arg(projectId); - auto dl = Net::Download::makeByteArray(url, output); + auto dl = Net::ApiDownload::makeByteArray(url, output); qDebug() << "Fetching url slug for file:" << mod->fileName; - QObject::connect(dl.get(), &Net::Download::succeeded, [block, index, output]() { + QObject::connect(dl.get(), &Net::ApiDownload::succeeded, [block, index, output]() { auto mod = block->at(index); // use the shared_ptr so it is captured and only freed when we are done auto json = QJsonDocument::fromJson(*output); auto base = diff --git a/launcher/modplatform/flame/FlameAPI.cpp b/launcher/modplatform/flame/FlameAPI.cpp index 885c46873..a9697893d 100644 --- a/launcher/modplatform/flame/FlameAPI.cpp +++ b/launcher/modplatform/flame/FlameAPI.cpp @@ -6,8 +6,9 @@ #include "FlameModIndex.h" #include "Application.h" -#include "BuildConfig.h" #include "Json.h" +#include "net/ApiDownload.h" +#include "net/ApiUpload.h" #include "net/NetJob.h" #include "net/Upload.h" @@ -26,7 +27,7 @@ Task::Ptr FlameAPI::matchFingerprints(const QList& fingerprints, std::shar QJsonDocument body(body_obj); auto body_raw = body.toJson(); - netJob->addNetAction(Net::Upload::makeByteArray(QString("https://api.curseforge.com/v1/fingerprints"), response, body_raw)); + netJob->addNetAction(Net::ApiUpload::makeByteArray(QString("https://api.curseforge.com/v1/fingerprints"), response, body_raw)); return netJob; } @@ -38,7 +39,7 @@ auto FlameAPI::getModFileChangelog(int modId, int fileId) -> QString auto netJob = makeShared(QString("Flame::FileChangelog"), APPLICATION->network()); auto response = std::make_shared(); - netJob->addNetAction(Net::Download::makeByteArray( + netJob->addNetAction(Net::ApiDownload::makeByteArray( QString("https://api.curseforge.com/v1/mods/%1/files/%2/changelog") .arg(QString::fromStdString(std::to_string(modId)), QString::fromStdString(std::to_string(fileId))), response)); @@ -73,8 +74,8 @@ auto FlameAPI::getModDescription(int modId) -> QString auto netJob = makeShared(QString("Flame::ModDescription"), APPLICATION->network()); auto response = std::make_shared(); - netJob->addNetAction( - Net::Download::makeByteArray(QString("https://api.curseforge.com/v1/mods/%1/description").arg(QString::number(modId)), response)); + netJob->addNetAction(Net::ApiDownload::makeByteArray( + QString("https://api.curseforge.com/v1/mods/%1/description").arg(QString::number(modId)), response)); QObject::connect(netJob.get(), &NetJob::succeeded, [&netJob, response, &description] { QJsonParseError parse_error{}; @@ -113,7 +114,7 @@ auto FlameAPI::getLatestVersion(VersionSearchArgs&& args) -> ModPlatform::Indexe auto response = std::make_shared(); ModPlatform::IndexedVersion ver; - netJob->addNetAction(Net::Download::makeByteArray(versions_url, response)); + netJob->addNetAction(Net::ApiDownload::makeByteArray(versions_url, response)); QObject::connect(netJob.get(), &NetJob::succeeded, [response, args, &ver] { QJsonParseError parse_error{}; @@ -129,20 +130,15 @@ auto FlameAPI::getLatestVersion(VersionSearchArgs&& args) -> ModPlatform::Indexe auto obj = Json::requireObject(doc); auto arr = Json::requireArray(obj, "data"); - QJsonObject latest_file_obj; - ModPlatform::IndexedVersion ver_tmp; - for (auto file : arr) { auto file_obj = Json::requireObject(file); auto file_tmp = FlameMod::loadIndexedPackVersion(file_obj); - bool better_release = file_tmp.version_type <= ver_tmp.version_type; - if (file_tmp.date > ver_tmp.date && better_release) { - ver_tmp = file_tmp; - latest_file_obj = file_obj; - } + if (file_tmp.date > ver.date && + (!args.loaders.has_value() || !file_tmp.loaders || args.loaders.value() & file_tmp.loaders) && + file_tmp.version_type <= ver.version_type) + ver = file_tmp; } - ver = FlameMod::loadIndexedPackVersion(latest_file_obj); } catch (Json::JsonException& e) { qCritical() << "Failed to parse response from a version request."; qCritical() << e.what(); @@ -174,7 +170,7 @@ Task::Ptr FlameAPI::getProjects(QStringList addonIds, std::shared_ptraddNetAction(Net::Upload::makeByteArray(QString("https://api.curseforge.com/v1/mods"), response, body_raw)); + netJob->addNetAction(Net::ApiUpload::makeByteArray(QString("https://api.curseforge.com/v1/mods"), response, body_raw)); QObject::connect(netJob.get(), &NetJob::failed, [body_raw] { qDebug() << body_raw; }); @@ -196,13 +192,24 @@ Task::Ptr FlameAPI::getFiles(const QStringList& fileIds, std::shared_ptraddNetAction(Net::Upload::makeByteArray(QString("https://api.curseforge.com/v1/mods/files"), response, body_raw)); + netJob->addNetAction(Net::ApiUpload::makeByteArray(QString("https://api.curseforge.com/v1/mods/files"), response, body_raw)); QObject::connect(netJob.get(), &NetJob::failed, [body_raw] { qDebug() << body_raw; }); return netJob; } +Task::Ptr FlameAPI::getFile(const QString& addonId, const QString& fileId, std::shared_ptr response) const +{ + auto netJob = makeShared(QString("Flame::GetFile"), APPLICATION->network()); + netJob->addNetAction( + Net::ApiDownload::makeByteArray(QUrl(QString("https://api.curseforge.com/v1/mods/%1/files/%2").arg(addonId, fileId)), response)); + + QObject::connect(netJob.get(), &NetJob::failed, [addonId, fileId] { qDebug() << "Flame API file failure" << addonId << fileId; }); + + return netJob; +} + // https://docs.curseforge.com/?python#tocS_ModsSearchSortField static QList s_sorts = { { 1, "Featured", QObject::tr("Sort by Featured") }, { 2, "Popularity", QObject::tr("Sort by Popularity") }, diff --git a/launcher/modplatform/flame/FlameAPI.h b/launcher/modplatform/flame/FlameAPI.h index 49bc316f2..e22d8f0d8 100644 --- a/launcher/modplatform/flame/FlameAPI.h +++ b/launcher/modplatform/flame/FlameAPI.h @@ -20,10 +20,14 @@ class FlameAPI : public NetworkResourceAPI { Task::Ptr getProjects(QStringList addonIds, std::shared_ptr response) const override; Task::Ptr matchFingerprints(const QList& fingerprints, std::shared_ptr response); 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; [[nodiscard]] auto getSortingMethods() const -> QList override; - static inline auto validateModLoaders(ModLoaderTypes loaders) -> bool { return loaders & (Forge | Fabric | Quilt); } + static inline auto validateModLoaders(ModPlatform::ModLoaderTypes loaders) -> bool + { + return loaders & (ModPlatform::NeoForge | ModPlatform::Forge | ModPlatform::Fabric | ModPlatform::Quilt); + } private: static int getClassId(ModPlatform::ResourceType type) @@ -34,22 +38,47 @@ class FlameAPI : public NetworkResourceAPI { return 6; case ModPlatform::ResourceType::RESOURCE_PACK: return 12; + case ModPlatform::ResourceType::SHADER_PACK: + return 6552; } } - static int getMappedModLoader(ModLoaderTypes loaders) + static int getMappedModLoader(ModPlatform::ModLoaderType loaders) { // https://docs.curseforge.com/?http#tocS_ModLoaderType - if (loaders & Forge) - return 1; - if (loaders & Fabric) - return 4; - // TODO: remove this once Quilt drops official Fabric support - if (loaders & Quilt) // NOTE: Most if not all Fabric mods should work *currently* - return 4; // Quilt would probably be 5 + switch (loaders) { + case ModPlatform::Forge: + return 1; + case ModPlatform::Cauldron: + return 2; + case ModPlatform::LiteLoader: + return 3; + case ModPlatform::Fabric: + return 4; + case ModPlatform::Quilt: + return 5; + case ModPlatform::NeoForge: + return 6; + } return 0; } + static auto getModLoaderStrings(const ModPlatform::ModLoaderTypes types) -> const QStringList + { + QStringList l; + for (auto loader : { ModPlatform::NeoForge, ModPlatform::Forge, ModPlatform::Fabric, ModPlatform::Quilt }) { + if (types & loader) { + l << QString::number(getMappedModLoader(loader)); + } + } + return l; + } + + static auto getModLoaderFilters(ModPlatform::ModLoaderTypes types) -> const QString + { + return "[" + getModLoaderStrings(types).join(',') + "]"; + } + private: [[nodiscard]] std::optional getSearchURL(SearchArgs const& args) const override { @@ -66,7 +95,7 @@ class FlameAPI : public NetworkResourceAPI { get_arguments.append(QString("sortField=%1").arg(args.sorting.value().index)); get_arguments.append("sortOrder=desc"); if (args.loaders.has_value()) - get_arguments.append(QString("modLoaderType=%1").arg(getMappedModLoader(args.loaders.value()))); + get_arguments.append(QString("modLoaderTypes=%1").arg(getModLoaderFilters(args.loaders.value()))); get_arguments.append(gameVersionStr); return "https://api.curseforge.com/v1/mods/search?gameId=432&" + get_arguments.join('&'); @@ -80,47 +109,27 @@ class FlameAPI : public NetworkResourceAPI { [[nodiscard]] std::optional getVersionsURL(VersionSearchArgs const& args) const override { auto addonId = args.pack.addonId.toString(); - QString url{ QString("https://api.curseforge.com/v1/mods/%1/files?pageSize=10000&").arg(addonId) }; + QString url = QString("https://api.curseforge.com/v1/mods/%1/files?pageSize=10000").arg(addonId); - QStringList get_parameters; if (args.mcVersions.has_value()) - get_parameters.append(QString("gameVersion=%1").arg(args.mcVersions.value().front().toString())); + url += QString("&gameVersion=%1").arg(args.mcVersions.value().front().toString()); - if (args.loaders.has_value()) { - int mappedModLoader = getMappedModLoader(args.loaders.value()); - - if (args.loaders.value() & Quilt) { - auto overide = ModPlatform::getOverrideDeps(); - auto over = std::find_if(overide.cbegin(), overide.cend(), [addonId](auto dep) { - return dep.provider == ModPlatform::ResourceProvider::FLAME && addonId == dep.quilt; - }); - if (over != overide.cend()) { - mappedModLoader = 5; - } - } - - get_parameters.append(QString("modLoaderType=%1").arg(mappedModLoader)); + if (args.loaders.has_value() && ModPlatform::hasSingleModLoaderSelected(args.loaders.value())) { + int mappedModLoader = getMappedModLoader(static_cast(static_cast(args.loaders.value()))); + url += QString("&modLoaderType=%1").arg(mappedModLoader); } - - return url + get_parameters.join('&'); + return url; }; [[nodiscard]] std::optional getDependencyURL(DependencySearchArgs const& args) const override { - auto mappedModLoader = getMappedModLoader(args.loader); auto addonId = args.dependency.addonId.toString(); - if (args.loader & Quilt) { - auto overide = ModPlatform::getOverrideDeps(); - auto over = std::find_if(overide.cbegin(), overide.cend(), [addonId](auto dep) { - return dep.provider == ModPlatform::ResourceProvider::FLAME && addonId == dep.quilt; - }); - if (over != overide.cend()) { - mappedModLoader = 5; - } + auto url = + QString("https://api.curseforge.com/v1/mods/%1/files?pageSize=10000&gameVersion=%2").arg(addonId, args.mcVersion.toString()); + if (args.loader && ModPlatform::hasSingleModLoaderSelected(args.loader)) { + int mappedModLoader = getMappedModLoader(static_cast(static_cast(args.loader))); + url += QString("&modLoaderType=%1").arg(mappedModLoader); } - return QString("https://api.curseforge.com/v1/mods/%1/files?pageSize=10000&gameVersion=%2&modLoaderType=%3") - .arg(addonId) - .arg(args.mcVersion.toString()) - .arg(mappedModLoader); + return url; }; }; diff --git a/launcher/modplatform/flame/FlameCheckUpdate.cpp b/launcher/modplatform/flame/FlameCheckUpdate.cpp index 5ff833cfa..023e26e40 100644 --- a/launcher/modplatform/flame/FlameCheckUpdate.cpp +++ b/launcher/modplatform/flame/FlameCheckUpdate.cpp @@ -5,13 +5,13 @@ #include #include -#include "FileSystem.h" #include "Json.h" #include "ResourceDownloadTask.h" #include "minecraft/mod/ModFolderModel.h" -#include "minecraft/mod/ResourceFolderModel.h" + +#include "net/ApiDownload.h" static FlameAPI api; @@ -33,7 +33,7 @@ ModPlatform::IndexedPack getProjectInfo(ModPlatform::IndexedVersion& ver_info) auto response = std::make_shared(); auto url = QString("https://api.curseforge.com/v1/mods/%1").arg(ver_info.addonId.toString()); - auto dl = Net::Download::makeByteArray(url, response); + auto dl = Net::ApiDownload::makeByteArray(url, response); get_project_job->addNetAction(dl); QObject::connect(get_project_job, &NetJob::succeeded, [response, &pack]() { @@ -77,7 +77,7 @@ ModPlatform::IndexedVersion getFileInfo(int addonId, int fileId) auto response = std::make_shared(); auto url = QString("https://api.curseforge.com/v1/mods/%1/files/%2").arg(QString::number(addonId), QString::number(fileId)); - auto dl = Net::Download::makeByteArray(url, response); + auto dl = Net::ApiDownload::makeByteArray(url, response); get_file_info_job->addNetAction(dl); QObject::connect(get_file_info_job, &NetJob::succeeded, [response, &ver]() { diff --git a/launcher/modplatform/flame/FlameCheckUpdate.h b/launcher/modplatform/flame/FlameCheckUpdate.h index e3465d7e2..05c619a70 100644 --- a/launcher/modplatform/flame/FlameCheckUpdate.h +++ b/launcher/modplatform/flame/FlameCheckUpdate.h @@ -10,7 +10,7 @@ class FlameCheckUpdate : public CheckUpdateTask { public: FlameCheckUpdate(QList& mods, std::list& mcVersions, - std::optional loaders, + std::optional loaders, std::shared_ptr mods_folder) : CheckUpdateTask(mods, mcVersions, loaders, mods_folder) {} diff --git a/launcher/modplatform/flame/FlameInstanceCreationTask.cpp b/launcher/modplatform/flame/FlameInstanceCreationTask.cpp index 8b6622944..45b4e2125 100644 --- a/launcher/modplatform/flame/FlameInstanceCreationTask.cpp +++ b/launcher/modplatform/flame/FlameInstanceCreationTask.cpp @@ -61,6 +61,7 @@ #include "meta/VersionList.h" #include "minecraft/World.h" #include "minecraft/mod/tasks/LocalResourceParse.h" +#include "net/ApiDownload.h" static const FlameAPI api; @@ -283,7 +284,7 @@ QString FlameCreationTask::getVersionForLoader(QString uid, QString loaderType, // filter by minecraft version, if the loader depends on a certain version. // not all mod loaders depend on a given Minecraft version, so we won't do this // filtering for those loaders. - if (loaderType == "forge") { + if (loaderType == "forge" || loaderType == "neoforge") { auto iter = std::find_if(reqs.begin(), reqs.end(), [mcVersion](const Meta::Require& req) { return req.uid == "net.minecraft" && req.equalsVersion == mcVersion; }); @@ -349,7 +350,11 @@ bool FlameCreationTask::createInstance() for (auto& loader : m_pack.minecraft.modLoaders) { auto id = loader.id; - if (id.startsWith("forge-")) { + if (id.startsWith("neoforge-")) { + id.remove("neoforge-"); + loaderType = "neoforge"; + loaderUid = "net.neoforged"; + } else if (id.startsWith("forge-")) { id.remove("forge-"); loaderType = "forge"; loaderUid = "net.minecraftforge"; @@ -523,7 +528,7 @@ void FlameCreationTask::setupDownloadJob(QEventLoop& loop) case Flame::File::Type::Mod: { if (!result.url.isEmpty()) { qDebug() << "Will download" << result.url << "to" << path; - auto dl = Net::Download::makeFile(result.url, path); + auto dl = Net::ApiDownload::makeFile(result.url, path); m_files_job->addNetAction(dl); } break; diff --git a/launcher/modplatform/flame/FlameModIndex.cpp b/launcher/modplatform/flame/FlameModIndex.cpp index 5a44d4db7..3754ae1a6 100644 --- a/launcher/modplatform/flame/FlameModIndex.cpp +++ b/launcher/modplatform/flame/FlameModIndex.cpp @@ -54,7 +54,7 @@ void FlameMod::loadURLs(ModPlatform::IndexedPack& pack, QJsonObject& obj) pack.extraDataLoaded = true; } -void FlameMod::loadBody(ModPlatform::IndexedPack& pack, QJsonObject& obj) +void FlameMod::loadBody(ModPlatform::IndexedPack& pack, [[maybe_unused]] QJsonObject& obj) { pack.extraData.body = api.getModDescription(pack.addonId.toInt()); @@ -75,12 +75,13 @@ static QString enumToString(int hash_algorithm) void FlameMod::loadIndexedPackVersions(ModPlatform::IndexedPack& pack, QJsonArray& arr, - const shared_qobject_ptr& network, + [[maybe_unused]] const shared_qobject_ptr& network, const BaseInstance* inst) { QVector unsortedVersions; auto profile = (dynamic_cast(inst))->getPackProfile(); QString mcVersion = profile->getComponentVersion("net.minecraft"); + auto loaders = profile->getSupportedModLoaders(); for (auto versionIter : arr) { auto obj = versionIter.toObject(); @@ -89,7 +90,8 @@ void FlameMod::loadIndexedPackVersions(ModPlatform::IndexedPack& pack, if (!file.addonId.isValid()) file.addonId = pack.addonId; - if (file.fileId.isValid()) // Heuristic to check if the returned value is valid + if (file.fileId.isValid() && + (!loaders.has_value() || !file.loaders || loaders.value() & file.loaders)) // Heuristic to check if the returned value is valid unsortedVersions.append(file); } @@ -116,6 +118,19 @@ auto FlameMod::loadIndexedPackVersion(QJsonObject& obj, bool load_changelog) -> if (str.contains('.')) file.mcVersion.append(str); + auto loader = str.toLower(); + if (loader == "neoforge") + file.loaders |= ModPlatform::NeoForge; + if (loader == "forge") + file.loaders |= ModPlatform::Forge; + if (loader == "cauldron") + file.loaders |= ModPlatform::Cauldron; + if (loader == "liteloader") + file.loaders |= ModPlatform::LiteLoader; + if (loader == "fabric") + file.loaders |= ModPlatform::Fabric; + if (loader == "quilt") + file.loaders |= ModPlatform::Quilt; } file.addonId = Json::requireInteger(obj, "modId"); @@ -175,8 +190,11 @@ auto FlameMod::loadIndexedPackVersion(QJsonObject& obj, bool load_changelog) -> return file; } -ModPlatform::IndexedVersion FlameMod::loadDependencyVersions(const ModPlatform::Dependency& m, QJsonArray& arr) +ModPlatform::IndexedVersion FlameMod::loadDependencyVersions(const ModPlatform::Dependency& m, QJsonArray& arr, const BaseInstance* inst) { + auto profile = (dynamic_cast(inst))->getPackProfile(); + QString mcVersion = profile->getComponentVersion("net.minecraft"); + auto loaders = profile->getSupportedModLoaders(); QVector versions; for (auto versionIter : arr) { auto obj = versionIter.toObject(); @@ -185,7 +203,8 @@ ModPlatform::IndexedVersion FlameMod::loadDependencyVersions(const ModPlatform:: if (!file.addonId.isValid()) file.addonId = m.addonId; - if (file.fileId.isValid()) // Heuristic to check if the returned value is valid + if (file.fileId.isValid() && + (!loaders.has_value() || !file.loaders || loaders.value() & file.loaders)) // Heuristic to check if the returned value is valid versions.append(file); } @@ -194,5 +213,7 @@ ModPlatform::IndexedVersion FlameMod::loadDependencyVersions(const ModPlatform:: return a.date > b.date; }; std::sort(versions.begin(), versions.end(), orderSortPredicate); - return versions.front(); -}; + if (versions.size() != 0) + return versions.front(); + return {}; +} diff --git a/launcher/modplatform/flame/FlameModIndex.h b/launcher/modplatform/flame/FlameModIndex.h index aa0d6f812..1bcaa44ba 100644 --- a/launcher/modplatform/flame/FlameModIndex.h +++ b/launcher/modplatform/flame/FlameModIndex.h @@ -19,5 +19,5 @@ void loadIndexedPackVersions(ModPlatform::IndexedPack& pack, const shared_qobject_ptr& network, const BaseInstance* inst); auto loadIndexedPackVersion(QJsonObject& obj, bool load_changelog = false) -> ModPlatform::IndexedVersion; -auto loadDependencyVersions(const ModPlatform::Dependency& m, QJsonArray& arr) -> ModPlatform::IndexedVersion; +auto loadDependencyVersions(const ModPlatform::Dependency& m, QJsonArray& arr, const BaseInstance* inst) -> ModPlatform::IndexedVersion; } // namespace FlameMod \ No newline at end of file diff --git a/launcher/modplatform/flame/FlamePackExportTask.cpp b/launcher/modplatform/flame/FlamePackExportTask.cpp index f5f3af372..d86d34bf4 100644 --- a/launcher/modplatform/flame/FlamePackExportTask.cpp +++ b/launcher/modplatform/flame/FlamePackExportTask.cpp @@ -43,12 +43,14 @@ const QStringList FlamePackExportTask::FILE_EXTENSIONS({ "jar", "zip" }); FlamePackExportTask::FlamePackExportTask(const QString& name, const QString& version, const QString& author, + bool optionalFiles, InstancePtr instance, const QString& output, MMCZip::FilterFunction filter) : name(name) , version(version) , author(author) + , optionalFiles(optionalFiles) , instance(instance) , mcInstance(dynamic_cast(instance.get())) , gameRoot(instance->gameRoot()) @@ -381,6 +383,7 @@ QByteArray FlamePackExportTask::generateIndex() const ComponentPtr quilt = profile->getComponent("org.quiltmc.quilt-loader"); const ComponentPtr fabric = profile->getComponent("net.fabricmc.fabric-loader"); const ComponentPtr forge = profile->getComponent("net.minecraftforge"); + const ComponentPtr neoforge = profile->getComponent("net.neoforged"); // convert all available components to mrpack dependencies if (minecraft != nullptr) @@ -392,6 +395,8 @@ QByteArray FlamePackExportTask::generateIndex() id = "fabric-" + fabric->getVersion(); else if (forge != nullptr) id = "forge-" + forge->getVersion(); + else if (neoforge != nullptr) + id = "neoforge-" + neoforge->getVersion(); version["modLoaders"] = QJsonArray(); if (!id.isEmpty()) { QJsonObject loader; @@ -407,7 +412,7 @@ QByteArray FlamePackExportTask::generateIndex() QJsonObject file; file["projectID"] = mod.addonId; file["fileID"] = mod.version; - file["required"] = mod.enabled; + file["required"] = mod.enabled || !optionalFiles; files << file; } obj["files"] = files; diff --git a/launcher/modplatform/flame/FlamePackExportTask.h b/launcher/modplatform/flame/FlamePackExportTask.h index d3dc6281e..78b46e91f 100644 --- a/launcher/modplatform/flame/FlamePackExportTask.h +++ b/launcher/modplatform/flame/FlamePackExportTask.h @@ -30,6 +30,7 @@ class FlamePackExportTask : public Task { FlamePackExportTask(const QString& name, const QString& version, const QString& author, + bool optionalFiles, InstancePtr instance, const QString& output, MMCZip::FilterFunction filter); @@ -44,6 +45,7 @@ class FlamePackExportTask : public Task { // inputs const QString name, version, author; + const bool optionalFiles; const InstancePtr instance; MinecraftInstance* mcInstance; const QDir gameRoot; diff --git a/launcher/modplatform/helpers/NetworkResourceAPI.cpp b/launcher/modplatform/helpers/NetworkResourceAPI.cpp index c278f800d..dccccdc21 100644 --- a/launcher/modplatform/helpers/NetworkResourceAPI.cpp +++ b/launcher/modplatform/helpers/NetworkResourceAPI.cpp @@ -10,6 +10,8 @@ #include "modplatform/ModIndex.h" +#include "net/ApiDownload.h" + Task::Ptr NetworkResourceAPI::searchProjects(SearchArgs&& args, SearchCallbacks&& callbacks) const { auto search_url_optional = getSearchURL(args); @@ -23,7 +25,7 @@ Task::Ptr NetworkResourceAPI::searchProjects(SearchArgs&& args, SearchCallbacks& auto response = std::make_shared(); auto netJob = makeShared(QString("%1::Search").arg(debugName()), APPLICATION->network()); - netJob->addNetAction(Net::Download::makeByteArray(QUrl(search_url), response)); + netJob->addNetAction(Net::ApiDownload::makeByteArray(QUrl(search_url), response)); QObject::connect(netJob.get(), &NetJob::succeeded, [this, response, callbacks] { QJsonParseError parse_error{}; @@ -70,7 +72,8 @@ Task::Ptr NetworkResourceAPI::getProjectInfo(ProjectInfoArgs&& args, ProjectInfo callbacks.on_succeed(doc, args.pack); }); - + QObject::connect(job.get(), &NetJob::failed, [callbacks](QString reason) { callbacks.on_fail(reason); }); + QObject::connect(job.get(), &NetJob::aborted, [callbacks] { callbacks.on_abort(); }); return job; } @@ -85,7 +88,7 @@ Task::Ptr NetworkResourceAPI::getProjectVersions(VersionSearchArgs&& args, Versi auto netJob = makeShared(QString("%1::Versions").arg(args.pack.name), APPLICATION->network()); auto response = std::make_shared(); - netJob->addNetAction(Net::Download::makeByteArray(versions_url, response)); + netJob->addNetAction(Net::ApiDownload::makeByteArray(versions_url, response)); QObject::connect(netJob.get(), &NetJob::succeeded, [response, callbacks, args] { QJsonParseError parse_error{}; @@ -113,7 +116,7 @@ Task::Ptr NetworkResourceAPI::getProject(QString addonId, std::shared_ptr(QString("%1::GetProject").arg(addonId), APPLICATION->network()); - netJob->addNetAction(Net::Download::makeByteArray(QUrl(project_url), response)); + netJob->addNetAction(Net::ApiDownload::makeByteArray(QUrl(project_url), response)); return netJob; } @@ -129,7 +132,7 @@ Task::Ptr NetworkResourceAPI::getDependencyVersion(DependencySearchArgs&& args, auto netJob = makeShared(QString("%1::Dependency").arg(args.dependency.addonId.toString()), APPLICATION->network()); auto response = std::make_shared(); - netJob->addNetAction(Net::Download::makeByteArray(versions_url, response)); + netJob->addNetAction(Net::ApiDownload::makeByteArray(versions_url, response)); QObject::connect(netJob.get(), &NetJob::succeeded, [=] { QJsonParseError parse_error{}; @@ -145,4 +148,4 @@ Task::Ptr NetworkResourceAPI::getDependencyVersion(DependencySearchArgs&& args, }); return netJob; -}; +} diff --git a/launcher/modplatform/import_ftb/PackHelpers.cpp b/launcher/modplatform/import_ftb/PackHelpers.cpp index 4a1bbef96..ecf973452 100644 --- a/launcher/modplatform/import_ftb/PackHelpers.cpp +++ b/launcher/modplatform/import_ftb/PackHelpers.cpp @@ -59,16 +59,20 @@ Modpack parseDirectory(QString path) auto obj = Json::requireObject(target, "target"); auto name = Json::requireString(obj, "name", "name"); auto version = Json::requireString(obj, "version", "version"); - if (name == "forge") { - modpack.loaderType = ResourceAPI::Forge; + if (name == "neoforge") { + modpack.loaderType = ModPlatform::NeoForge; + modpack.version = version; + break; + } else if (name == "forge") { + modpack.loaderType = ModPlatform::Forge; modpack.version = version; break; } else if (name == "fabric") { - modpack.loaderType = ResourceAPI::Fabric; + modpack.loaderType = ModPlatform::Fabric; modpack.version = version; break; } else if (name == "quilt") { - modpack.loaderType = ResourceAPI::Quilt; + modpack.loaderType = ModPlatform::Quilt; modpack.version = version; break; } diff --git a/launcher/modplatform/import_ftb/PackHelpers.h b/launcher/modplatform/import_ftb/PackHelpers.h index 8ea4f3faf..5400252b6 100644 --- a/launcher/modplatform/import_ftb/PackHelpers.h +++ b/launcher/modplatform/import_ftb/PackHelpers.h @@ -39,7 +39,7 @@ struct Modpack { // not needed for instance creation QVariant jvmArgs; - std::optional loaderType; + std::optional loaderType; QString loaderVersion; QIcon icon; diff --git a/launcher/modplatform/import_ftb/PackInstallTask.cpp b/launcher/modplatform/import_ftb/PackInstallTask.cpp index b5e424d12..9a3b2595b 100644 --- a/launcher/modplatform/import_ftb/PackInstallTask.cpp +++ b/launcher/modplatform/import_ftb/PackInstallTask.cpp @@ -68,21 +68,25 @@ void PackInstallTask::copySettings() auto modloader = m_pack.loaderType; if (modloader.has_value()) switch (modloader.value()) { - case ResourceAPI::Forge: { + case ModPlatform::NeoForge: { + components->setComponentVersion("net.neoforged", m_pack.version, true); + break; + } + case ModPlatform::Forge: { components->setComponentVersion("net.minecraftforge", m_pack.version, true); break; } - case ResourceAPI::Fabric: { + case ModPlatform::Fabric: { components->setComponentVersion("net.fabricmc.fabric-loader", m_pack.version, true); break; } - case ResourceAPI::Quilt: { + case ModPlatform::Quilt: { components->setComponentVersion("org.quiltmc.quilt-loader", m_pack.version, true); break; } - case ResourceAPI::Cauldron: + case ModPlatform::Cauldron: break; - case ResourceAPI::LiteLoader: + case ModPlatform::LiteLoader: break; } components->saveNow(); diff --git a/launcher/modplatform/legacy_ftb/PackFetchTask.cpp b/launcher/modplatform/legacy_ftb/PackFetchTask.cpp index 6d27357a9..8f1a6e2ff 100644 --- a/launcher/modplatform/legacy_ftb/PackFetchTask.cpp +++ b/launcher/modplatform/legacy_ftb/PackFetchTask.cpp @@ -40,6 +40,8 @@ #include "Application.h" #include "BuildConfig.h" +#include "net/ApiDownload.h" + namespace LegacyFTB { void PackFetchTask::fetch() @@ -51,7 +53,7 @@ void PackFetchTask::fetch() QUrl publicPacksUrl = QUrl(BuildConfig.LEGACY_FTB_CDN_BASE_URL + "static/modpacks.xml"); qDebug() << "Downloading public version info from" << publicPacksUrl.toString(); - jobPtr->addNetAction(Net::Download::makeByteArray(publicPacksUrl, publicModpacksXmlFileData)); + jobPtr->addNetAction(Net::ApiDownload::makeByteArray(publicPacksUrl, publicModpacksXmlFileData)); QUrl thirdPartyUrl = QUrl(BuildConfig.LEGACY_FTB_CDN_BASE_URL + "static/thirdparty.xml"); qDebug() << "Downloading thirdparty version info from" << thirdPartyUrl.toString(); @@ -71,7 +73,7 @@ void PackFetchTask::fetchPrivate(const QStringList& toFetch) for (auto& packCode : toFetch) { auto data = std::make_shared(); NetJob* job = new NetJob("Fetching private pack", m_network); - job->addNetAction(Net::Download::makeByteArray(privatePackBaseUrl.arg(packCode), data)); + job->addNetAction(Net::ApiDownload::makeByteArray(privatePackBaseUrl.arg(packCode), data)); QObject::connect(job, &NetJob::succeeded, this, [this, job, data, packCode] { ModpackList packs; diff --git a/launcher/modplatform/legacy_ftb/PackInstallTask.cpp b/launcher/modplatform/legacy_ftb/PackInstallTask.cpp index 91c821f0e..761f622bb 100644 --- a/launcher/modplatform/legacy_ftb/PackInstallTask.cpp +++ b/launcher/modplatform/legacy_ftb/PackInstallTask.cpp @@ -48,6 +48,8 @@ #include "Application.h" #include "BuildConfig.h" +#include "net/ApiDownload.h" + namespace LegacyFTB { PackInstallTask::PackInstallTask(shared_qobject_ptr network, Modpack pack, QString version) @@ -77,7 +79,7 @@ void PackInstallTask::downloadPack() } else { url = QString(BuildConfig.LEGACY_FTB_CDN_BASE_URL + "modpacks/%1").arg(archivePath); } - netJobContainer->addNetAction(Net::Download::makeFile(url, archivePath)); + netJobContainer->addNetAction(Net::ApiDownload::makeFile(url, archivePath)); connect(netJobContainer.get(), &NetJob::succeeded, this, &PackInstallTask::unzip); connect(netJobContainer.get(), &NetJob::failed, this, &PackInstallTask::emitFailed); diff --git a/launcher/modplatform/modrinth/ModrinthAPI.cpp b/launcher/modplatform/modrinth/ModrinthAPI.cpp index 364cf3f30..f453f5cb9 100644 --- a/launcher/modplatform/modrinth/ModrinthAPI.cpp +++ b/launcher/modplatform/modrinth/ModrinthAPI.cpp @@ -6,6 +6,8 @@ #include "Application.h" #include "Json.h" +#include "net/ApiDownload.h" +#include "net/ApiUpload.h" #include "net/NetJob.h" #include "net/Upload.h" @@ -13,7 +15,7 @@ Task::Ptr ModrinthAPI::currentVersion(QString hash, QString hash_format, std::sh { auto netJob = makeShared(QString("Modrinth::GetCurrentVersion"), APPLICATION->network()); - netJob->addNetAction(Net::Download::makeByteArray( + netJob->addNetAction(Net::ApiDownload::makeByteArray( QString(BuildConfig.MODRINTH_PROD_URL + "/version_file/%1?algorithm=%2").arg(hash, hash_format), response)); return netJob; @@ -31,7 +33,7 @@ Task::Ptr ModrinthAPI::currentVersions(const QStringList& hashes, QString hash_f QJsonDocument body(body_obj); auto body_raw = body.toJson(); - netJob->addNetAction(Net::Upload::makeByteArray(QString(BuildConfig.MODRINTH_PROD_URL + "/version_files"), response, body_raw)); + netJob->addNetAction(Net::ApiUpload::makeByteArray(QString(BuildConfig.MODRINTH_PROD_URL + "/version_files"), response, body_raw)); return netJob; } @@ -39,7 +41,7 @@ Task::Ptr ModrinthAPI::currentVersions(const QStringList& hashes, QString hash_f Task::Ptr ModrinthAPI::latestVersion(QString hash, QString hash_format, std::optional> mcVersions, - std::optional loaders, + std::optional loaders, std::shared_ptr response) { auto netJob = makeShared(QString("Modrinth::GetLatestVersion"), APPLICATION->network()); @@ -60,7 +62,7 @@ Task::Ptr ModrinthAPI::latestVersion(QString hash, QJsonDocument body(body_obj); auto body_raw = body.toJson(); - netJob->addNetAction(Net::Upload::makeByteArray( + netJob->addNetAction(Net::ApiUpload::makeByteArray( QString(BuildConfig.MODRINTH_PROD_URL + "/version_file/%1/update?algorithm=%2").arg(hash, hash_format), response, body_raw)); return netJob; @@ -69,7 +71,7 @@ Task::Ptr ModrinthAPI::latestVersion(QString hash, Task::Ptr ModrinthAPI::latestVersions(const QStringList& hashes, QString hash_format, std::optional> mcVersions, - std::optional loaders, + std::optional loaders, std::shared_ptr response) { auto netJob = makeShared(QString("Modrinth::GetLatestVersions"), APPLICATION->network()); @@ -93,7 +95,8 @@ Task::Ptr ModrinthAPI::latestVersions(const QStringList& hashes, QJsonDocument body(body_obj); auto body_raw = body.toJson(); - netJob->addNetAction(Net::Upload::makeByteArray(QString(BuildConfig.MODRINTH_PROD_URL + "/version_files/update"), response, body_raw)); + netJob->addNetAction( + Net::ApiUpload::makeByteArray(QString(BuildConfig.MODRINTH_PROD_URL + "/version_files/update"), response, body_raw)); return netJob; } @@ -103,7 +106,7 @@ Task::Ptr ModrinthAPI::getProjects(QStringList addonIds, std::shared_ptr(QString("Modrinth::GetProjects"), APPLICATION->network()); auto searchUrl = getMultipleModInfoURL(addonIds); - netJob->addNetAction(Net::Download::makeByteArray(QUrl(searchUrl), response)); + netJob->addNetAction(Net::ApiDownload::makeByteArray(QUrl(searchUrl), response)); return netJob; } diff --git a/launcher/modplatform/modrinth/ModrinthAPI.h b/launcher/modplatform/modrinth/ModrinthAPI.h index 58af14cc7..d0f0811b2 100644 --- a/launcher/modplatform/modrinth/ModrinthAPI.h +++ b/launcher/modplatform/modrinth/ModrinthAPI.h @@ -19,13 +19,13 @@ class ModrinthAPI : public NetworkResourceAPI { auto latestVersion(QString hash, QString hash_format, std::optional> mcVersions, - std::optional loaders, + std::optional loaders, std::shared_ptr response) -> Task::Ptr; auto latestVersions(const QStringList& hashes, QString hash_format, std::optional> mcVersions, - std::optional loaders, + std::optional loaders, std::shared_ptr response) -> Task::Ptr; Task::Ptr getProjects(QStringList addonIds, std::shared_ptr response) const override; @@ -35,20 +35,19 @@ class ModrinthAPI : public NetworkResourceAPI { inline auto getAuthorURL(const QString& name) const -> QString { return "https://modrinth.com/user/" + name; }; - static auto getModLoaderStrings(const ModLoaderTypes types) -> const QStringList + static auto getModLoaderStrings(const ModPlatform::ModLoaderTypes types) -> const QStringList { QStringList l; - for (auto loader : { Forge, Fabric, Quilt, LiteLoader }) { + for (auto loader : + { ModPlatform::NeoForge, ModPlatform::Forge, ModPlatform::Fabric, ModPlatform::Quilt, ModPlatform::LiteLoader }) { if (types & loader) { l << getModLoaderString(loader); } } - if ((types & Quilt) && (~types & Fabric)) // Add Fabric if Quilt is in use, if Fabric isn't already there - l << getModLoaderString(Fabric); return l; } - static auto getModLoaderFilters(ModLoaderTypes types) -> const QString + static auto getModLoaderFilters(ModPlatform::ModLoaderTypes types) -> const QString { QStringList l; for (auto loader : getModLoaderStrings(types)) { @@ -141,7 +140,10 @@ class ModrinthAPI : public NetworkResourceAPI { return s.isEmpty() ? QString() : s; } - static inline auto validateModLoaders(ModLoaderTypes loaders) -> bool { return loaders & (Forge | Fabric | Quilt | LiteLoader); } + static inline auto validateModLoaders(ModPlatform::ModLoaderTypes loaders) -> bool + { + return loaders & (ModPlatform::NeoForge | ModPlatform::Forge | ModPlatform::Fabric | ModPlatform::Quilt | ModPlatform::LiteLoader); + } [[nodiscard]] std::optional getDependencyURL(DependencySearchArgs const& args) const override { diff --git a/launcher/modplatform/modrinth/ModrinthCheckUpdate.cpp b/launcher/modplatform/modrinth/ModrinthCheckUpdate.cpp index 845e0c61e..804eb0ba8 100644 --- a/launcher/modplatform/modrinth/ModrinthCheckUpdate.cpp +++ b/launcher/modplatform/modrinth/ModrinthCheckUpdate.cpp @@ -11,7 +11,6 @@ #include "tasks/ConcurrentTask.h" #include "minecraft/mod/ModFolderModel.h" -#include "minecraft/mod/ResourceFolderModel.h" static ModrinthAPI api; static ModPlatform::ProviderCapabilities ProviderCaps; @@ -111,11 +110,11 @@ void ModrinthCheckUpdate::executeTask() // so we may want to filter it QString loader_filter; if (m_loaders.has_value()) { - static auto flags = { ResourceAPI::ModLoaderType::Forge, ResourceAPI::ModLoaderType::Fabric, - ResourceAPI::ModLoaderType::Quilt }; + static auto flags = { ModPlatform::ModLoaderType::NeoForge, ModPlatform::ModLoaderType::Forge, + ModPlatform::ModLoaderType::Fabric, ModPlatform::ModLoaderType::Quilt }; for (auto flag : flags) { if (m_loaders.value().testFlag(flag)) { - loader_filter = api.getModLoaderString(flag); + loader_filter = ModPlatform::getModLoaderString(flag); break; } } diff --git a/launcher/modplatform/modrinth/ModrinthCheckUpdate.h b/launcher/modplatform/modrinth/ModrinthCheckUpdate.h index 4583dd6ce..f2f2c7e92 100644 --- a/launcher/modplatform/modrinth/ModrinthCheckUpdate.h +++ b/launcher/modplatform/modrinth/ModrinthCheckUpdate.h @@ -10,7 +10,7 @@ class ModrinthCheckUpdate : public CheckUpdateTask { public: ModrinthCheckUpdate(QList& mods, std::list& mcVersions, - std::optional loaders, + std::optional loaders, std::shared_ptr mods_folder) : CheckUpdateTask(mods, mcVersions, loaders, mods_folder) {} diff --git a/launcher/modplatform/modrinth/ModrinthInstanceCreationTask.cpp b/launcher/modplatform/modrinth/ModrinthInstanceCreationTask.cpp index 16e375cde..9ff6b374d 100644 --- a/launcher/modplatform/modrinth/ModrinthInstanceCreationTask.cpp +++ b/launcher/modplatform/modrinth/ModrinthInstanceCreationTask.cpp @@ -11,6 +11,7 @@ #include "net/ChecksumValidator.h" +#include "net/ApiDownload.h" #include "net/NetJob.h" #include "settings/INISettingsObject.h" @@ -210,6 +211,8 @@ bool ModrinthCreationTask::createInstance() components->setComponentVersion("org.quiltmc.quilt-loader", m_quilt_version); if (!m_forge_version.isEmpty()) components->setComponentVersion("net.minecraftforge", m_forge_version); + if (!m_neoForge_version.isEmpty()) + components->setComponentVersion("net.neoforged", m_neoForge_version); if (m_instIcon != "default") { instance.setIconKey(m_instIcon); @@ -238,7 +241,7 @@ bool ModrinthCreationTask::createInstance() } qDebug() << "Will try to download" << file.downloads.front() << "to" << file_path; - auto dl = Net::Download::makeFile(file.downloads.dequeue(), file_path); + auto dl = Net::ApiDownload::makeFile(file.downloads.dequeue(), file_path); dl->addValidator(new Net::ChecksumValidator(file.hashAlgorithm, file.hash)); m_files_job->addNetAction(dl); @@ -247,7 +250,7 @@ bool ModrinthCreationTask::createInstance() // MultipleOptionsTask's , once those exist :) auto param = dl.toWeakRef(); connect(dl.get(), &NetAction::failed, [this, &file, file_path, param] { - auto ndl = Net::Download::makeFile(file.downloads.dequeue(), file_path); + auto ndl = Net::ApiDownload::makeFile(file.downloads.dequeue(), file_path); ndl->addValidator(new Net::ChecksumValidator(file.hashAlgorithm, file.hash)); m_files_job->addNetAction(ndl); if (auto shared = param.lock()) @@ -397,6 +400,8 @@ bool ModrinthCreationTask::parseManifest(const QString& index_path, m_quilt_version = Json::requireString(*it, "Quilt Loader version"); } else if (name == "forge") { m_forge_version = Json::requireString(*it, "Forge version"); + } else if (name == "neoforge") { + m_neoForge_version = Json::requireString(*it, "NeoForge version"); } else { throw JSONValidationError("Unknown dependency type: " + name); } diff --git a/launcher/modplatform/modrinth/ModrinthInstanceCreationTask.h b/launcher/modplatform/modrinth/ModrinthInstanceCreationTask.h index 07e417be5..1bd5b7de9 100644 --- a/launcher/modplatform/modrinth/ModrinthInstanceCreationTask.h +++ b/launcher/modplatform/modrinth/ModrinthInstanceCreationTask.h @@ -39,7 +39,7 @@ class ModrinthCreationTask final : public InstanceCreationTask { private: QWidget* m_parent = nullptr; - QString m_minecraft_version, m_fabric_version, m_quilt_version, m_forge_version; + QString m_minecraft_version, m_fabric_version, m_quilt_version, m_forge_version, m_neoForge_version; QString m_managed_id, m_managed_version_id, m_managed_name; std::vector m_files; diff --git a/launcher/modplatform/modrinth/ModrinthPackExportTask.cpp b/launcher/modplatform/modrinth/ModrinthPackExportTask.cpp index 7bf296398..a9ddb0c91 100644 --- a/launcher/modplatform/modrinth/ModrinthPackExportTask.cpp +++ b/launcher/modplatform/modrinth/ModrinthPackExportTask.cpp @@ -33,12 +33,14 @@ const QStringList ModrinthPackExportTask::FILE_EXTENSIONS({ "jar", "litemod", "z ModrinthPackExportTask::ModrinthPackExportTask(const QString& name, const QString& version, const QString& summary, + bool optionalFiles, InstancePtr instance, const QString& output, MMCZip::FilterFunction filter) : name(name) , version(version) , summary(summary) + , optionalFiles(optionalFiles) , instance(instance) , mcInstance(dynamic_cast(instance.get())) , gameRoot(instance->gameRoot()) @@ -174,10 +176,10 @@ void ModrinthPackExportTask::parseApiResponse(const std::shared_ptr if (obj.isEmpty()) continue; - const QJsonArray files = obj["files"].toArray(); - if (auto fileIter = std::find_if(files.begin(), files.end(), + const QJsonArray files_array = obj["files"].toArray(); + if (auto fileIter = std::find_if(files_array.begin(), files_array.end(), [&iterator](const QJsonValue& file) { return file["hashes"]["sha512"] == iterator.value(); }); - fileIter != files.end()) { + fileIter != files_array.end()) { // map the file to the url resolvedFiles[iterator.key()] = ResolvedFile{ fileIter->toObject()["hashes"].toObject()["sha1"].toString(), iterator.value(), @@ -245,6 +247,7 @@ QByteArray ModrinthPackExportTask::generateIndex() const ComponentPtr quilt = profile->getComponent("org.quiltmc.quilt-loader"); const ComponentPtr fabric = profile->getComponent("net.fabricmc.fabric-loader"); const ComponentPtr forge = profile->getComponent("net.minecraftforge"); + const ComponentPtr neoForge = profile->getComponent("net.neoforged"); // convert all available components to mrpack dependencies QJsonObject dependencies; @@ -256,6 +259,8 @@ QByteArray ModrinthPackExportTask::generateIndex() dependencies["fabric-loader"] = fabric->m_version; if (forge != nullptr) dependencies["forge"] = forge->m_version; + if (neoForge != nullptr) + dependencies["neoforge"] = neoForge->m_version; out["dependencies"] = dependencies; } @@ -267,16 +272,18 @@ QByteArray ModrinthPackExportTask::generateIndex() QString path = iterator.key(); const ResolvedFile& value = iterator.value(); - // detect disabled mod - const QFileInfo pathInfo(path); - if (pathInfo.suffix() == "disabled") { - // rename it - path = pathInfo.dir().filePath(pathInfo.completeBaseName()); - // ...and make it optional - QJsonObject env; - env["client"] = "optional"; - env["server"] = "optional"; - fileOut["env"] = env; + if (optionalFiles) { + // detect disabled mod + const QFileInfo pathInfo(path); + if (pathInfo.suffix() == "disabled") { + // rename it + path = pathInfo.dir().filePath(pathInfo.completeBaseName()); + // ...and make it optional + QJsonObject env; + env["client"] = "optional"; + env["server"] = "optional"; + fileOut["env"] = env; + } } fileOut["path"] = path; diff --git a/launcher/modplatform/modrinth/ModrinthPackExportTask.h b/launcher/modplatform/modrinth/ModrinthPackExportTask.h index 1f9e0eb77..83540dfac 100644 --- a/launcher/modplatform/modrinth/ModrinthPackExportTask.h +++ b/launcher/modplatform/modrinth/ModrinthPackExportTask.h @@ -31,6 +31,7 @@ class ModrinthPackExportTask : public Task { ModrinthPackExportTask(const QString& name, const QString& version, const QString& summary, + bool optionalFiles, InstancePtr instance, const QString& output, MMCZip::FilterFunction filter); @@ -50,6 +51,7 @@ class ModrinthPackExportTask : public Task { // inputs const QString name, version, summary; + const bool optionalFiles; const InstancePtr instance; MinecraftInstance* mcInstance; const QDir gameRoot; diff --git a/launcher/modplatform/modrinth/ModrinthPackIndex.cpp b/launcher/modplatform/modrinth/ModrinthPackIndex.cpp index dbb6aeed6..f1e77d465 100644 --- a/launcher/modplatform/modrinth/ModrinthPackIndex.cpp +++ b/launcher/modplatform/modrinth/ModrinthPackIndex.cpp @@ -93,19 +93,19 @@ void Modrinth::loadExtraPackData(ModPlatform::IndexedPack& pack, QJsonObject& ob pack.extraDataLoaded = true; } -void Modrinth::loadIndexedPackVersions(ModPlatform::IndexedPack& pack, - QJsonArray& arr, - const shared_qobject_ptr& network, - const BaseInstance* inst) +void Modrinth::loadIndexedPackVersions(ModPlatform::IndexedPack& pack, QJsonArray& arr, const BaseInstance* inst) { QVector unsortedVersions; - QString mcVersion = (static_cast(inst))->getPackProfile()->getComponentVersion("net.minecraft"); + auto profile = (dynamic_cast(inst))->getPackProfile(); + QString mcVersion = profile->getComponentVersion("net.minecraft"); + auto loaders = profile->getSupportedModLoaders(); for (auto versionIter : arr) { auto obj = versionIter.toObject(); auto file = loadIndexedPackVersion(obj); - if (file.fileId.isValid()) // Heuristic to check if the returned value is valid + if (file.fileId.isValid() && + (!loaders.has_value() || !file.loaders || loaders.value() & file.loaders)) // Heuristic to check if the returned value is valid unsortedVersions.append(file); } auto orderSortPredicate = [](const ModPlatform::IndexedVersion& a, const ModPlatform::IndexedVersion& b) -> bool { @@ -135,7 +135,18 @@ auto Modrinth::loadIndexedPackVersion(QJsonObject& obj, QString preferred_hash_t } auto loaders = Json::requireArray(obj, "loaders"); for (auto loader : loaders) { - file.loaders.append(loader.toString()); + if (loader == "neoforge") + file.loaders |= ModPlatform::NeoForge; + if (loader == "forge") + file.loaders |= ModPlatform::Forge; + if (loader == "cauldron") + file.loaders |= ModPlatform::Cauldron; + if (loader == "liteloader") + file.loaders |= ModPlatform::LiteLoader; + if (loader == "fabric") + file.loaders |= ModPlatform::Fabric; + if (loader == "quilt") + file.loaders |= ModPlatform::Quilt; } file.version = Json::requireString(obj, "name"); file.version_number = Json::requireString(obj, "version_number"); @@ -221,15 +232,20 @@ auto Modrinth::loadIndexedPackVersion(QJsonObject& obj, QString preferred_hash_t return {}; } -auto Modrinth::loadDependencyVersions(const ModPlatform::Dependency& m, QJsonArray& arr) -> ModPlatform::IndexedVersion +auto Modrinth::loadDependencyVersions([[maybe_unused]] const ModPlatform::Dependency& m, QJsonArray& arr, const BaseInstance* inst) + -> ModPlatform::IndexedVersion { - QVector versions; + auto profile = (dynamic_cast(inst))->getPackProfile(); + QString mcVersion = profile->getComponentVersion("net.minecraft"); + auto loaders = profile->getSupportedModLoaders(); + QVector versions; for (auto versionIter : arr) { auto obj = versionIter.toObject(); auto file = loadIndexedPackVersion(obj); - if (file.fileId.isValid()) // Heuristic to check if the returned value is valid + if (file.fileId.isValid() && + (!loaders.has_value() || !file.loaders || loaders.value() & file.loaders)) // Heuristic to check if the returned value is valid versions.append(file); } auto orderSortPredicate = [](const ModPlatform::IndexedVersion& a, const ModPlatform::IndexedVersion& b) -> bool { @@ -238,4 +254,4 @@ auto Modrinth::loadDependencyVersions(const ModPlatform::Dependency& m, QJsonArr }; std::sort(versions.begin(), versions.end(), orderSortPredicate); return versions.length() != 0 ? versions.front() : ModPlatform::IndexedVersion(); -} \ No newline at end of file +} diff --git a/launcher/modplatform/modrinth/ModrinthPackIndex.h b/launcher/modplatform/modrinth/ModrinthPackIndex.h index 58a0f227c..93f91eec2 100644 --- a/launcher/modplatform/modrinth/ModrinthPackIndex.h +++ b/launcher/modplatform/modrinth/ModrinthPackIndex.h @@ -26,11 +26,8 @@ namespace Modrinth { void loadIndexedPack(ModPlatform::IndexedPack& m, QJsonObject& obj); void loadExtraPackData(ModPlatform::IndexedPack& m, QJsonObject& obj); -void loadIndexedPackVersions(ModPlatform::IndexedPack& pack, - QJsonArray& arr, - const shared_qobject_ptr& network, - const BaseInstance* inst); +void loadIndexedPackVersions(ModPlatform::IndexedPack& pack, QJsonArray& arr, const BaseInstance* inst); auto loadIndexedPackVersion(QJsonObject& obj, QString hash_type = "sha512", QString filename_prefer = "") -> ModPlatform::IndexedVersion; -auto loadDependencyVersions(const ModPlatform::Dependency& m, QJsonArray& arr) -> ModPlatform::IndexedVersion; +auto loadDependencyVersions(const ModPlatform::Dependency& m, QJsonArray& arr, const BaseInstance* inst) -> ModPlatform::IndexedVersion; } // namespace Modrinth diff --git a/launcher/modplatform/packwiz/Packwiz.cpp b/launcher/modplatform/packwiz/Packwiz.cpp index 1be283787..71f66bf3e 100644 --- a/launcher/modplatform/packwiz/Packwiz.cpp +++ b/launcher/modplatform/packwiz/Packwiz.cpp @@ -89,7 +89,8 @@ auto intEntry(toml::table table, QString entry_name) -> int return node.value_or(0); } -auto V1::createModFormat(QDir& index_dir, ModPlatform::IndexedPack& mod_pack, ModPlatform::IndexedVersion& mod_version) -> Mod +auto V1::createModFormat([[maybe_unused]] QDir& index_dir, ModPlatform::IndexedPack& mod_pack, ModPlatform::IndexedVersion& mod_version) + -> Mod { Mod mod; @@ -114,7 +115,7 @@ auto V1::createModFormat(QDir& index_dir, ModPlatform::IndexedPack& mod_pack, Mo return mod; } -auto V1::createModFormat(QDir& index_dir, ::Mod& internal_mod, QString slug) -> Mod +auto V1::createModFormat(QDir& index_dir, [[maybe_unused]] ::Mod& internal_mod, QString slug) -> Mod { // Try getting metadata if it exists Mod mod{ getIndexForMod(index_dir, slug) }; @@ -241,12 +242,13 @@ auto V1::getIndexForMod(QDir& index_dir, QString slug) -> Mod return {}; } #else - table = toml::parse_file(StringUtils::toStdString(index_dir.absoluteFilePath(real_fname))); - if (!table) { + toml::parse_result result = toml::parse_file(StringUtils::toStdString(index_dir.absoluteFilePath(real_fname))); + if (!result) { qWarning() << QString("Could not open file %1!").arg(normalized_fname); - qWarning() << "Reason: " << QString(table.error().what()); + qWarning() << "Reason: " << result.error().description(); return {}; } + table = result.table(); #endif // index_file.close(); diff --git a/launcher/modplatform/technic/SingleZipPackInstallTask.cpp b/launcher/modplatform/technic/SingleZipPackInstallTask.cpp index dd59e652c..ad08d72d4 100644 --- a/launcher/modplatform/technic/SingleZipPackInstallTask.cpp +++ b/launcher/modplatform/technic/SingleZipPackInstallTask.cpp @@ -23,6 +23,8 @@ #include "Application.h" +#include "net/ApiDownload.h" + Technic::SingleZipPackInstallTask::SingleZipPackInstallTask(const QUrl& sourceUrl, const QString& minecraftVersion) { m_sourceUrl = sourceUrl; @@ -45,7 +47,7 @@ void Technic::SingleZipPackInstallTask::executeTask() auto entry = APPLICATION->metacache()->resolveEntry("general", path); entry->setStale(true); m_filesNetJob.reset(new NetJob(tr("Modpack download"), APPLICATION->network())); - m_filesNetJob->addNetAction(Net::Download::makeCached(m_sourceUrl, entry)); + m_filesNetJob->addNetAction(Net::ApiDownload::makeCached(m_sourceUrl, entry)); m_archivePath = entry->getFullPath(); auto job = m_filesNetJob.get(); connect(job, &NetJob::succeeded, this, &Technic::SingleZipPackInstallTask::downloadSucceeded); diff --git a/launcher/modplatform/technic/SolderPackInstallTask.cpp b/launcher/modplatform/technic/SolderPackInstallTask.cpp index ad564de00..c162d6253 100644 --- a/launcher/modplatform/technic/SolderPackInstallTask.cpp +++ b/launcher/modplatform/technic/SolderPackInstallTask.cpp @@ -42,6 +42,7 @@ #include "SolderPackManifest.h" #include "TechnicPackProcessor.h" +#include "net/ApiDownload.h" #include "net/ChecksumValidator.h" Technic::SolderPackInstallTask::SolderPackInstallTask(shared_qobject_ptr network, @@ -71,7 +72,7 @@ void Technic::SolderPackInstallTask::executeTask() m_filesNetJob.reset(new NetJob(tr("Resolving modpack files"), m_network)); auto sourceUrl = QString("%1/modpack/%2/%3").arg(m_solderUrl.toString(), m_pack, m_version); - m_filesNetJob->addNetAction(Net::Download::makeByteArray(sourceUrl, m_response)); + m_filesNetJob->addNetAction(Net::ApiDownload::makeByteArray(sourceUrl, m_response)); auto job = m_filesNetJob.get(); connect(job, &NetJob::succeeded, this, &Technic::SolderPackInstallTask::fileListSucceeded); @@ -111,7 +112,7 @@ void Technic::SolderPackInstallTask::fileListSucceeded() for (const auto& mod : build.mods) { auto path = FS::PathCombine(m_outputDir.path(), QString("%1").arg(i)); - auto dl = Net::Download::makeFile(mod.url, path); + auto dl = Net::ApiDownload::makeFile(mod.url, path); if (!mod.md5.isEmpty()) { auto rawMd5 = QByteArray::fromHex(mod.md5.toLatin1()); dl->addValidator(new Net::ChecksumValidator(QCryptographicHash::Md5, rawMd5)); diff --git a/launcher/modplatform/technic/TechnicPackProcessor.cpp b/launcher/modplatform/technic/TechnicPackProcessor.cpp index 778a6531e..3b9424bf8 100644 --- a/launcher/modplatform/technic/TechnicPackProcessor.cpp +++ b/launcher/modplatform/technic/TechnicPackProcessor.cpp @@ -31,7 +31,7 @@ void Technic::TechnicPackProcessor::run(SettingsObjectPtr globalSettings, const QString& instIcon, const QString& stagingPath, const QString& minecraftVersion, - const bool isSolder) + [[maybe_unused]] const bool isSolder) { QString minecraftPath = FS::PathCombine(stagingPath, ".minecraft"); QString configPath = FS::PathCombine(stagingPath, "instance.cfg"); @@ -138,15 +138,15 @@ void Technic::TechnicPackProcessor::run(SettingsObjectPtr globalSettings, try { QJsonDocument doc = Json::requireDocument(data); QJsonObject root = Json::requireObject(doc, "version.json"); - QString minecraftVersion = Json::ensureString(root, "inheritsFrom", QString(), ""); - if (minecraftVersion.isEmpty()) { + QString packMinecraftVersion = Json::ensureString(root, "inheritsFrom", QString(), ""); + if (packMinecraftVersion.isEmpty()) { if (fmlMinecraftVersion.isEmpty()) { emit failed(tr("Could not understand \"version.json\":\ninheritsFrom is missing")); return; } - minecraftVersion = fmlMinecraftVersion; + packMinecraftVersion = fmlMinecraftVersion; } - components->setComponentVersion("net.minecraft", minecraftVersion, true); + components->setComponentVersion("net.minecraft", packMinecraftVersion, true); for (auto library : Json::ensureArray(root, "libraries", {})) { if (!library.isObject()) { continue; diff --git a/launcher/net/ApiDownload.cpp b/launcher/net/ApiDownload.cpp new file mode 100644 index 000000000..aaa8ff650 --- /dev/null +++ b/launcher/net/ApiDownload.cpp @@ -0,0 +1,66 @@ +// SPDX-License-Identifier: GPL-3.0-only +/* + * Prism Launcher - Minecraft Launcher + * Copyright (C) 2023 Rachel Powers <508861+Ryex@users.noreply.github.com> + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, version 3. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + * + */ + +#include "net/ApiDownload.h" +#include "ByteArraySink.h" +#include "ChecksumValidator.h" +#include "MetaCacheSink.h" +#include "net/NetAction.h" + +namespace Net { + +auto ApiDownload::makeCached(QUrl url, MetaEntryPtr entry, Options options) -> Download::Ptr +{ + auto dl = makeShared(); + dl->m_url = url; + dl->setObjectName(QString("CACHE:") + url.toString()); + dl->m_options = options; + auto md5Node = new ChecksumValidator(QCryptographicHash::Md5); + auto cachedNode = new MetaCacheSink(entry, md5Node, options.testFlag(Option::MakeEternal)); + dl->m_sink.reset(cachedNode); + return dl; +} + +auto ApiDownload::makeByteArray(QUrl url, std::shared_ptr output, Options options) -> Download::Ptr +{ + auto dl = makeShared(); + dl->m_url = url; + dl->setObjectName(QString("BYTES:") + url.toString()); + dl->m_options = options; + dl->m_sink.reset(new ByteArraySink(output)); + return dl; +} + +auto ApiDownload::makeFile(QUrl url, QString path, Options options) -> Download::Ptr +{ + auto dl = makeShared(); + dl->m_url = url; + dl->setObjectName(QString("FILE:") + url.toString()); + dl->m_options = options; + dl->m_sink.reset(new FileSink(path)); + return dl; +} + +void ApiDownload::init() +{ + qDebug() << "Setting up api download"; + auto api_headers = new ApiHeaderProxy(); + addHeaderProxy(api_headers); +} +} // namespace Net diff --git a/launcher/net/ApiDownload.h b/launcher/net/ApiDownload.h new file mode 100644 index 000000000..638c94e11 --- /dev/null +++ b/launcher/net/ApiDownload.h @@ -0,0 +1,38 @@ +// SPDX-License-Identifier: GPL-3.0-only +/* + * Prism Launcher - Minecraft Launcher + * Copyright (C) 2023 Rachel Powers <508861+Ryex@users.noreply.github.com> + * + * 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 "ApiHeaderProxy.h" +#include "Download.h" + +namespace Net { + +class ApiDownload : public Download { + public: + virtual ~ApiDownload() = default; + + static auto makeCached(QUrl url, MetaEntryPtr entry, Options options = Option::NoOptions) -> Download::Ptr; + static auto makeByteArray(QUrl url, std::shared_ptr output, Options options = Option::NoOptions) -> Download::Ptr; + static auto makeFile(QUrl url, QString path, Options options = Option::NoOptions) -> Download::Ptr; + + void init() override; +}; + +} // namespace Net diff --git a/launcher/net/ApiHeaderProxy.h b/launcher/net/ApiHeaderProxy.h new file mode 100644 index 000000000..789a6fada --- /dev/null +++ b/launcher/net/ApiHeaderProxy.h @@ -0,0 +1,49 @@ +// SPDX-License-Identifier: GPL-3.0-only +/* + * Prism Launcher - Minecraft Launcher + * Copyright (C) 2023 Rachel Powers <508861+Ryex@users.noreply.github.com> + * + * 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 "Application.h" +#include "BuildConfig.h" +#include "net/HeaderProxy.h" + +namespace Net { + +class ApiHeaderProxy : public HeaderProxy { + public: + ApiHeaderProxy() : HeaderProxy() {} + virtual ~ApiHeaderProxy() = default; + + public: + virtual QList headers(const QNetworkRequest& request) const override + { + QList hdrs; + if (APPLICATION->capabilities() & Application::SupportsFlame && request.url().host() == QUrl(BuildConfig.FLAME_BASE_URL).host()) { + hdrs.append({ "x-api-key", APPLICATION->getFlameAPIKey().toUtf8() }); + } else if (request.url().host() == QUrl(BuildConfig.MODRINTH_PROD_URL).host() || + request.url().host() == QUrl(BuildConfig.MODRINTH_STAGING_URL).host()) { + QString token = APPLICATION->getModrinthAPIToken(); + if (!token.isNull()) + hdrs.append({ "Authorization", token.toUtf8() }); + } + return hdrs; + }; +}; + +} // namespace Net diff --git a/launcher/net/ApiUpload.cpp b/launcher/net/ApiUpload.cpp new file mode 100644 index 000000000..c1221b764 --- /dev/null +++ b/launcher/net/ApiUpload.cpp @@ -0,0 +1,43 @@ +// SPDX-License-Identifier: GPL-3.0-only +/* + * Prism Launcher - Minecraft Launcher + * Copyright (C) 2023 Rachel Powers <508861+Ryex@users.noreply.github.com> + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, version 3. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + * + */ + +#include "net/ApiUpload.h" +#include "ByteArraySink.h" +#include "ChecksumValidator.h" +#include "MetaCacheSink.h" +#include "net/NetAction.h" + +namespace Net { + +Upload::Ptr ApiUpload::makeByteArray(QUrl url, std::shared_ptr output, QByteArray m_post_data) +{ + auto up = makeShared(); + up->m_url = std::move(url); + up->m_sink.reset(new ByteArraySink(output)); + up->m_post_data = std::move(m_post_data); + return up; +} + +void ApiUpload::init() +{ + qDebug() << "Setting up api upload"; + auto api_headers = new ApiHeaderProxy(); + addHeaderProxy(api_headers); +} +} // namespace Net diff --git a/launcher/net/ApiUpload.h b/launcher/net/ApiUpload.h new file mode 100644 index 000000000..b12842b05 --- /dev/null +++ b/launcher/net/ApiUpload.h @@ -0,0 +1,36 @@ +// SPDX-License-Identifier: GPL-3.0-only +/* + * Prism Launcher - Minecraft Launcher + * Copyright (C) 2023 Rachel Powers <508861+Ryex@users.noreply.github.com> + * + * 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 "ApiHeaderProxy.h" +#include "Upload.h" + +namespace Net { + +class ApiUpload : public Upload { + public: + virtual ~ApiUpload() = default; + + static Upload::Ptr makeByteArray(QUrl url, std::shared_ptr output, QByteArray m_post_data); + + void init() override; +}; + +} // namespace Net diff --git a/launcher/net/ByteArraySink.h b/launcher/net/ByteArraySink.h index d6b17d605..7b8f0f8aa 100644 --- a/launcher/net/ByteArraySink.h +++ b/launcher/net/ByteArraySink.h @@ -42,8 +42,6 @@ namespace Net { /* * Sink object for downloads that uses an external QByteArray it doesn't own as a target. - * FIXME: It is possible that the QByteArray is freed while we're doing some operation on it, - * causing a segmentation fault. */ class ByteArraySink : public Sink { public: diff --git a/launcher/net/Download.cpp b/launcher/net/Download.cpp index 9ad49ded3..d25447b2d 100644 --- a/launcher/net/Download.cpp +++ b/launcher/net/Download.cpp @@ -47,17 +47,11 @@ #include "ChecksumValidator.h" #include "MetaCacheSink.h" -#include "Application.h" -#include "BuildConfig.h" - -#include "net/Logging.h" #include "net/NetAction.h" -#include "MMCTime.h" -#include "StringUtils.h" - namespace Net { +#if defined(LAUNCHER_APPLICATION) auto Download::makeCached(QUrl url, MetaEntryPtr entry, Options options) -> Download::Ptr { auto dl = makeShared(); @@ -69,6 +63,7 @@ auto Download::makeCached(QUrl url, MetaEntryPtr entry, Options options) -> Down dl->m_sink.reset(cachedNode); return dl; } +#endif auto Download::makeByteArray(QUrl url, std::shared_ptr output, Options options) -> Download::Ptr { @@ -90,259 +85,8 @@ auto Download::makeFile(QUrl url, QString path, Options options) -> Download::Pt return dl; } -void Download::addValidator(Validator* v) +QNetworkReply* Download::getReply(QNetworkRequest& request) { - m_sink->addValidator(v); -} - -void Download::executeTask() -{ - setStatus(tr("Downloading %1").arg(StringUtils::truncateUrlHumanFriendly(m_url, 80))); - - if (getState() == Task::State::AbortedByUser) { - qCWarning(taskDownloadLogC) << getUid().toString() << "Attempt to start an aborted Download:" << m_url.toString(); - emitAborted(); - return; - } - - QNetworkRequest request(m_url); - m_state = m_sink->init(request); - switch (m_state) { - case State::Succeeded: - emit succeeded(); - qCDebug(taskDownloadLogC) << getUid().toString() << "Download cache hit " << m_url.toString(); - return; - case State::Running: - qCDebug(taskDownloadLogC) << getUid().toString() << "Downloading " << m_url.toString(); - break; - case State::Inactive: - case State::Failed: - emitFailed(); - return; - case State::AbortedByUser: - emitAborted(); - return; - } - - request.setHeader(QNetworkRequest::UserAgentHeader, APPLICATION->getUserAgent().toUtf8()); - // TODO remove duplication - if (APPLICATION->capabilities() & Application::SupportsFlame && request.url().host() == QUrl(BuildConfig.FLAME_BASE_URL).host()) { - request.setRawHeader("x-api-key", APPLICATION->getFlameAPIKey().toUtf8()); - } else if (request.url().host() == QUrl(BuildConfig.MODRINTH_PROD_URL).host() || - request.url().host() == QUrl(BuildConfig.MODRINTH_STAGING_URL).host()) { - QString token = APPLICATION->getModrinthAPIToken(); - if (!token.isNull()) - request.setRawHeader("Authorization", token.toUtf8()); - } - -#if QT_VERSION >= QT_VERSION_CHECK(5, 15, 0) - request.setTransferTimeout(); -#endif - - m_last_progress_time = m_clock.now(); - m_last_progress_bytes = 0; - - QNetworkReply* rep = m_network->get(request); - m_reply.reset(rep); - connect(rep, &QNetworkReply::downloadProgress, this, &Download::downloadProgress); - connect(rep, &QNetworkReply::finished, this, &Download::downloadFinished); -#if QT_VERSION >= QT_VERSION_CHECK(5, 15, 0) // QNetworkReply::errorOccurred added in 5.15 - connect(rep, &QNetworkReply::errorOccurred, this, &Download::downloadError); -#else - connect(rep, QOverload::of(&QNetworkReply::error), this, &Download::downloadError); -#endif - connect(rep, &QNetworkReply::sslErrors, this, &Download::sslErrors); - connect(rep, &QNetworkReply::readyRead, this, &Download::downloadReadyRead); -} - -void Download::downloadProgress(qint64 bytesReceived, qint64 bytesTotal) -{ - auto now = m_clock.now(); - auto elapsed = now - m_last_progress_time; - - // use milliseconds for speed precision - auto elapsed_ms = std::chrono::duration_cast(elapsed); - auto bytes_received_since = bytesReceived - m_last_progress_bytes; - auto dl_speed_bps = (double)bytes_received_since / elapsed_ms.count() * 1000; - auto remaing_time_s = (bytesTotal - bytesReceived) / dl_speed_bps; - - //: Current amount of bytes downloaded, out of the total amount of bytes in the download - QString dl_progress = - tr("%1 / %2").arg(StringUtils::humanReadableFileSize(bytesReceived)).arg(StringUtils::humanReadableFileSize(bytesTotal)); - - QString dl_speed_str; - if (elapsed_ms.count() > 0) { - auto str_eta = bytesTotal > 0 ? Time::humanReadableDuration(remaing_time_s) : tr("unknown"); - //: Download speed, in bytes per second (remaining download time in parenthesis) - dl_speed_str = tr("%1 /s (%2)").arg(StringUtils::humanReadableFileSize(dl_speed_bps)).arg(str_eta); - } else { - //: Download speed at 0 bytes per second - dl_speed_str = tr("0 B/s"); - } - - setDetails(dl_progress + "\n" + dl_speed_str); - - setProgress(bytesReceived, bytesTotal); -} - -void Download::downloadError(QNetworkReply::NetworkError error) -{ - if (error == QNetworkReply::OperationCanceledError) { - qCCritical(taskDownloadLogC) << getUid().toString() << "Aborted " << m_url.toString(); - m_state = State::AbortedByUser; - } else { - if (m_options & Option::AcceptLocalFiles) { - if (m_sink->hasLocalData()) { - m_state = State::Succeeded; - return; - } - } - // error happened during download. - qCCritical(taskDownloadLogC) << getUid().toString() << "Failed " << m_url.toString() << " with reason " << error; - m_state = State::Failed; - } -} - -void Download::sslErrors(const QList& errors) -{ - int i = 1; - for (auto error : errors) { - qCCritical(taskDownloadLogC) << getUid().toString() << "Download" << m_url.toString() << "SSL Error #" << i << " : " - << error.errorString(); - auto cert = error.certificate(); - qCCritical(taskDownloadLogC) << getUid().toString() << "Certificate in question:\n" << cert.toText(); - i++; - } -} - -auto Download::handleRedirect() -> bool -{ - QUrl redirect = m_reply->header(QNetworkRequest::LocationHeader).toUrl(); - if (!redirect.isValid()) { - if (!m_reply->hasRawHeader("Location")) { - // no redirect -> it's fine to continue - return false; - } - // there is a Location header, but it's not correct. we need to apply some workarounds... - QByteArray redirectBA = m_reply->rawHeader("Location"); - if (redirectBA.size() == 0) { - // empty, yet present redirect header? WTF? - return false; - } - QString redirectStr = QString::fromUtf8(redirectBA); - - if (redirectStr.startsWith("//")) { - /* - * IF the URL begins with //, we need to insert the URL scheme. - * See: https://bugreports.qt.io/browse/QTBUG-41061 - * See: http://tools.ietf.org/html/rfc3986#section-4.2 - */ - redirectStr = m_reply->url().scheme() + ":" + redirectStr; - } else if (redirectStr.startsWith("/")) { - /* - * IF the URL begins with /, we need to process it as a relative URL - */ - auto url = m_reply->url(); - url.setPath(redirectStr, QUrl::TolerantMode); - redirectStr = url.toString(); - } - - /* - * Next, make sure the URL is parsed in tolerant mode. Qt doesn't parse the location header in tolerant mode, which causes issues. - * FIXME: report Qt bug for this - */ - redirect = QUrl(redirectStr, QUrl::TolerantMode); - if (!redirect.isValid()) { - qCWarning(taskDownloadLogC) << getUid().toString() << "Failed to parse redirect URL:" << redirectStr; - downloadError(QNetworkReply::ProtocolFailure); - return false; - } - qCDebug(taskDownloadLogC) << getUid().toString() << "Fixed location header:" << redirect; - } else { - qCDebug(taskDownloadLogC) << getUid().toString() << "Location header:" << redirect; - } - - m_url = QUrl(redirect.toString()); - qCDebug(taskDownloadLogC) << getUid().toString() << "Following redirect to " << m_url.toString(); - startAction(m_network); - - return true; -} - -void Download::downloadFinished() -{ - // handle HTTP redirection first - if (handleRedirect()) { - qCDebug(taskDownloadLogC) << getUid().toString() << "Download redirected:" << m_url.toString(); - return; - } - - // if the download failed before this point ... - if (m_state == State::Succeeded) // pretend to succeed so we continue processing :) - { - qCDebug(taskDownloadLogC) << getUid().toString() << "Download failed but we are allowed to proceed:" << m_url.toString(); - m_sink->abort(); - m_reply.reset(); - emit succeeded(); - return; - } else if (m_state == State::Failed) { - qCDebug(taskDownloadLogC) << getUid().toString() << "Download failed in previous step:" << m_url.toString(); - m_sink->abort(); - m_reply.reset(); - emit failed(""); - return; - } else if (m_state == State::AbortedByUser) { - qCDebug(taskDownloadLogC) << getUid().toString() << "Download aborted in previous step:" << m_url.toString(); - m_sink->abort(); - m_reply.reset(); - emit aborted(); - return; - } - - // make sure we got all the remaining data, if any - auto data = m_reply->readAll(); - if (data.size()) { - qCDebug(taskDownloadLogC) << getUid().toString() << "Writing extra" << data.size() << "bytes"; - m_state = m_sink->write(data); - } - - // otherwise, finalize the whole graph - m_state = m_sink->finalize(*m_reply.get()); - if (m_state != State::Succeeded) { - qCDebug(taskDownloadLogC) << getUid().toString() << "Download failed to finalize:" << m_url.toString(); - m_sink->abort(); - m_reply.reset(); - emit failed(""); - return; - } - - m_reply.reset(); - qCDebug(taskDownloadLogC) << getUid().toString() << "Download succeeded:" << m_url.toString(); - emit succeeded(); -} - -void Download::downloadReadyRead() -{ - if (m_state == State::Running) { - auto data = m_reply->readAll(); - m_state = m_sink->write(data); - if (m_state == State::Failed) { - qCCritical(taskDownloadLogC) << getUid().toString() << "Failed to process response chunk"; - } - // qDebug() << "Download" << m_url.toString() << "gained" << data.size() << "bytes"; - } else { - qCCritical(taskDownloadLogC) << getUid().toString() << "Cannot write download data! illegal status " << m_status; - } -} - -} // namespace Net - -auto Net::Download::abort() -> bool -{ - if (m_reply) { - m_reply->abort(); - } else { - m_state = State::AbortedByUser; - } - return true; + return m_network->get(request); } +} // namespace Net \ No newline at end of file diff --git a/launcher/net/Download.h b/launcher/net/Download.h index 2e861732d..5f6a5caf1 100644 --- a/launcher/net/Download.h +++ b/launcher/net/Download.h @@ -38,57 +38,26 @@ #pragma once -#include - #include "HttpMetaCache.h" -#include "NetAction.h" -#include "Sink.h" -#include "Validator.h" #include "QObjectPtr.h" +#include "net/NetRequest.h" namespace Net { -class Download : public NetAction { +class Download : public NetRequest { Q_OBJECT - public: using Ptr = shared_qobject_ptr; - enum class Option { NoOptions = 0, AcceptLocalFiles = 1, MakeEternal = 2 }; - Q_DECLARE_FLAGS(Options, Option) - - public: - ~Download() override = default; + explicit Download() : NetRequest() { logCat = taskDownloadLogC; } +#if defined(LAUNCHER_APPLICATION) static auto makeCached(QUrl url, MetaEntryPtr entry, Options options = Option::NoOptions) -> Download::Ptr; +#endif + static auto makeByteArray(QUrl url, std::shared_ptr output, Options options = Option::NoOptions) -> Download::Ptr; static auto makeFile(QUrl url, QString path, Options options = Option::NoOptions) -> Download::Ptr; - public: - void addValidator(Validator* v); - auto abort() -> bool override; - auto canAbort() const -> bool override { return true; }; - - private: - auto handleRedirect() -> bool; - - protected slots: - void downloadProgress(qint64 bytesReceived, qint64 bytesTotal) override; - void downloadError(QNetworkReply::NetworkError error) override; - void sslErrors(const QList& errors) override; - void downloadFinished() override; - void downloadReadyRead() override; - - public slots: - void executeTask() override; - - private: - std::unique_ptr m_sink; - Options m_options; - - std::chrono::steady_clock m_clock; - std::chrono::time_point m_last_progress_time; - qint64 m_last_progress_bytes; + protected: + virtual QNetworkReply* getReply(QNetworkRequest&) override; }; } // namespace Net - -Q_DECLARE_OPERATORS_FOR_FLAGS(Net::Download::Options) diff --git a/launcher/net/HeaderProxy.h b/launcher/net/HeaderProxy.h new file mode 100644 index 000000000..20362924f --- /dev/null +++ b/launcher/net/HeaderProxy.h @@ -0,0 +1,49 @@ +// SPDX-License-Identifier: GPL-3.0-only +/* + * Prism Launcher - Minecraft Launcher + * Copyright (C) 2023 Rachel Powers <508861+Ryex@users.noreply.github.com> + * + * 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 + +namespace Net { + +struct HeaderPair { + QByteArray headerName; + QByteArray headerValue; +}; + +class HeaderProxy { + public: + HeaderProxy() {} + virtual ~HeaderProxy() {} + + public: + virtual QList headers(const QNetworkRequest& request) const = 0; + + public: + void writeHeaders(QNetworkRequest& request) + { + for (auto header : headers(request)) { + request.setRawHeader(header.headerName, header.headerValue); + } + } +}; + +} // namespace Net diff --git a/launcher/net/HttpMetaCache.cpp b/launcher/net/HttpMetaCache.cpp index 7809d40fc..f37bc0bf8 100644 --- a/launcher/net/HttpMetaCache.cpp +++ b/launcher/net/HttpMetaCache.cpp @@ -218,9 +218,24 @@ void HttpMetaCache::Load() if (!index.open(QIODevice::ReadOnly)) return; - QJsonDocument json = QJsonDocument::fromJson(index.readAll()); + QJsonParseError parseError; + QJsonDocument json = QJsonDocument::fromJson(index.readAll(), &parseError); - auto root = Json::requireObject(json, "HttpMetaCache root"); + // Fail if the JSON is invalid. + if (parseError.error != QJsonParseError::NoError) { + qCritical() << QString("Failed to parse HttpMetaCache file: %1 at offset %2") + .arg(parseError.errorString(), QString::number(parseError.offset)) + .toUtf8(); + return; + } + + // Make sure the root is an object. + if (!json.isObject()) { + qCritical() << "HttpMetaCache root should be an object."; + return; + } + + auto root = json.object(); // check file version first auto version_val = Json::ensureString(root, "version"); diff --git a/launcher/net/HttpMetaCache.h b/launcher/net/HttpMetaCache.h index 0dcb5668d..036a8dd94 100644 --- a/launcher/net/HttpMetaCache.h +++ b/launcher/net/HttpMetaCache.h @@ -74,7 +74,7 @@ class MetaEntry { auto getMaximumAge() -> qint64 { return m_max_age; } void setMaximumAge(qint64 age) { m_max_age = age; } - bool isExpired(qint64 offset) { return !m_is_eternal && (m_current_age >= m_max_age - offset); }; + bool isExpired(qint64 offset) { return !m_is_eternal && (m_current_age >= m_max_age - offset); } protected: QString m_baseId; diff --git a/launcher/net/NetAction.h b/launcher/net/NetAction.h index 54eddc72b..b66b91941 100644 --- a/launcher/net/NetAction.h +++ b/launcher/net/NetAction.h @@ -42,10 +42,12 @@ #include "QObjectPtr.h" #include "tasks/Task.h" +#include "HeaderProxy.h" + class NetAction : public Task { Q_OBJECT protected: - explicit NetAction() : Task(){}; + explicit NetAction() : Task() {} public: using Ptr = shared_qobject_ptr; @@ -56,6 +58,9 @@ class NetAction : public Task { void setNetwork(shared_qobject_ptr network) { m_network = network; } + void addHeaderProxy(Net::HeaderProxy* proxy) { m_headerProxies.push_back(std::shared_ptr(proxy)); } + virtual void init() = 0; + protected slots: virtual void downloadProgress(qint64 bytesReceived, qint64 bytesTotal) = 0; virtual void downloadError(QNetworkReply::NetworkError error) = 0; @@ -71,7 +76,7 @@ class NetAction : public Task { qCritical() << "Certificate in question:\n" << cert.toText(); i++; } - }; + } public slots: void startAction(shared_qobject_ptr network) @@ -81,7 +86,7 @@ class NetAction : public Task { } protected: - void executeTask() override{}; + void executeTask() override {} public: shared_qobject_ptr m_network; @@ -91,4 +96,5 @@ class NetAction : public Task { /// source URL QUrl m_url; + std::vector> m_headerProxies; }; diff --git a/launcher/net/NetRequest.cpp b/launcher/net/NetRequest.cpp new file mode 100644 index 000000000..ff59da18b --- /dev/null +++ b/launcher/net/NetRequest.cpp @@ -0,0 +1,331 @@ +// SPDX-License-Identifier: GPL-3.0-only +/* + * Prism Launcher - Minecraft Launcher + * Copyright (c) 2022 flowln + * Copyright (C) 2022 Sefa Eyeoglu + * Copyright (C) 2023 TheKodeToad + * Copyright (C) 2023 Rachel Powers <508861+Ryex@users.noreply.github.com> + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, version 3. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + * + * This file incorporates work covered by the following copyright and + * permission notice: + * + * Copyright 2013-2021 MultiMC Contributors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#include "NetRequest.h" +#include + +#include +#include +#include + +#if defined(LAUNCHER_APPLICATION) +#include "Application.h" +#endif + +#include "net/NetAction.h" + +#include "MMCTime.h" +#include "StringUtils.h" + +namespace Net { + +void NetRequest::addValidator(Validator* v) +{ + m_sink->addValidator(v); +} + +void NetRequest::executeTask() +{ + init(); + + setStatus(tr("Requesting %1").arg(StringUtils::truncateUrlHumanFriendly(m_url, 80))); + + if (getState() == Task::State::AbortedByUser) { + qCWarning(logCat) << getUid().toString() << "Attempt to start an aborted Request:" << m_url.toString(); + emitAborted(); + return; + } + + QNetworkRequest request(m_url); + m_state = m_sink->init(request); + switch (m_state) { + case State::Succeeded: + qCDebug(logCat) << getUid().toString() << "Request cache hit " << m_url.toString(); + emit succeeded(); + emit finished(); + return; + case State::Running: + qCDebug(logCat) << getUid().toString() << "Runninng " << m_url.toString(); + break; + case State::Inactive: + case State::Failed: + emitFailed(); + return; + case State::AbortedByUser: + emitAborted(); + return; + } + +#if defined(LAUNCHER_APPLICATION) + auto user_agent = APPLICATION->getUserAgent(); +#else + auto user_agent = BuildConfig.USER_AGENT; +#endif + + request.setHeader(QNetworkRequest::UserAgentHeader, user_agent.toUtf8()); + for (auto& header_proxy : m_headerProxies) { + header_proxy->writeHeaders(request); + } + // TODO remove duplication + +#if QT_VERSION >= QT_VERSION_CHECK(5, 15, 0) + request.setTransferTimeout(); +#endif + + m_last_progress_time = m_clock.now(); + m_last_progress_bytes = 0; + + QNetworkReply* rep = getReply(request); + m_reply.reset(rep); + connect(rep, &QNetworkReply::downloadProgress, this, &NetRequest::downloadProgress); + connect(rep, &QNetworkReply::finished, this, &NetRequest::downloadFinished); +#if QT_VERSION >= QT_VERSION_CHECK(5, 15, 0) // QNetworkReply::errorOccurred added in 5.15 + connect(rep, &QNetworkReply::errorOccurred, this, &NetRequest::downloadError); +#else + connect(rep, QOverload::of(&QNetworkReply::error), this, &NetRequest::downloadError); +#endif + connect(rep, &QNetworkReply::sslErrors, this, &NetRequest::sslErrors); + connect(rep, &QNetworkReply::readyRead, this, &NetRequest::downloadReadyRead); +} + +void NetRequest::downloadProgress(qint64 bytesReceived, qint64 bytesTotal) +{ + auto now = m_clock.now(); + auto elapsed = now - m_last_progress_time; + + // use milliseconds for speed precision + auto elapsed_ms = std::chrono::duration_cast(elapsed); + auto bytes_received_since = bytesReceived - m_last_progress_bytes; + auto dl_speed_bps = (double)bytes_received_since / elapsed_ms.count() * 1000; + auto remaining_time_s = (bytesTotal - bytesReceived) / dl_speed_bps; + + //: Current amount of bytes downloaded, out of the total amount of bytes in the download + QString dl_progress = + tr("%1 / %2").arg(StringUtils::humanReadableFileSize(bytesReceived)).arg(StringUtils::humanReadableFileSize(bytesTotal)); + + QString dl_speed_str; + if (elapsed_ms.count() > 0) { + auto str_eta = bytesTotal > 0 ? Time::humanReadableDuration(remaining_time_s) : tr("unknown"); + //: Download speed, in bytes per second (remaining download time in parenthesis) + dl_speed_str = tr("%1 /s (%2)").arg(StringUtils::humanReadableFileSize(dl_speed_bps)).arg(str_eta); + } else { + //: Download speed at 0 bytes per second + dl_speed_str = tr("0 B/s"); + } + + setDetails(dl_progress + "\n" + dl_speed_str); + + setProgress(bytesReceived, bytesTotal); +} + +void NetRequest::downloadError(QNetworkReply::NetworkError error) +{ + if (error == QNetworkReply::OperationCanceledError) { + qCCritical(logCat) << getUid().toString() << "Aborted " << m_url.toString(); + m_state = State::Failed; + } else { + if (m_options & Option::AcceptLocalFiles) { + if (m_sink->hasLocalData()) { + m_state = State::Succeeded; + return; + } + } + // error happened during download. + qCCritical(logCat) << getUid().toString() << "Failed " << m_url.toString() << " with reason " << error; + m_state = State::Failed; + } +} + +void NetRequest::sslErrors(const QList& errors) +{ + int i = 1; + for (auto error : errors) { + qCCritical(logCat) << getUid().toString() << "Request" << m_url.toString() << "SSL Error #" << i << " : " << error.errorString(); + auto cert = error.certificate(); + qCCritical(logCat) << getUid().toString() << "Certificate in question:\n" << cert.toText(); + i++; + } +} + +auto NetRequest::handleRedirect() -> bool +{ + QUrl redirect = m_reply->header(QNetworkRequest::LocationHeader).toUrl(); + if (!redirect.isValid()) { + if (!m_reply->hasRawHeader("Location")) { + // no redirect -> it's fine to continue + return false; + } + // there is a Location header, but it's not correct. we need to apply some workarounds... + QByteArray redirectBA = m_reply->rawHeader("Location"); + if (redirectBA.size() == 0) { + // empty, yet present redirect header? WTF? + return false; + } + QString redirectStr = QString::fromUtf8(redirectBA); + + if (redirectStr.startsWith("//")) { + /* + * IF the URL begins with //, we need to insert the URL scheme. + * See: https://bugreports.qt.io/browse/QTBUG-41061 + * See: http://tools.ietf.org/html/rfc3986#section-4.2 + */ + redirectStr = m_reply->url().scheme() + ":" + redirectStr; + } else if (redirectStr.startsWith("/")) { + /* + * IF the URL begins with /, we need to process it as a relative URL + */ + auto url = m_reply->url(); + url.setPath(redirectStr, QUrl::TolerantMode); + redirectStr = url.toString(); + } + + /* + * Next, make sure the URL is parsed in tolerant mode. Qt doesn't parse the location header in tolerant mode, which causes issues. + * FIXME: report Qt bug for this + */ + redirect = QUrl(redirectStr, QUrl::TolerantMode); + if (!redirect.isValid()) { + qCWarning(logCat) << getUid().toString() << "Failed to parse redirect URL:" << redirectStr; + downloadError(QNetworkReply::ProtocolFailure); + return false; + } + qCDebug(logCat) << getUid().toString() << "Fixed location header:" << redirect; + } else { + qCDebug(logCat) << getUid().toString() << "Location header:" << redirect; + } + + m_url = QUrl(redirect.toString()); + qCDebug(logCat) << getUid().toString() << "Following redirect to " << m_url.toString(); + startAction(m_network); + + return true; +} + +void NetRequest::downloadFinished() +{ + // handle HTTP redirection first + if (handleRedirect()) { + qCDebug(logCat) << getUid().toString() << "Request redirected:" << m_url.toString(); + return; + } + + // if the download failed before this point ... + if (m_state == State::Succeeded) // pretend to succeed so we continue processing :) + { + qCDebug(logCat) << getUid().toString() << "Request failed but we are allowed to proceed:" << m_url.toString(); + m_sink->abort(); + m_reply.reset(); + emit succeeded(); + emit finished(); + return; + } else if (m_state == State::Failed) { + qCDebug(logCat) << getUid().toString() << "Request failed in previous step:" << m_url.toString(); + m_sink->abort(); + m_reply.reset(); + emit failed(""); + emit finished(); + return; + } else if (m_state == State::AbortedByUser) { + qCDebug(logCat) << getUid().toString() << "Request aborted in previous step:" << m_url.toString(); + m_sink->abort(); + m_reply.reset(); + emit aborted(); + emit finished(); + return; + } + + // make sure we got all the remaining data, if any + auto data = m_reply->readAll(); + if (data.size()) { + qCDebug(logCat) << getUid().toString() << "Writing extra" << data.size() << "bytes"; + m_state = m_sink->write(data); + if (m_state != State::Succeeded) { + qCDebug(logCat) << getUid().toString() << "Request failed to write:" << m_url.toString(); + m_sink->abort(); + emit failed(""); + emit finished(); + return; + } + } + + // otherwise, finalize the whole graph + m_state = m_sink->finalize(*m_reply.get()); + if (m_state != State::Succeeded) { + qCDebug(logCat) << getUid().toString() << "Request failed to finalize:" << m_url.toString(); + m_sink->abort(); + m_reply.reset(); + emit failed(""); + emit finished(); + return; + } + + m_reply.reset(); + qCDebug(logCat) << getUid().toString() << "Request succeeded:" << m_url.toString(); + emit succeeded(); + emit finished(); +} + +void NetRequest::downloadReadyRead() +{ + if (m_state == State::Running) { + auto data = m_reply->readAll(); + m_state = m_sink->write(data); + if (m_state == State::Failed) { + qCCritical(logCat) << getUid().toString() << "Failed to process response chunk"; + } + // qDebug() << "Request" << m_url.toString() << "gained" << data.size() << "bytes"; + } else { + qCCritical(logCat) << getUid().toString() << "Cannot write download data! illegal status " << m_status; + } +} + +auto NetRequest::abort() -> bool +{ + m_state = State::AbortedByUser; + if (m_reply) { +#if QT_VERSION >= QT_VERSION_CHECK(5, 15, 0) // QNetworkReply::errorOccurred added in 5.15 + disconnect(m_reply.get(), &QNetworkReply::errorOccurred, nullptr, nullptr); +#else + disconnect(m_reply.get(), QOverload::of(&QNetworkReply::error), nullptr, nullptr); +#endif + m_reply->abort(); + } + return true; +} + +} // namespace Net diff --git a/launcher/net/NetRequest.h b/launcher/net/NetRequest.h new file mode 100644 index 000000000..ee47ab2a6 --- /dev/null +++ b/launcher/net/NetRequest.h @@ -0,0 +1,99 @@ +// SPDX-License-Identifier: GPL-3.0-only +/* + * Prism Launcher - Minecraft Launcher + * Copyright (c) 2022 flowln + * Copyright (C) 2022 Sefa Eyeoglu + * Copyright (C) 2023 Rachel Powers <508861+Ryex@users.noreply.github.com> + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, version 3. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + * + * This file incorporates work covered by the following copyright and + * permission notice: + * + * Copyright 2013-2021 MultiMC Contributors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#pragma once + +#include +#include + +#include "NetAction.h" +#include "Sink.h" +#include "Validator.h" + +#include "QObjectPtr.h" +#include "net/Logging.h" + +namespace Net { +class NetRequest : public NetAction { + Q_OBJECT + protected: + explicit NetRequest() : NetAction() {} + + public: + using Ptr = shared_qobject_ptr; + enum class Option { NoOptions = 0, AcceptLocalFiles = 1, MakeEternal = 2 }; + Q_DECLARE_FLAGS(Options, Option) + + public: + ~NetRequest() override = default; + + void init() override {} + + public: + void addValidator(Validator* v); + auto abort() -> bool override; + auto canAbort() const -> bool override { return true; } + + private: + auto handleRedirect() -> bool; + virtual QNetworkReply* getReply(QNetworkRequest&) = 0; + + protected slots: + void downloadProgress(qint64 bytesReceived, qint64 bytesTotal) override; + void downloadError(QNetworkReply::NetworkError error) override; + void sslErrors(const QList& errors) override; + void downloadFinished() override; + void downloadReadyRead() override; + + public slots: + void executeTask() override; + + protected: + std::unique_ptr m_sink; + Options m_options; + + typedef const QLoggingCategory& (*logCatFunc)(); + logCatFunc logCat = taskUploadLogC; + + std::chrono::steady_clock m_clock; + std::chrono::time_point m_last_progress_time; + qint64 m_last_progress_bytes; +}; +} // namespace Net + +Q_DECLARE_OPERATORS_FOR_FLAGS(Net::NetRequest::Options) diff --git a/launcher/net/RawHeaderProxy.h b/launcher/net/RawHeaderProxy.h new file mode 100644 index 000000000..09b3d4d02 --- /dev/null +++ b/launcher/net/RawHeaderProxy.h @@ -0,0 +1,44 @@ +// SPDX-License-Identifier: GPL-3.0-only +/* + * Prism Launcher - Minecraft Launcher + * Copyright (c) 2022 flowln + * Copyright (C) 2022 Sefa Eyeoglu + * Copyright (C) 2023 Rachel Powers <508861+Ryex@users.noreply.github.com> + * + * 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 "net/HeaderProxy.h" + +namespace Net { + +class RawHeaderProxy : public HeaderProxy { + public: + RawHeaderProxy() : HeaderProxy() {} + virtual ~RawHeaderProxy() = default; + + public: + virtual QList headers(const QNetworkRequest&) const override { return m_headers; }; + + void addHeader(const HeaderPair& header) { m_headers.append(header); } + void addHeader(const QByteArray& headerName, const QByteArray& headerValue) { m_headers.append({ headerName, headerValue }); } + void addHeaders(const QList& headers) { m_headers.append(headers); } + + private: + QList m_headers; +}; + +} // namespace Net diff --git a/launcher/net/Upload.cpp b/launcher/net/Upload.cpp index 3f6f58290..726572e52 100644 --- a/launcher/net/Upload.cpp +++ b/launcher/net/Upload.cpp @@ -38,219 +38,16 @@ #include "Upload.h" +#include #include -#include "Application.h" -#include "BuildConfig.h" #include "ByteArraySink.h" -#include "net/Logging.h" - namespace Net { -bool Upload::abort() +QNetworkReply* Upload::getReply(QNetworkRequest& request) { - if (m_reply) { - m_reply->abort(); - } else { - m_state = State::AbortedByUser; - } - return true; -} - -void Upload::downloadProgress(qint64 bytesReceived, qint64 bytesTotal) -{ - setProgress(bytesReceived, bytesTotal); -} - -void Upload::downloadError(QNetworkReply::NetworkError error) -{ - if (error == QNetworkReply::OperationCanceledError) { - qCCritical(taskUploadLogC) << getUid().toString() << "Aborted " << m_url.toString(); - m_state = State::AbortedByUser; - } else { - // error happened during download. - qCCritical(taskUploadLogC) << getUid().toString() << "Failed " << m_url.toString() << " with reason " << error; - m_state = State::Failed; - } -} - -void Upload::sslErrors(const QList& errors) -{ - int i = 1; - for (const auto& error : errors) { - qCCritical(taskUploadLogC) << getUid().toString() << "Upload" << m_url.toString() << "SSL Error #" << i << " : " - << error.errorString(); - auto cert = error.certificate(); - qCCritical(taskUploadLogC) << getUid().toString() << "Certificate in question:\n" << cert.toText(); - i++; - } -} - -bool Upload::handleRedirect() -{ - QUrl redirect = m_reply->header(QNetworkRequest::LocationHeader).toUrl(); - if (!redirect.isValid()) { - if (!m_reply->hasRawHeader("Location")) { - // no redirect -> it's fine to continue - return false; - } - // there is a Location header, but it's not correct. we need to apply some workarounds... - QByteArray redirectBA = m_reply->rawHeader("Location"); - if (redirectBA.size() == 0) { - // empty, yet present redirect header? WTF? - return false; - } - QString redirectStr = QString::fromUtf8(redirectBA); - - if (redirectStr.startsWith("//")) { - /* - * IF the URL begins with //, we need to insert the URL scheme. - * See: https://bugreports.qt.io/browse/QTBUG-41061 - * See: http://tools.ietf.org/html/rfc3986#section-4.2 - */ - redirectStr = m_reply->url().scheme() + ":" + redirectStr; - } else if (redirectStr.startsWith("/")) { - /* - * IF the URL begins with /, we need to process it as a relative URL - */ - auto url = m_reply->url(); - url.setPath(redirectStr, QUrl::TolerantMode); - redirectStr = url.toString(); - } - - /* - * Next, make sure the URL is parsed in tolerant mode. Qt doesn't parse the location header in tolerant mode, which causes issues. - * FIXME: report Qt bug for this - */ - redirect = QUrl(redirectStr, QUrl::TolerantMode); - if (!redirect.isValid()) { - qCWarning(taskUploadLogC) << getUid().toString() << "Failed to parse redirect URL:" << redirectStr; - downloadError(QNetworkReply::ProtocolFailure); - return false; - } - qCDebug(taskUploadLogC) << getUid().toString() << "Fixed location header:" << redirect; - } else { - qCDebug(taskUploadLogC) << getUid().toString() << "Location header:" << redirect; - } - - m_url = QUrl(redirect.toString()); - qCDebug(taskUploadLogC) << getUid().toString() << "Following redirect to " << m_url.toString(); - startAction(m_network); - return true; -} - -void Upload::downloadFinished() -{ - // handle HTTP redirection first - // very unlikely for post requests, still can happen - if (handleRedirect()) { - qCDebug(taskUploadLogC) << getUid().toString() << "Upload redirected:" << m_url.toString(); - return; - } - - // if the download failed before this point ... - if (m_state == State::Succeeded) { - qCDebug(taskUploadLogC) << getUid().toString() << "Upload failed but we are allowed to proceed:" << m_url.toString(); - m_sink->abort(); - m_reply.reset(); - emit succeeded(); - return; - } else if (m_state == State::Failed) { - qCDebug(taskUploadLogC) << getUid().toString() << "Upload failed in previous step:" << m_url.toString(); - m_sink->abort(); - m_reply.reset(); - emit failed(""); - return; - } else if (m_state == State::AbortedByUser) { - qCDebug(taskUploadLogC) << getUid().toString() << "Upload aborted in previous step:" << m_url.toString(); - m_sink->abort(); - m_reply.reset(); - emit aborted(); - return; - } - - // make sure we got all the remaining data, if any - auto data = m_reply->readAll(); - if (data.size()) { - qCDebug(taskUploadLogC) << getUid().toString() << "Writing extra" << data.size() << "bytes"; - m_state = m_sink->write(data); - } - - // otherwise, finalize the whole graph - m_state = m_sink->finalize(*m_reply.get()); - if (m_state != State::Succeeded) { - qCDebug(taskUploadLogC) << getUid().toString() << "Upload failed to finalize:" << m_url.toString(); - m_sink->abort(); - m_reply.reset(); - emit failed(""); - return; - } - m_reply.reset(); - qCDebug(taskUploadLogC) << getUid().toString() << "Upload succeeded:" << m_url.toString(); - emit succeeded(); -} - -void Upload::downloadReadyRead() -{ - if (m_state == State::Running) { - auto data = m_reply->readAll(); - m_state = m_sink->write(data); - } -} - -void Upload::executeTask() -{ - setStatus(tr("Uploading %1").arg(m_url.toString())); - - if (m_state == State::AbortedByUser) { - qCWarning(taskUploadLogC) << getUid().toString() << "Attempt to start an aborted Upload:" << m_url.toString(); - emit aborted(); - return; - } - QNetworkRequest request(m_url); - m_state = m_sink->init(request); - switch (m_state) { - case State::Succeeded: - emitSucceeded(); - qCDebug(taskUploadLogC) << getUid().toString() << "Upload cache hit " << m_url.toString(); - return; - case State::Running: - qCDebug(taskUploadLogC) << getUid().toString() << "Uploading " << m_url.toString(); - break; - case State::Inactive: - case State::Failed: - emitFailed(""); - return; - case State::AbortedByUser: - emitAborted(); - return; - } - - request.setHeader(QNetworkRequest::UserAgentHeader, APPLICATION->getUserAgent().toUtf8()); - // TODO remove duplication - if (APPLICATION->capabilities() & Application::SupportsFlame && request.url().host() == QUrl(BuildConfig.FLAME_BASE_URL).host()) { - request.setRawHeader("x-api-key", APPLICATION->getFlameAPIKey().toUtf8()); - } else if (request.url().host() == QUrl(BuildConfig.MODRINTH_PROD_URL).host() || - request.url().host() == QUrl(BuildConfig.MODRINTH_STAGING_URL).host()) { - QString token = APPLICATION->getModrinthAPIToken(); - if (!token.isNull()) - request.setRawHeader("Authorization", token.toUtf8()); - } - - // TODO other types of post requests ? request.setHeader(QNetworkRequest::ContentTypeHeader, "application/json"); - QNetworkReply* rep = m_network->post(request, m_post_data); - - m_reply.reset(rep); - connect(rep, &QNetworkReply::downloadProgress, this, &Upload::downloadProgress); - connect(rep, &QNetworkReply::finished, this, &Upload::downloadFinished); -#if QT_VERSION >= QT_VERSION_CHECK(5, 15, 0) // QNetworkReply::errorOccurred added in 5.15 - connect(rep, &QNetworkReply::errorOccurred, this, &Upload::downloadError); -#else - connect(rep, QOverload::of(&QNetworkReply::error), this, &Upload::downloadError); -#endif - connect(rep, &QNetworkReply::sslErrors, this, &Upload::sslErrors); - connect(rep, &QNetworkReply::readyRead, this, &Upload::downloadReadyRead); + return m_network->post(request, m_post_data); } Upload::Ptr Upload::makeByteArray(QUrl url, std::shared_ptr output, QByteArray m_post_data) diff --git a/launcher/net/Upload.h b/launcher/net/Upload.h index 0b0c94976..f920e5561 100644 --- a/launcher/net/Upload.h +++ b/launcher/net/Upload.h @@ -37,36 +37,21 @@ #pragma once -#include "NetAction.h" -#include "Sink.h" +#include "net/NetRequest.h" namespace Net { -class Upload : public NetAction { +class Upload : public NetRequest { Q_OBJECT - public: using Ptr = shared_qobject_ptr; + explicit Upload() : NetRequest() { logCat = taskUploadLogC; }; static Upload::Ptr makeByteArray(QUrl url, std::shared_ptr output, QByteArray m_post_data); - auto abort() -> bool override; - auto canAbort() const -> bool override { return true; }; - protected slots: - void downloadProgress(qint64 bytesReceived, qint64 bytesTotal) override; - void downloadError(QNetworkReply::NetworkError error) override; - void sslErrors(const QList& errors) override; - void downloadFinished() override; - void downloadReadyRead() override; - - public slots: - void executeTask() override; - - private: - std::unique_ptr m_sink; + protected: + virtual QNetworkReply* getReply(QNetworkRequest&) override; QByteArray m_post_data; - - bool handleRedirect(); }; } // namespace Net diff --git a/launcher/net/Validator.h b/launcher/net/Validator.h index 33c4cfee1..92ac6ea15 100644 --- a/launcher/net/Validator.h +++ b/launcher/net/Validator.h @@ -39,8 +39,8 @@ namespace Net { class Validator { public: /* con/des */ - Validator(){}; - virtual ~Validator(){}; + Validator() {} + virtual ~Validator() {} public: /* methods */ virtual bool init(QNetworkRequest& request) = 0; diff --git a/launcher/news/NewsEntry.cpp b/launcher/news/NewsEntry.cpp index cf6d3e46b..ea25f2e54 100644 --- a/launcher/news/NewsEntry.cpp +++ b/launcher/news/NewsEntry.cpp @@ -39,14 +39,14 @@ inline QString childValue(const QDomElement& element, const QString& childName, { QDomNodeList nodes = element.elementsByTagName(childName); if (nodes.count() > 0) { - QDomElement element = nodes.at(0).toElement(); - return element.text(); + QDomElement elem = nodes.at(0).toElement(); + return elem.text(); } else { return defaultVal; } } -bool NewsEntry::fromXmlElement(const QDomElement& element, NewsEntry* entry, QString* errorMsg) +bool NewsEntry::fromXmlElement(const QDomElement& element, NewsEntry* entry, [[maybe_unused]] QString* errorMsg) { QString title = childValue(element, "title", tr("Untitled")); QString content = childValue(element, "content", tr("No content.")); diff --git a/launcher/pathmatcher/IPathMatcher.h b/launcher/pathmatcher/IPathMatcher.h index 8121de5cb..cd6121979 100644 --- a/launcher/pathmatcher/IPathMatcher.h +++ b/launcher/pathmatcher/IPathMatcher.h @@ -7,6 +7,6 @@ class IPathMatcher { typedef std::shared_ptr Ptr; public: - virtual ~IPathMatcher(){}; + virtual ~IPathMatcher() {} virtual bool matches(const QString& string) const = 0; }; diff --git a/launcher/pathmatcher/RegexpMatcher.h b/launcher/pathmatcher/RegexpMatcher.h index fe4aee6a7..a6a3e616d 100644 --- a/launcher/pathmatcher/RegexpMatcher.h +++ b/launcher/pathmatcher/RegexpMatcher.h @@ -3,7 +3,7 @@ class RegexpMatcher : public IPathMatcher { public: - virtual ~RegexpMatcher(){}; + virtual ~RegexpMatcher() {} RegexpMatcher(const QString& regexp) { m_regexp.setPattern(regexp); diff --git a/launcher/resources/multimc/128x128/instances/forge.png b/launcher/resources/multimc/128x128/instances/forge.png new file mode 100644 index 000000000..d8ff79a53 Binary files /dev/null and b/launcher/resources/multimc/128x128/instances/forge.png differ diff --git a/launcher/resources/multimc/128x128/instances/liteloader.png b/launcher/resources/multimc/128x128/instances/liteloader.png new file mode 100644 index 000000000..646217de0 Binary files /dev/null and b/launcher/resources/multimc/128x128/instances/liteloader.png differ diff --git a/launcher/resources/multimc/index.theme b/launcher/resources/multimc/index.theme index 070e23f10..4da8072d9 100644 --- a/launcher/resources/multimc/index.theme +++ b/launcher/resources/multimc/index.theme @@ -1,5 +1,5 @@ [Icon Theme] -Name=multimc +Name=Legacy Comment=Default Icons Inherits=default Directories=8x8,16x16,22x22,24x24,32x32,32x32/instances,48x48,50x50/instances,64x64,128x128/instances,256x256,scalable,scalable/instances diff --git a/launcher/resources/multimc/multimc.qrc b/launcher/resources/multimc/multimc.qrc index 2c00f28fa..80981559b 100644 --- a/launcher/resources/multimc/multimc.qrc +++ b/launcher/resources/multimc/multimc.qrc @@ -347,5 +347,11 @@ scalable/export.svg scalable/launch.svg scalable/server.svg + + scalable/instances/quiltmc.svg + scalable/instances/fabricmc.svg + scalable/instances/neoforged.svg + 128x128/instances/forge.png + 128x128/instances/liteloader.png diff --git a/launcher/resources/multimc/scalable/instances/fabricmc.svg b/launcher/resources/multimc/scalable/instances/fabricmc.svg new file mode 100644 index 000000000..7bfc75487 --- /dev/null +++ b/launcher/resources/multimc/scalable/instances/fabricmc.svg @@ -0,0 +1,71 @@ + + + + diff --git a/launcher/resources/multimc/scalable/instances/neoforged.svg b/launcher/resources/multimc/scalable/instances/neoforged.svg new file mode 100644 index 000000000..706d53a0e --- /dev/null +++ b/launcher/resources/multimc/scalable/instances/neoforged.svg @@ -0,0 +1,3 @@ + + +Sefa Eyeoglu <contact@scrumplex.net> diff --git a/launcher/resources/multimc/scalable/instances/quiltmc.svg b/launcher/resources/multimc/scalable/instances/quiltmc.svg new file mode 100644 index 000000000..a7aaca53f --- /dev/null +++ b/launcher/resources/multimc/scalable/instances/quiltmc.svg @@ -0,0 +1,98 @@ + + + + + + + + + + + + + + + + + diff --git a/launcher/resources/pe_blue/index.theme b/launcher/resources/pe_blue/index.theme index c9e0d93ad..6d842b5d2 100644 --- a/launcher/resources/pe_blue/index.theme +++ b/launcher/resources/pe_blue/index.theme @@ -1,5 +1,5 @@ [Icon Theme] -Name=pe_blue +Name=Simple (Blue) Comment=Icons by pexner (blue) Inherits=multimc Directories=scalable diff --git a/launcher/resources/pe_colored/index.theme b/launcher/resources/pe_colored/index.theme index b757bbd79..bca5494f1 100644 --- a/launcher/resources/pe_colored/index.theme +++ b/launcher/resources/pe_colored/index.theme @@ -1,5 +1,5 @@ [Icon Theme] -Name=pe_colored +Name=Simple (Colored) Comment=Icons by pexner (colored) Inherits=multimc Directories=scalable diff --git a/launcher/resources/pe_dark/index.theme b/launcher/resources/pe_dark/index.theme index b7d1ad011..4cfbf09c4 100644 --- a/launcher/resources/pe_dark/index.theme +++ b/launcher/resources/pe_dark/index.theme @@ -1,5 +1,5 @@ [Icon Theme] -Name=pe_dark +Name=Simple (Dark) Comment=Icons by pexner (dark) Inherits=multimc Directories=scalable diff --git a/launcher/resources/pe_light/index.theme b/launcher/resources/pe_light/index.theme index c106acc8b..87b76d139 100644 --- a/launcher/resources/pe_light/index.theme +++ b/launcher/resources/pe_light/index.theme @@ -1,5 +1,5 @@ [Icon Theme] -Name=pe_light +Name=Simple (Light) Comment=Icons by pexner (light) Inherits=multimc Directories=scalable diff --git a/launcher/screenshots/ImgurAlbumCreation.cpp b/launcher/screenshots/ImgurAlbumCreation.cpp index d598d77e2..d548c89ae 100644 --- a/launcher/screenshots/ImgurAlbumCreation.cpp +++ b/launcher/screenshots/ImgurAlbumCreation.cpp @@ -81,7 +81,7 @@ void ImgurAlbumCreation::executeTask() connect(rep, &QNetworkReply::sslErrors, this, &ImgurAlbumCreation::sslErrors); } -void ImgurAlbumCreation::downloadError(QNetworkReply::NetworkError error) +void ImgurAlbumCreation::downloadError([[maybe_unused]] QNetworkReply::NetworkError error) { qDebug() << m_reply->errorString(); m_state = State::Failed; diff --git a/launcher/screenshots/ImgurAlbumCreation.h b/launcher/screenshots/ImgurAlbumCreation.h index 3a82a956a..bb71bec2b 100644 --- a/launcher/screenshots/ImgurAlbumCreation.h +++ b/launcher/screenshots/ImgurAlbumCreation.h @@ -50,6 +50,8 @@ class ImgurAlbumCreation : public NetAction { QString deleteHash() const { return m_deleteHash; } QString id() const { return m_id; } + void init() override{}; + protected slots: void downloadProgress(qint64 bytesReceived, qint64 bytesTotal) override; void downloadError(QNetworkReply::NetworkError error) override; diff --git a/launcher/screenshots/ImgurUpload.cpp b/launcher/screenshots/ImgurUpload.cpp index 706af7ba5..29499a20d 100644 --- a/launcher/screenshots/ImgurUpload.cpp +++ b/launcher/screenshots/ImgurUpload.cpp @@ -96,7 +96,7 @@ void ImgurUpload::executeTask() connect(rep, &QNetworkReply::sslErrors, this, &ImgurUpload::sslErrors); } -void ImgurUpload::downloadError(QNetworkReply::NetworkError error) +void ImgurUpload::downloadError([[maybe_unused]] QNetworkReply::NetworkError error) { qCritical() << "ImgurUpload failed with error" << m_reply->errorString() << "Server reply:\n" << m_reply->readAll(); if (finished) { diff --git a/launcher/screenshots/ImgurUpload.h b/launcher/screenshots/ImgurUpload.h index 8714e0c04..542255b2d 100644 --- a/launcher/screenshots/ImgurUpload.h +++ b/launcher/screenshots/ImgurUpload.h @@ -44,6 +44,7 @@ class ImgurUpload : public NetAction { explicit ImgurUpload(ScreenShot::Ptr shot); static Ptr make(ScreenShot::Ptr shot) { return Ptr(new ImgurUpload(shot)); } + void init() override{}; protected slots: void downloadProgress(qint64 bytesReceived, qint64 bytesTotal) override; diff --git a/launcher/tasks/ConcurrentTask.cpp b/launcher/tasks/ConcurrentTask.cpp index 884923466..6cfd864cc 100644 --- a/launcher/tasks/ConcurrentTask.cpp +++ b/launcher/tasks/ConcurrentTask.cpp @@ -88,6 +88,7 @@ bool ConcurrentTask::abort() QMutableHashIterator doing_iter(m_doing); while (doing_iter.hasNext()) { auto task = doing_iter.next(); + disconnect(task->get(), &Task::aborted, this, 0); suceedeed &= (task.value())->abort(); } @@ -130,6 +131,7 @@ void ConcurrentTask::startNext() connect(next.get(), &Task::succeeded, this, [this, next]() { subTaskSucceeded(next); }); connect(next.get(), &Task::failed, this, [this, next](QString msg) { subTaskFailed(next, msg); }); + connect(next.get(), &Task::aborted, this, [this, next] { subTaskFailed(next, "Aborted"); }); connect(next.get(), &Task::status, this, [this, next](QString msg) { subTaskStatus(next, msg); }); connect(next.get(), &Task::details, this, [this, next](QString msg) { subTaskDetails(next, msg); }); @@ -171,7 +173,7 @@ void ConcurrentTask::subTaskSucceeded(Task::Ptr task) startNext(); } -void ConcurrentTask::subTaskFailed(Task::Ptr task, const QString& msg) +void ConcurrentTask::subTaskFailed(Task::Ptr task, [[maybe_unused]] const QString& msg) { m_done.insert(task.get(), task); m_failed.insert(task.get(), task); diff --git a/launcher/tasks/ConcurrentTask.h b/launcher/tasks/ConcurrentTask.h index 6325fc9e7..8b696bf58 100644 --- a/launcher/tasks/ConcurrentTask.h +++ b/launcher/tasks/ConcurrentTask.h @@ -53,7 +53,7 @@ class ConcurrentTask : public Task { bool canAbort() const override { return true; } - inline auto isMultiStep() const -> bool override { return totalSize() > 1; }; + inline auto isMultiStep() const -> bool override { return totalSize() > 1; } auto getStepProgress() const -> TaskStepProgressList override; void addTask(Task::Ptr task); @@ -80,7 +80,7 @@ class ConcurrentTask : public Task { protected: // NOTE: This is not thread-safe. - [[nodiscard]] unsigned int totalSize() const { return m_queue.size() + m_doing.size() + m_done.size(); } + [[nodiscard]] unsigned int totalSize() const { return static_cast(m_queue.size() + m_doing.size() + m_done.size()); } enum class Operation { ADDED, REMOVED, CHANGED }; void updateStepProgress(TaskStepProgress const& changed_progress, Operation); diff --git a/launcher/tasks/Task.h b/launcher/tasks/Task.h index 7e1defd80..94b4089f3 100644 --- a/launcher/tasks/Task.h +++ b/launcher/tasks/Task.h @@ -59,16 +59,18 @@ struct TaskStepProgress { QString status = ""; QString details = ""; TaskStepState state = TaskStepState::Waiting; + TaskStepProgress() { this->uid = QUuid::createUuid(); } - TaskStepProgress(QUuid uid) { this->uid = uid; } + TaskStepProgress(QUuid uid_) : uid(uid_) {} + bool isDone() const { return (state == TaskStepState::Failed) || (state == TaskStepState::Succeeded); } - void update(qint64 current, qint64 total) + void update(qint64 new_current, qint64 new_total) { this->old_current = this->current; this->old_total = this->total; - this->current = current; - this->total = total; + this->current = new_current; + this->total = new_total; this->state = TaskStepState::Running; } }; @@ -150,7 +152,7 @@ class Task : public QObject, public QRunnable { if (canAbort()) emitAborted(); return canAbort(); - }; + } void setAbortable(bool can_abort) { diff --git a/launcher/tools/JProfiler.cpp b/launcher/tools/JProfiler.cpp index 3fa9e94e4..7a532a3d2 100644 --- a/launcher/tools/JProfiler.cpp +++ b/launcher/tools/JProfiler.cpp @@ -29,7 +29,7 @@ void JProfiler::profilerStarted() emit readyToLaunch(tr("Listening on port: %1").arg(listeningPort)); } -void JProfiler::profilerFinished(int exit, QProcess::ExitStatus status) +void JProfiler::profilerFinished([[maybe_unused]] int exit, QProcess::ExitStatus status) { if (status == QProcess::CrashExit) { emit abortLaunch(tr("Profiler aborted")); diff --git a/launcher/tools/JVisualVM.cpp b/launcher/tools/JVisualVM.cpp index 5f2762ebd..4da4e1e54 100644 --- a/launcher/tools/JVisualVM.cpp +++ b/launcher/tools/JVisualVM.cpp @@ -27,7 +27,7 @@ void JVisualVM::profilerStarted() emit readyToLaunch(tr("JVisualVM started")); } -void JVisualVM::profilerFinished(int exit, QProcess::ExitStatus status) +void JVisualVM::profilerFinished([[maybe_unused]] int exit, QProcess::ExitStatus status) { if (status == QProcess::CrashExit) { emit abortLaunch(tr("Profiler aborted")); diff --git a/launcher/translations/POTranslator.cpp b/launcher/translations/POTranslator.cpp index 71d7878df..51ef4852b 100644 --- a/launcher/translations/POTranslator.cpp +++ b/launcher/translations/POTranslator.cpp @@ -275,7 +275,7 @@ POTranslator::~POTranslator() delete d; } -QString POTranslator::translate(const char* context, const char* sourceText, const char* disambiguation, int n) const +QString POTranslator::translate(const char* context, const char* sourceText, const char* disambiguation, [[maybe_unused]] int n) const { if (disambiguation) { auto disambiguationKey = QByteArray(context) + "|" + QByteArray(sourceText) + "@" + QByteArray(disambiguation); diff --git a/launcher/translations/TranslationsModel.cpp b/launcher/translations/TranslationsModel.cpp index 06d5bfd86..933fe2d35 100644 --- a/launcher/translations/TranslationsModel.cpp +++ b/launcher/translations/TranslationsModel.cpp @@ -228,12 +228,11 @@ void readIndex(const QString& path, QMap& languages) QByteArray data; try { data = FS::read(path); - } catch (const Exception& e) { + } catch ([[maybe_unused]] const Exception& e) { qCritical() << "Translations Download Failed: index file not readable"; return; } - int index = 1; try { auto toplevel_doc = Json::requireDocument(data); auto doc = Json::requireObject(toplevel_doc); @@ -259,9 +258,8 @@ void readIndex(const QString& path, QMap& languages) lang.file_size = Json::requireInteger(langObj, "size"); languages.insert(lang.key, lang); - index++; } - } catch (Json::JsonException& e) { + } catch ([[maybe_unused]] Json::JsonException& e) { qCritical() << "Translations Download Failed: index file could not be parsed as json"; } } @@ -409,12 +407,12 @@ QVariant TranslationsModel::headerData(int section, Qt::Orientation orientation, return QAbstractListModel::headerData(section, orientation, role); } -int TranslationsModel::rowCount(const QModelIndex& parent) const +int TranslationsModel::rowCount([[maybe_unused]] const QModelIndex& parent) const { return d->m_languages.size(); } -int TranslationsModel::columnCount(const QModelIndex& parent) const +int TranslationsModel::columnCount([[maybe_unused]] const QModelIndex& parent) const { return 2; } diff --git a/launcher/ui/InstanceWindow.cpp b/launcher/ui/InstanceWindow.cpp index 9c7886fac..bf83a56c9 100644 --- a/launcher/ui/InstanceWindow.cpp +++ b/launcher/ui/InstanceWindow.cpp @@ -2,6 +2,7 @@ /* * Prism Launcher - Minecraft Launcher * Copyright (C) 2022 Sefa Eyeoglu + * Copyright (C) 2023 TheKodeToad * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by @@ -43,8 +44,6 @@ #include #include -#include "ui/dialogs/CustomMessageBox.h" -#include "ui/dialogs/ProgressDialog.h" #include "ui/widgets/PageContainer.h" #include "InstancePageProvider.h" @@ -75,40 +74,44 @@ InstanceWindow::InstanceWindow(InstancePtr instance, QWidget* parent) : QMainWin // Add custom buttons to the page container layout. { - auto horizontalLayout = new QHBoxLayout(); + auto horizontalLayout = new QHBoxLayout(this); horizontalLayout->setObjectName(QStringLiteral("horizontalLayout")); horizontalLayout->setContentsMargins(6, -1, 6, -1); - auto btnHelp = new QPushButton(); + auto btnHelp = new QPushButton(this); btnHelp->setText(tr("Help")); horizontalLayout->addWidget(btnHelp); - connect(btnHelp, SIGNAL(clicked(bool)), m_container, SLOT(help())); + connect(btnHelp, &QPushButton::clicked, m_container, &PageContainer::help); auto spacer = new QSpacerItem(40, 20, QSizePolicy::Expanding, QSizePolicy::Minimum); horizontalLayout->addSpacerItem(spacer); - m_killButton = new QPushButton(); + m_launchButton = new QToolButton(this); + m_launchButton->setText(tr("&Launch")); + m_launchButton->setToolTip(tr("Launch the instance")); + m_launchButton->setPopupMode(QToolButton::MenuButtonPopup); + m_launchButton->setMinimumWidth(80); // HACK!! + horizontalLayout->addWidget(m_launchButton); + connect(m_launchButton, &QPushButton::clicked, this, [this] { APPLICATION->launch(m_instance); }); + + m_killButton = new QPushButton(this); + m_killButton->setText(tr("&Kill")); + m_killButton->setToolTip(tr("Kill the running instance")); + m_killButton->setShortcut(QKeySequence(tr("Ctrl+K"))); horizontalLayout->addWidget(m_killButton); - connect(m_killButton, SIGNAL(clicked(bool)), SLOT(on_btnKillMinecraft_clicked())); + connect(m_killButton, &QPushButton::clicked, this, [this] { APPLICATION->kill(m_instance); }); - m_launchOfflineButton = new QPushButton(); - horizontalLayout->addWidget(m_launchOfflineButton); - m_launchOfflineButton->setText(tr("Launch Offline")); + updateButtons(); - m_launchDemoButton = new QPushButton(); - horizontalLayout->addWidget(m_launchDemoButton); - m_launchDemoButton->setText(tr("Launch Demo")); - - updateLaunchButtons(); - connect(m_launchOfflineButton, SIGNAL(clicked(bool)), SLOT(on_btnLaunchMinecraftOffline_clicked())); - connect(m_launchDemoButton, SIGNAL(clicked(bool)), SLOT(on_btnLaunchMinecraftDemo_clicked())); - - m_closeButton = new QPushButton(); + m_closeButton = new QPushButton(this); m_closeButton->setText(tr("Close")); horizontalLayout->addWidget(m_closeButton); - connect(m_closeButton, SIGNAL(clicked(bool)), SLOT(on_closeButton_clicked())); + connect(m_closeButton, &QPushButton::clicked, this, &QMainWindow::close); m_container->addButtons(horizontalLayout); + + connect(m_instance.get(), &BaseInstance::profilerChanged, this, &InstanceWindow::updateButtons); + connect(APPLICATION, &Application::globalSettingsClosed, this, &InstanceWindow::updateButtons); } // restore window state @@ -148,47 +151,18 @@ void InstanceWindow::on_instanceStatusChanged(BaseInstance::Status, BaseInstance } } -void InstanceWindow::updateLaunchButtons() +void InstanceWindow::updateButtons() { - if (m_instance->isRunning()) { - m_launchOfflineButton->setEnabled(false); - m_launchDemoButton->setEnabled(false); - m_killButton->setText(tr("Kill")); - m_killButton->setObjectName("killButton"); - m_killButton->setToolTip(tr("Kill the running instance")); - } else if (!m_instance->canLaunch()) { - m_launchOfflineButton->setEnabled(false); - m_launchDemoButton->setEnabled(false); - m_killButton->setText(tr("Launch")); - m_killButton->setObjectName("launchButton"); - m_killButton->setToolTip(tr("Launch the instance")); - m_killButton->setEnabled(false); - } else { - m_launchOfflineButton->setEnabled(true); + m_launchButton->setEnabled(m_instance->canLaunch()); + m_killButton->setEnabled(m_instance->isRunning()); - // Disable demo-mode if not available. - auto instance = dynamic_cast(m_instance.get()); - if (instance) { - m_launchDemoButton->setEnabled(instance->supportsDemo()); - } - - m_killButton->setText(tr("Launch")); - m_killButton->setObjectName("launchButton"); - m_killButton->setToolTip(tr("Launch the instance")); - } - // NOTE: this is a hack to force the button to recalculate its style - m_killButton->setStyleSheet("/* */"); - m_killButton->setStyleSheet(QString()); -} - -void InstanceWindow::on_btnLaunchMinecraftOffline_clicked() -{ - APPLICATION->launch(m_instance, false, false, nullptr); -} - -void InstanceWindow::on_btnLaunchMinecraftDemo_clicked() -{ - APPLICATION->launch(m_instance, false, true, nullptr); + QMenu* launchMenu = m_launchButton->menu(); + if (launchMenu) + launchMenu->clear(); + else + launchMenu = new QMenu(this); + m_instance->populateLaunchMenu(launchMenu); + m_launchButton->setMenu(launchMenu); } void InstanceWindow::instanceLaunchTaskChanged(shared_qobject_ptr proc) @@ -198,18 +172,13 @@ void InstanceWindow::instanceLaunchTaskChanged(shared_qobject_ptr pr void InstanceWindow::runningStateChanged(bool running) { - updateLaunchButtons(); + updateButtons(); m_container->refreshContainer(); if (running) { selectPage("log"); } } -void InstanceWindow::on_closeButton_clicked() -{ - close(); -} - void InstanceWindow::closeEvent(QCloseEvent* event) { bool proceed = true; @@ -232,15 +201,6 @@ bool InstanceWindow::saveAll() return m_container->saveAll(); } -void InstanceWindow::on_btnKillMinecraft_clicked() -{ - if (m_instance->isRunning()) { - APPLICATION->kill(m_instance); - } else { - APPLICATION->launch(m_instance, true, false, nullptr); - } -} - QString InstanceWindow::instanceId() { return m_instance->id(); @@ -256,7 +216,10 @@ void InstanceWindow::refreshContainer() m_container->refreshContainer(); } -InstanceWindow::~InstanceWindow() {} +BasePage* InstanceWindow::selectedPage() const +{ + return m_container->selectedPage(); +} bool InstanceWindow::requestClose() { diff --git a/launcher/ui/InstanceWindow.h b/launcher/ui/InstanceWindow.h index 508bcaa14..e5bc24d44 100644 --- a/launcher/ui/InstanceWindow.h +++ b/launcher/ui/InstanceWindow.h @@ -2,6 +2,7 @@ /* * Prism Launcher - Minecraft Launcher * Copyright (C) 2022 Sefa Eyeoglu + * Copyright (C) 2023 TheKodeToad * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by @@ -37,6 +38,7 @@ #include #include +#include #include "LaunchController.h" #include "launch/LaunchTask.h" @@ -52,9 +54,10 @@ class InstanceWindow : public QMainWindow, public BasePageContainer { public: explicit InstanceWindow(InstancePtr proc, QWidget* parent = 0); - virtual ~InstanceWindow(); + virtual ~InstanceWindow() = default; bool selectPage(QString pageId) override; + BasePage* selectedPage() const override; void refreshContainer() override; QString instanceId(); @@ -69,11 +72,6 @@ class InstanceWindow : public QMainWindow, public BasePageContainer { void isClosing(); private slots: - void on_closeButton_clicked(); - void on_btnKillMinecraft_clicked(); - void on_btnLaunchMinecraftOffline_clicked(); - void on_btnLaunchMinecraftDemo_clicked(); - void instanceLaunchTaskChanged(shared_qobject_ptr proc); void runningStateChanged(bool running); void on_instanceStatusChanged(BaseInstance::Status, BaseInstance::Status newStatus); @@ -82,7 +80,7 @@ class InstanceWindow : public QMainWindow, public BasePageContainer { void closeEvent(QCloseEvent*) override; private: - void updateLaunchButtons(); + void updateButtons(); private: shared_qobject_ptr m_proc; @@ -90,7 +88,6 @@ class InstanceWindow : public QMainWindow, public BasePageContainer { bool m_doNotSave = false; PageContainer* m_container = nullptr; QPushButton* m_closeButton = nullptr; + QToolButton* m_launchButton = nullptr; QPushButton* m_killButton = nullptr; - QPushButton* m_launchOfflineButton = nullptr; - QPushButton* m_launchDemoButton = nullptr; }; diff --git a/launcher/ui/MainWindow.cpp b/launcher/ui/MainWindow.cpp index e342e833e..5e55a5abb 100644 --- a/launcher/ui/MainWindow.cpp +++ b/launcher/ui/MainWindow.cpp @@ -43,7 +43,6 @@ #include "FileSystem.h" #include "MainWindow.h" -#include "ui/dialogs/ExportToModListDialog.h" #include "ui_MainWindow.h" #include @@ -85,22 +84,19 @@ #include #include #include -#include +#include #include #include #include #include -#include "InstancePageProvider.h" #include "InstanceWindow.h" -#include "JavaCommon.h" -#include "LaunchController.h" #include "ui/dialogs/AboutDialog.h" #include "ui/dialogs/CopyInstanceDialog.h" #include "ui/dialogs/CustomMessageBox.h" -#include "ui/dialogs/EditAccountDialog.h" #include "ui/dialogs/ExportInstanceDialog.h" #include "ui/dialogs/ExportPackDialog.h" +#include "ui/dialogs/ExportToModListDialog.h" #include "ui/dialogs/IconPickerDialog.h" #include "ui/dialogs/ImportResourceDialog.h" #include "ui/dialogs/NewInstanceDialog.h" @@ -113,15 +109,24 @@ #include "ui/themes/ThemeManager.h" #include "ui/widgets/LabeledToolButton.h" +#include "minecraft/PackProfile.h" +#include "minecraft/VersionFile.h" #include "minecraft/WorldList.h" #include "minecraft/mod/ModFolderModel.h" +#include "minecraft/mod/ResourcePackFolderModel.h" #include "minecraft/mod/ShaderPackFolderModel.h" +#include "minecraft/mod/TexturePackFolderModel.h" #include "minecraft/mod/tasks/LocalResourceParse.h" +#include "modplatform/ModIndex.h" +#include "modplatform/flame/FlameAPI.h" +#include "modplatform/flame/FlameModIndex.h" + #include "KonamiCode.h" #include "InstanceCopyTask.h" -#include "InstanceImportTask.h" + +#include "Json.h" #include "MMCTime.h" @@ -515,9 +520,9 @@ void MainWindow::showInstanceContextMenu(const QPoint& pos) QAction* actionCreateInstance = new QAction(tr("Create instance"), this); actionCreateInstance->setToolTip(ui->actionAddInstance->toolTip()); if (!group.isNull()) { - QVariantMap data; - data["group"] = group; - actionCreateInstance->setData(data); + QVariantMap instance_action_data; + instance_action_data["group"] = group; + actionCreateInstance->setData(instance_action_data); } connect(actionCreateInstance, SIGNAL(triggered(bool)), SLOT(on_actionAddInstance_triggered())); @@ -527,9 +532,9 @@ void MainWindow::showInstanceContextMenu(const QPoint& pos) actions.append(actionCreateInstance); if (!group.isNull()) { QAction* actionDeleteGroup = new QAction(tr("Delete group '%1'").arg(group), this); - QVariantMap data; - data["group"] = group; - actionDeleteGroup->setData(data); + QVariantMap delete_group_action_data; + delete_group_action_data["group"] = group; + actionDeleteGroup->setData(delete_group_action_data); connect(actionDeleteGroup, SIGNAL(triggered(bool)), SLOT(deleteGroup())); actions.append(actionDeleteGroup); } @@ -549,71 +554,15 @@ void MainWindow::updateMainToolBar() ui->mainToolBar->setVisible(ui->menuBar->isNativeMenuBar() || !APPLICATION->settings()->get("MenuBarInsteadOfToolBar").toBool()); } -void MainWindow::updateToolsMenu() +void MainWindow::updateLaunchButton() { - bool currentInstanceRunning = m_selectedInstance && m_selectedInstance->isRunning(); - - ui->actionLaunchInstance->setDisabled(!m_selectedInstance || currentInstanceRunning); - ui->actionLaunchInstanceOffline->setDisabled(!m_selectedInstance || currentInstanceRunning); - ui->actionLaunchInstanceDemo->setDisabled(!m_selectedInstance || currentInstanceRunning); - QMenu* launchMenu = ui->actionLaunchInstance->menu(); - if (launchMenu) { + if (launchMenu) launchMenu->clear(); - } else { + else launchMenu = new QMenu(this); - } - QAction* normalLaunch = launchMenu->addAction(tr("Launch")); - normalLaunch->setShortcut(QKeySequence::Open); - QAction* normalLaunchOffline = launchMenu->addAction(tr("Launch Offline")); - normalLaunchOffline->setShortcut(QKeySequence(tr("Ctrl+Shift+O"))); - QAction* normalLaunchDemo = launchMenu->addAction(tr("Launch Demo")); - normalLaunchDemo->setShortcut(QKeySequence(tr("Ctrl+Alt+O"))); - if (m_selectedInstance) { - normalLaunch->setEnabled(m_selectedInstance->canLaunch()); - normalLaunchOffline->setEnabled(m_selectedInstance->canLaunch()); - normalLaunchDemo->setEnabled(m_selectedInstance->canLaunch()); - - connect(normalLaunch, &QAction::triggered, [this]() { APPLICATION->launch(m_selectedInstance, true, false); }); - connect(normalLaunchOffline, &QAction::triggered, [this]() { APPLICATION->launch(m_selectedInstance, false, false); }); - connect(normalLaunchDemo, &QAction::triggered, [this]() { APPLICATION->launch(m_selectedInstance, false, true); }); - } else { - normalLaunch->setDisabled(true); - normalLaunchOffline->setDisabled(true); - normalLaunchDemo->setDisabled(true); - } - - // Disable demo-mode if not available. - auto instance = dynamic_cast(m_selectedInstance.get()); - if (instance) { - normalLaunchDemo->setEnabled(instance->supportsDemo()); - } - - QString profilersTitle = tr("Profilers"); - launchMenu->addSeparator()->setText(profilersTitle); - for (auto profiler : APPLICATION->profilers().values()) { - QAction* profilerAction = launchMenu->addAction(profiler->name()); - QAction* profilerOfflineAction = launchMenu->addAction(tr("%1 Offline").arg(profiler->name())); - QString error; - if (!profiler->check(&error)) { - profilerAction->setDisabled(true); - profilerOfflineAction->setDisabled(true); - QString profilerToolTip = tr("Profiler not setup correctly. Go into settings, \"External Tools\"."); - profilerAction->setToolTip(profilerToolTip); - profilerOfflineAction->setToolTip(profilerToolTip); - } else if (m_selectedInstance) { - profilerAction->setEnabled(m_selectedInstance->canLaunch()); - profilerOfflineAction->setEnabled(m_selectedInstance->canLaunch()); - - connect(profilerAction, &QAction::triggered, - [this, profiler]() { APPLICATION->launch(m_selectedInstance, true, false, profiler.get()); }); - connect(profilerOfflineAction, &QAction::triggered, - [this, profiler]() { APPLICATION->launch(m_selectedInstance, false, false, profiler.get()); }); - } else { - profilerAction->setDisabled(true); - profilerOfflineAction->setDisabled(true); - } - } + if (m_selectedInstance) + m_selectedInstance->populateLaunchMenu(launchMenu); ui->actionLaunchInstance->setMenu(launchMenu); } @@ -627,7 +576,7 @@ void MainWindow::updateThemeMenu() themeMenu = new QMenu(this); } - auto themes = APPLICATION->getValidApplicationThemes(); + auto themes = APPLICATION->themeManager()->getValidApplicationThemes(); QActionGroup* themesGroup = new QActionGroup(this); @@ -641,7 +590,7 @@ void MainWindow::updateThemeMenu() themeAction->setActionGroup(themesGroup); connect(themeAction, &QAction::triggered, [theme]() { - APPLICATION->setApplicationTheme(theme->id()); + APPLICATION->themeManager()->setApplicationTheme(theme->id()); APPLICATION->settings()->set("ApplicationTheme", theme->id()); }); } @@ -744,9 +693,9 @@ void MainWindow::changeActiveAccount() if (sAction->data().type() != QVariant::Type::Int) return; - QVariant data = sAction->data(); + QVariant action_data = sAction->data(); bool valid = false; - int index = data.toInt(&valid); + int index = action_data.toInt(&valid); if (!valid) { index = -1; } @@ -923,13 +872,13 @@ void MainWindow::finalizeInstance(InstancePtr inst) } else { CustomMessageBox::selectable(this, tr("Error"), tr("The launcher cannot download Minecraft or update instances unless you have at least " - "one account added.\nPlease add your Mojang or Minecraft account."), + "one account added.\nPlease add your Microsoft or Mojang account."), QMessageBox::Warning) ->show(); } } -void MainWindow::addInstance(QString url) +void MainWindow::addInstance(const QString& url, const QMap& extra_info) { QString groupName; do { @@ -949,7 +898,7 @@ void MainWindow::addInstance(QString url) groupName = APPLICATION->settings()->get("LastUsedGroupForNewInstance").toString(); } - NewInstanceDialog newInstDlg(groupName, url, this); + NewInstanceDialog newInstDlg(groupName, url, extra_info, this); if (!newInstDlg.exec()) return; @@ -976,18 +925,105 @@ void MainWindow::processURLs(QList urls) if (url.scheme().isEmpty()) url.setScheme("file"); - if (!url.isLocalFile()) { // probably instance/modpack - addInstance(url.toString()); - break; + ModPlatform::IndexedVersion version; + QMap extra_info; + QUrl local_url; + if (!url.isLocalFile()) { // download the remote resource and identify + QUrl dl_url; + if (url.scheme() == "curseforge") { + // need to find the download link for the modpack / resource + // format of url curseforge://install?addonId=IDHERE&fileId=IDHERE + QUrlQuery query(url); + + if (query.allQueryItemValues("addonId").isEmpty() || query.allQueryItemValues("fileId").isEmpty()) { + qDebug() << "Invalid curseforge link:" << url; + continue; + } + + auto addonId = query.allQueryItemValues("addonId")[0]; + auto fileId = query.allQueryItemValues("fileId")[0]; + + extra_info.insert("pack_id", addonId); + extra_info.insert("pack_version_id", fileId); + + auto array = std::make_shared(); + + auto api = FlameAPI(); + auto job = api.getFile(addonId, fileId, array); + + connect(job.get(), &Task::failed, this, + [this](QString reason) { CustomMessageBox::selectable(this, tr("Error"), reason, QMessageBox::Critical)->show(); }); + connect(job.get(), &Task::succeeded, this, [this, array, addonId, fileId, &dl_url, &version] { + qDebug() << "Returned CFURL Json:\n" << array->toStdString().c_str(); + auto doc = Json::requireDocument(*array); + auto data = Json::ensureObject(Json::ensureObject(doc.object()), "data"); + // No way to find out if it's a mod or a modpack before here + // And also we need to check if it ends with .zip, instead of any better way + version = FlameMod::loadIndexedPackVersion(data); + auto fileName = version.fileName; + + // Have to use ensureString then use QUrl to get proper url encoding + dl_url = QUrl(version.downloadUrl); + if (!dl_url.isValid()) { + CustomMessageBox::selectable( + this, tr("Error"), + tr("The modpack, mod, or resource %1 is blocked for third-parties! Please download it manually.").arg(fileName), + QMessageBox::Critical) + ->show(); + return; + } + + QFileInfo dl_file(dl_url.fileName()); + }); + + { // drop stack + ProgressDialog dlUrlDialod(this); + dlUrlDialod.setSkipButton(true, tr("Abort")); + dlUrlDialod.execWithTask(job.get()); + } + + } else { + dl_url = url; + } + + if (!dl_url.isValid()) { + continue; // no valid url to download this resource + } + + const QString path = dl_url.host() + '/' + dl_url.path(); + auto entry = APPLICATION->metacache()->resolveEntry("general", path); + entry->setStale(true); + auto dl_job = unique_qobject_ptr(new NetJob(tr("Modpack download"), APPLICATION->network())); + dl_job->addNetAction(Net::ApiDownload::makeCached(dl_url, entry)); + auto archivePath = entry->getFullPath(); + + bool dl_success = false; + connect(dl_job.get(), &Task::failed, this, + [this](QString reason) { CustomMessageBox::selectable(this, tr("Error"), reason, QMessageBox::Critical)->show(); }); + connect(dl_job.get(), &Task::succeeded, this, [&dl_success] { dl_success = true; }); + + { // drop stack + ProgressDialog dlUrlDialod(this); + dlUrlDialod.setSkipButton(true, tr("Abort")); + dlUrlDialod.execWithTask(dl_job.get()); + } + + if (!dl_success) { + continue; // no local file to identify + } + local_url = QUrl::fromLocalFile(archivePath); + + } else { + local_url = url; } - auto localFileName = QDir::toNativeSeparators(url.toLocalFile()); + auto localFileName = QDir::toNativeSeparators(local_url.toLocalFile()); QFileInfo localFileInfo(localFileName); auto type = ResourceUtils::identify(localFileInfo); if (ResourceUtils::ValidResourceTypes.count(type) == 0) { // probably instance/modpack - addInstance(localFileName); + addInstance(localFileName, extra_info); continue; } @@ -1012,7 +1048,7 @@ void MainWindow::processURLs(QList urls) qWarning() << "Importing of Data Packs not supported at this time. Ignoring" << localFileName; break; case PackedResourceType::Mod: - minecraftInst->loaderModList()->installMod(localFileName); + minecraftInst->loaderModList()->installMod(localFileName, version); break; case PackedResourceType::ShaderPack: minecraftInst->shaderPackList()->installResource(localFileName); @@ -1061,9 +1097,9 @@ void MainWindow::on_actionChangeInstIcon_triggered() void MainWindow::iconUpdated(QString icon) { if (icon == m_currentInstIcon) { - auto icon = APPLICATION->icons()->getIcon(m_currentInstIcon); - ui->actionChangeInstIcon->setIcon(icon); - changeIconButton->setIcon(icon); + auto new_icon = APPLICATION->icons()->getIcon(m_currentInstIcon); + ui->actionChangeInstIcon->setIcon(new_icon); + changeIconButton->setIcon(new_icon); } } @@ -1134,16 +1170,35 @@ void MainWindow::undoTrashInstance() ui->actionUndoTrashInstance->setEnabled(APPLICATION->instances()->trashedSomething()); } +void MainWindow::on_actionViewLauncherRootFolder_triggered() +{ + DesktopServices::openDirectory("."); +} + void MainWindow::on_actionViewInstanceFolder_triggered() { QString str = APPLICATION->settings()->get("InstanceDir").toString(); DesktopServices::openDirectory(str); } -void MainWindow::on_actionViewLauncherRootFolder_triggered() +void MainWindow::on_actionViewCentralModsFolder_triggered() { - const QString dataPath = QDir::currentPath(); - DesktopServices::openDirectory(dataPath); + DesktopServices::openDirectory(APPLICATION->settings()->get("CentralModsDir").toString(), true); +} + +void MainWindow::on_actionViewIconThemeFolder_triggered() +{ + DesktopServices::openDirectory(APPLICATION->themeManager()->getIconThemesFolder().path()); +} + +void MainWindow::on_actionViewWidgetThemeFolder_triggered() +{ + DesktopServices::openDirectory(APPLICATION->themeManager()->getApplicationThemesFolder().path()); +} + +void MainWindow::on_actionViewCatPackFolder_triggered() +{ + DesktopServices::openDirectory(APPLICATION->themeManager()->getCatPacksFolder().path()); } void MainWindow::refreshInstances() @@ -1151,11 +1206,6 @@ void MainWindow::refreshInstances() APPLICATION->instances()->loadList(); } -void MainWindow::on_actionViewCentralModsFolder_triggered() -{ - DesktopServices::openDirectory(APPLICATION->settings()->get("CentralModsDir").toString(), true); -} - void MainWindow::checkForUpdates() { if (BuildConfig.UPDATER_ENABLED) { @@ -1177,7 +1227,7 @@ void MainWindow::globalSettingsClosed() proxymodel->invalidate(); proxymodel->sort(0); updateMainToolBar(); - updateToolsMenu(); + updateLaunchButton(); updateThemeMenu(); updateStatusCenter(); // This needs to be done to prevent UI elements disappearing in the event the config is changed @@ -1307,10 +1357,11 @@ void MainWindow::on_actionDeleteInstance_triggered() if (APPLICATION->instances()->trashInstance(id)) { ui->actionUndoTrashInstance->setEnabled(APPLICATION->instances()->trashedSomething()); - return; + } else { + APPLICATION->instances()->deleteInstance(id); } - - APPLICATION->instances()->deleteInstance(id); + APPLICATION->settings()->set("SelectedInstance", QString()); + selectionBad(); } void MainWindow::on_actionExportInstanceZip_triggered() @@ -1412,20 +1463,6 @@ void MainWindow::activateInstance(InstancePtr instance) APPLICATION->launch(instance); } -void MainWindow::on_actionLaunchInstanceOffline_triggered() -{ - if (m_selectedInstance) { - APPLICATION->launch(m_selectedInstance, false); - } -} - -void MainWindow::on_actionLaunchInstanceDemo_triggered() -{ - if (m_selectedInstance) { - APPLICATION->launch(m_selectedInstance, false, true); - } -} - void MainWindow::on_actionKillInstance_triggered() { if (m_selectedInstance && m_selectedInstance->isRunning()) { @@ -1590,7 +1627,7 @@ void MainWindow::startTask(Task* task) task->start(); } -void MainWindow::instanceChanged(const QModelIndex& current, const QModelIndex& previous) +void MainWindow::instanceChanged(const QModelIndex& current, [[maybe_unused]] const QModelIndex& previous) { if (!current.isValid()) { APPLICATION->settings()->set("SelectedInstance", QString()); @@ -1599,6 +1636,7 @@ void MainWindow::instanceChanged(const QModelIndex& current, const QModelIndex& } if (m_selectedInstance) { disconnect(m_selectedInstance.get(), &BaseInstance::runningStatusChanged, this, &MainWindow::refreshCurrentInstance); + disconnect(m_selectedInstance.get(), &BaseInstance::profilerChanged, this, &MainWindow::refreshCurrentInstance); } QString id = current.data(InstanceList::InstanceIDRole).toString(); m_selectedInstance = APPLICATION->instances()->getInstanceById(id); @@ -1606,14 +1644,6 @@ void MainWindow::instanceChanged(const QModelIndex& current, const QModelIndex& ui->instanceToolBar->setEnabled(true); setInstanceActionsEnabled(true); ui->actionLaunchInstance->setEnabled(m_selectedInstance->canLaunch()); - ui->actionLaunchInstanceOffline->setEnabled(m_selectedInstance->canLaunch()); - ui->actionLaunchInstanceDemo->setEnabled(m_selectedInstance->canLaunch()); - - // Disable demo-mode if not available. - auto instance = dynamic_cast(m_selectedInstance.get()); - if (instance) { - ui->actionLaunchInstanceDemo->setEnabled(instance->supportsDemo()); - } ui->actionKillInstance->setEnabled(m_selectedInstance->isRunning()); ui->actionExportInstance->setEnabled(m_selectedInstance->canExport()); @@ -1622,18 +1652,13 @@ void MainWindow::instanceChanged(const QModelIndex& current, const QModelIndex& updateStatusCenter(); updateInstanceToolIcon(m_selectedInstance->iconKey()); - updateToolsMenu(); + updateLaunchButton(); APPLICATION->settings()->set("SelectedInstance", m_selectedInstance->id()); connect(m_selectedInstance.get(), &BaseInstance::runningStatusChanged, this, &MainWindow::refreshCurrentInstance); + connect(m_selectedInstance.get(), &BaseInstance::profilerChanged, this, &MainWindow::refreshCurrentInstance); } else { - ui->instanceToolBar->setEnabled(false); - setInstanceActionsEnabled(false); - ui->actionLaunchInstance->setEnabled(false); - ui->actionLaunchInstanceOffline->setEnabled(false); - ui->actionLaunchInstanceDemo->setEnabled(false); - ui->actionKillInstance->setEnabled(false); APPLICATION->settings()->set("SelectedInstance", QString()); selectionBad(); return; @@ -1658,11 +1683,12 @@ void MainWindow::selectionBad() { // start by reseting everything... m_selectedInstance = nullptr; + m_statusLeft->setText(tr("No instance selected")); statusBar()->clearMessage(); ui->instanceToolBar->setEnabled(false); setInstanceActionsEnabled(false); - updateToolsMenu(); + updateLaunchButton(); renameButton->setText(tr("Rename Instance")); updateInstanceToolIcon("grass"); @@ -1709,7 +1735,9 @@ void MainWindow::updateStatusCenter() int timePlayed = APPLICATION->instances()->getTotalPlayTime(); if (timePlayed > 0) { - m_statusCenter->setText(tr("Total playtime: %1").arg(Time::prettifyDuration(timePlayed))); + m_statusCenter->setText( + tr("Total playtime: %1") + .arg(Time::prettifyDuration(timePlayed, APPLICATION->settings()->get("ShowGameTimeWithoutDays").toBool()))); } } // "Instance actions" are actions that require an instance to be selected (i.e. "new instance" is not here) @@ -1725,7 +1753,7 @@ void MainWindow::setInstanceActionsEnabled(bool enabled) ui->actionCreateInstanceShortcut->setEnabled(enabled); } -void MainWindow::refreshCurrentInstance(bool running) +void MainWindow::refreshCurrentInstance() { auto current = view->selectionModel()->currentIndex(); instanceChanged(current, current); diff --git a/launcher/ui/MainWindow.h b/launcher/ui/MainWindow.h index e6434dae2..0b6144522 100644 --- a/launcher/ui/MainWindow.h +++ b/launcher/ui/MainWindow.h @@ -109,16 +109,19 @@ class MainWindow : public QMainWindow { void on_actionChangeInstIcon_triggered(); - void on_actionViewInstanceFolder_triggered(); - void on_actionViewLauncherRootFolder_triggered(); + void on_actionViewInstanceFolder_triggered(); + void on_actionViewCentralModsFolder_triggered(); + + void on_actionViewIconThemeFolder_triggered(); + void on_actionViewWidgetThemeFolder_triggered(); + void on_actionViewCatPackFolder_triggered(); + void on_actionViewSelectedInstFolder_triggered(); void refreshInstances(); - void on_actionViewCentralModsFolder_triggered(); - void checkForUpdates(); void on_actionSettings_triggered(); @@ -141,10 +144,6 @@ class MainWindow : public QMainWindow { void on_actionLaunchInstance_triggered(); - void on_actionLaunchInstanceOffline_triggered(); - - void on_actionLaunchInstanceDemo_triggered(); - void on_actionKillInstance_triggered(); void on_actionDeleteInstance_triggered(); @@ -152,10 +151,7 @@ class MainWindow : public QMainWindow { void deleteGroup(); void undoTrashInstance(); - inline void on_actionExportInstance_triggered() - { - on_actionExportInstanceZip_triggered(); - } + inline void on_actionExportInstance_triggered() { on_actionExportInstanceZip_triggered(); } void on_actionExportInstanceZip_triggered(); void on_actionExportInstanceMrPack_triggered(); void on_actionExportInstanceFlamePack_triggered(); @@ -178,7 +174,7 @@ class MainWindow : public QMainWindow { void updateMainToolBar(); - void updateToolsMenu(); + void updateLaunchButton(); void updateThemeMenu(); @@ -212,12 +208,12 @@ class MainWindow : public QMainWindow { void keyReleaseEvent(QKeyEvent* event) override; #endif - void refreshCurrentInstance(bool running); + void refreshCurrentInstance(); private: void retranslateUi(); - void addInstance(QString url = QString()); + void addInstance(const QString& url = QString(), const QMap& extra_info = {}); void activateInstance(InstancePtr instance); void setCatBackground(bool enabled); void updateInstanceToolIcon(QString new_icon); diff --git a/launcher/ui/MainWindow.ui b/launcher/ui/MainWindow.ui index e4421d400..91b2c2703 100644 --- a/launcher/ui/MainWindow.ui +++ b/launcher/ui/MainWindow.ui @@ -186,9 +186,14 @@ true - + + + + + + @@ -435,22 +440,6 @@ Ctrl+D - - - Launch &Offline - - - Launch the selected instance in offline mode. - - - - - Launch &Demo - - - Launch the selected instance in demo mode. - - @@ -465,7 +454,8 @@ - + + .. Prism Launcher (zip) @@ -473,7 +463,8 @@ - + + .. Modrinth (mrpack) @@ -481,15 +472,17 @@ - + + .. - CurseForge (zip) + CurseForge (zip) - + + .. Mod List @@ -552,7 +545,7 @@ .. - &View Instance Folder + View &Instance Folder Open the instance folder in a file browser. @@ -564,7 +557,7 @@ .. - &View Launcher Root Folder + View Launcher &Root Folder Open the launcher's root folder in a file browser. @@ -719,6 +712,38 @@ Open the %1 wiki + + + + .. + + + View &Widget Themes Folder + + + View Widget Theme Folder + + + + + + .. + + + View I&con Theme Folder + + + View Icon Theme Folder + + + + + + + + View Cat Packs Folder + + diff --git a/launcher/ui/dialogs/CopyInstanceDialog.cpp b/launcher/ui/dialogs/CopyInstanceDialog.cpp index 0e4100279..8b2f5bb80 100644 --- a/launcher/ui/dialogs/CopyInstanceDialog.cpp +++ b/launcher/ui/dialogs/CopyInstanceDialog.cpp @@ -220,7 +220,7 @@ void CopyInstanceDialog::on_iconButton_clicked() } } -void CopyInstanceDialog::on_instNameTextBox_textChanged(const QString& arg1) +void CopyInstanceDialog::on_instNameTextBox_textChanged([[maybe_unused]] const QString& arg1) { updateDialogState(); } diff --git a/launcher/ui/dialogs/ExportInstanceDialog.cpp b/launcher/ui/dialogs/ExportInstanceDialog.cpp index d6a503ccd..703736d68 100644 --- a/launcher/ui/dialogs/ExportInstanceDialog.cpp +++ b/launcher/ui/dialogs/ExportInstanceDialog.cpp @@ -195,8 +195,8 @@ void ExportInstanceDialog::loadPackIgnore() if (!ignoreFile.open(QIODevice::ReadOnly)) { return; } - auto data = ignoreFile.readAll(); - auto string = QString::fromUtf8(data); + auto ignoreData = ignoreFile.readAll(); + auto string = QString::fromUtf8(ignoreData); #if QT_VERSION >= QT_VERSION_CHECK(5, 14, 0) proxyModel->setBlockedPaths(string.split('\n', Qt::SkipEmptyParts)); #else @@ -206,10 +206,10 @@ void ExportInstanceDialog::loadPackIgnore() void ExportInstanceDialog::savePackIgnore() { - auto data = proxyModel->blockedPaths().toStringList().join('\n').toUtf8(); + auto ignoreData = proxyModel->blockedPaths().toStringList().join('\n').toUtf8(); auto filename = ignoreFileName(); try { - FS::write(filename, data); + FS::write(filename, ignoreData); } catch (const Exception& e) { qWarning() << e.cause(); } diff --git a/launcher/ui/dialogs/ExportPackDialog.cpp b/launcher/ui/dialogs/ExportPackDialog.cpp index ad8db5ffb..5af24b1b7 100644 --- a/launcher/ui/dialogs/ExportPackDialog.cpp +++ b/launcher/ui/dialogs/ExportPackDialog.cpp @@ -37,15 +37,21 @@ ExportPackDialog::ExportPackDialog(InstancePtr instance, QWidget* parent, ModPlatform::ResourceProvider provider) : QDialog(parent), instance(instance), ui(new Ui::ExportPackDialog), m_provider(provider) { + Q_ASSERT(m_provider == ModPlatform::ResourceProvider::MODRINTH || m_provider == ModPlatform::ResourceProvider::FLAME); + ui->setupUi(this); - ui->name->setText(instance->name()); + ui->name->setPlaceholderText(instance->name()); + ui->name->setText(instance->settings()->get("ExportName").toString()); + ui->version->setText(instance->settings()->get("ExportVersion").toString()); + ui->optionalFiles->setChecked(instance->settings()->get("ExportOptionalFiles").toBool()); + if (m_provider == ModPlatform::ResourceProvider::MODRINTH) { - ui->summary->setText(instance->notes().split(QRegularExpression("\\r?\\n"))[0]); - setWindowTitle("Export Modrinth Pack"); + setWindowTitle(tr("Export Modrinth Pack")); + ui->summary->setText(instance->settings()->get("ExportSummary").toString()); } else { - setWindowTitle("Export CurseForge Pack"); - ui->version->setText(""); - ui->summaryLabel->setText("Author"); + setWindowTitle(tr("Export CurseForge Pack")); + ui->summaryLabel->setText(tr("&Author")); + ui->summary->setText(instance->settings()->get("ExportAuthor").toString()); } // ensure a valid pack is generated @@ -75,20 +81,19 @@ ExportPackDialog::ExportPackDialog(InstancePtr instance, QWidget* parent, ModPla MinecraftInstance* mcInstance = dynamic_cast(instance.get()); if (mcInstance) { - mcInstance->loaderModList()->update(); const QDir index = mcInstance->loaderModList()->indexDir(); if (index.exists()) - proxy->blockedPaths().insert(root.relativeFilePath(index.absolutePath())); + proxy->ignoreFilesWithPath().insert(root.relativeFilePath(index.absolutePath())); } - ui->treeView->setModel(proxy); - ui->treeView->setRootIndex(proxy->mapFromSource(model->index(instance->gameRoot()))); - ui->treeView->sortByColumn(0, Qt::AscendingOrder); + ui->files->setModel(proxy); + ui->files->setRootIndex(proxy->mapFromSource(model->index(instance->gameRoot()))); + ui->files->sortByColumn(0, Qt::AscendingOrder); model->setFilter(filter); model->setRootPath(instance->gameRoot()); - QHeaderView* headerView = ui->treeView->header(); + QHeaderView* headerView = ui->files->header(); headerView->setSectionResizeMode(QHeaderView::ResizeToContents); headerView->setSectionResizeMode(0, QHeaderView::Stretch); } @@ -100,26 +105,41 @@ ExportPackDialog::~ExportPackDialog() void ExportPackDialog::done(int result) { - if (result == Accepted) { - const QString filename = FS::RemoveInvalidFilenameChars(ui->name->text()); - QString output; - if (m_provider == ModPlatform::ResourceProvider::MODRINTH) - output = QFileDialog::getSaveFileName(this, tr("Export %1").arg(ui->name->text()), - FS::PathCombine(QDir::homePath(), filename + ".mrpack"), "Modrinth pack (*.mrpack *.zip)", - nullptr); - else - output = QFileDialog::getSaveFileName(this, tr("Export %1").arg(ui->name->text()), - FS::PathCombine(QDir::homePath(), filename + ".zip"), "CurseForge pack (*.zip)", nullptr); + auto settings = instance->settings(); + settings->set("ExportName", ui->name->text()); + settings->set("ExportVersion", ui->version->text()); + settings->set(m_provider == ModPlatform::ResourceProvider::FLAME ? "ExportAuthor" : "ExportSummary", ui->summary->text()); + settings->set("ExportOptionalFiles", ui->optionalFiles->isChecked()); + + if (result == Accepted) { + const QString name = ui->name->text().isEmpty() ? instance->name() : ui->name->text(); + const QString filename = FS::RemoveInvalidFilenameChars(name); + + QString output; + if (m_provider == ModPlatform::ResourceProvider::MODRINTH) { + output = QFileDialog::getSaveFileName(this, tr("Export %1").arg(name), FS::PathCombine(QDir::homePath(), filename + ".mrpack"), + "Modrinth pack (*.mrpack *.zip)", nullptr); + if (output.isEmpty()) + return; + if (!(output.endsWith(".zip") || output.endsWith(".mrpack"))) + output.append(".mrpack"); + } else { + output = QFileDialog::getSaveFileName(this, tr("Export %1").arg(name), FS::PathCombine(QDir::homePath(), filename + ".zip"), + "CurseForge pack (*.zip)", nullptr); + if (output.isEmpty()) + return; + if (!output.endsWith(".zip")) + output.append(".zip"); + } - if (output.isEmpty()) - return; Task* task; - if (m_provider == ModPlatform::ResourceProvider::MODRINTH) - task = new ModrinthPackExportTask(ui->name->text(), ui->version->text(), ui->summary->text(), instance, output, - std::bind(&FileIgnoreProxy::filterFile, proxy, std::placeholders::_1)); - else - task = new FlamePackExportTask(ui->name->text(), ui->version->text(), ui->summary->text(), instance, output, + if (m_provider == ModPlatform::ResourceProvider::MODRINTH) { + task = new ModrinthPackExportTask(name, ui->version->text(), ui->summary->text(), ui->optionalFiles->isChecked(), instance, + output, std::bind(&FileIgnoreProxy::filterFile, proxy, std::placeholders::_1)); + } else { + task = new FlamePackExportTask(name, ui->version->text(), ui->summary->text(), ui->optionalFiles->isChecked(), instance, output, std::bind(&FileIgnoreProxy::filterFile, proxy, std::placeholders::_1)); + } connect(task, &Task::failed, [this](const QString reason) { CustomMessageBox::selectable(this, tr("Error"), reason, QMessageBox::Critical)->show(); }); @@ -140,7 +160,6 @@ void ExportPackDialog::done(int result) void ExportPackDialog::validate() { - const bool invalid = - ui->name->text().isEmpty() || ((m_provider == ModPlatform::ResourceProvider::MODRINTH) && ui->version->text().isEmpty()); - ui->buttonBox->button(QDialogButtonBox::Ok)->setDisabled(invalid); + ui->buttonBox->button(QDialogButtonBox::Ok) + ->setDisabled(m_provider == ModPlatform::ResourceProvider::MODRINTH && ui->version->text().isEmpty()); } diff --git a/launcher/ui/dialogs/ExportPackDialog.ui b/launcher/ui/dialogs/ExportPackDialog.ui index 3976e28f8..09dea72a8 100644 --- a/launcher/ui/dialogs/ExportPackDialog.ui +++ b/launcher/ui/dialogs/ExportPackDialog.ui @@ -7,12 +7,9 @@ 0 0 650 - 413 + 510 - - Export Pack - true @@ -20,13 +17,16 @@ - Information + &Description - Summary + &Summary + + + summary @@ -36,14 +36,20 @@ - Name + &Name + + + name - Version + &Version + + + version @@ -57,31 +63,52 @@ - - - - Files + + + &Options - - - - - - true - - - QAbstractItemView::ExtendedSelection - - - true - - - false - + + + + + &Files + + + files + + + + + + + true + + + QAbstractItemView::ExtendedSelection + + + true + + + false + + + + + + + &Mark disabled files as optional + + + true + + + + @@ -97,7 +124,8 @@ name version summary - treeView + files + optionalFiles diff --git a/launcher/ui/dialogs/InstallLoaderDialog.cpp b/launcher/ui/dialogs/InstallLoaderDialog.cpp new file mode 100644 index 000000000..541119d10 --- /dev/null +++ b/launcher/ui/dialogs/InstallLoaderDialog.cpp @@ -0,0 +1,166 @@ +// SPDX-License-Identifier: GPL-3.0-only +/* + * Prism Launcher - Minecraft Launcher + * Copyright (C) 2023 TheKodeToad + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, version 3. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +#include "InstallLoaderDialog.h" + +#include +#include +#include +#include "Application.h" +#include "BuildConfig.h" +#include "DesktopServices.h" +#include "meta/Index.h" +#include "minecraft/MinecraftInstance.h" +#include "minecraft/PackProfile.h" +#include "ui/widgets/PageContainer.h" +#include "ui/widgets/VersionSelectWidget.h" + +class InstallLoaderPage : public VersionSelectWidget, public BasePage { + public: + InstallLoaderPage(const QString& id, + const QString& iconName, + const QString& name, + const Version& oldestVersion, + const std::shared_ptr profile) + : VersionSelectWidget(nullptr), uid(id), iconName(iconName), name(name) + { + const QString minecraftVersion = profile->getComponentVersion("net.minecraft"); + setEmptyString(tr("No versions are currently available for Minecraft %1").arg(minecraftVersion)); + setExactIfPresentFilter(BaseVersionList::ParentVersionRole, minecraftVersion); + + if (oldestVersion != Version() && Version(minecraftVersion) < oldestVersion) + setExactFilter(BaseVersionList::ParentVersionRole, "AAA"); + + if (const QString currentVersion = profile->getComponentVersion(id); !currentVersion.isNull()) + setCurrentVersion(currentVersion); + } + + QString id() const override { return uid; } + QString displayName() const override { return name; } + QIcon icon() const override { return APPLICATION->getThemedIcon(iconName); } + + void openedImpl() override + { + if (loaded) + return; + + const auto versions = APPLICATION->metadataIndex()->get(uid); + if (!versions) + return; + + initialize(versions.get()); + loaded = true; + } + + void setParentContainer(BasePageContainer* container) override + { + auto dialog = dynamic_cast(dynamic_cast(container)->parent()); + connect(view(), &QAbstractItemView::doubleClicked, dialog, &QDialog::accept); + } + + private: + const QString uid; + const QString iconName; + const QString name; + bool loaded = false; +}; + +static InstallLoaderPage* pageCast(BasePage* page) +{ + auto result = dynamic_cast(page); + Q_ASSERT(result != nullptr); + return result; +} + +InstallLoaderDialog::InstallLoaderDialog(std::shared_ptr profile, const QString& uid, QWidget* parent) + : QDialog(parent), profile(std::move(profile)), container(new PageContainer(this, QString(), this)), buttons(new QDialogButtonBox(this)) +{ + auto layout = new QVBoxLayout(this); + + container->setSizePolicy(QSizePolicy::Preferred, QSizePolicy::Expanding); + layout->addWidget(container); + + auto buttonLayout = new QHBoxLayout(this); + + auto refreshButton = new QPushButton(tr("&Refresh"), this); + connect(refreshButton, &QPushButton::clicked, this, [this] { pageCast(container->selectedPage())->loadList(); }); + buttonLayout->addWidget(refreshButton); + + buttons->setOrientation(Qt::Horizontal); + buttons->setStandardButtons(QDialogButtonBox::Cancel | QDialogButtonBox::Ok); + connect(buttons, &QDialogButtonBox::accepted, this, &QDialog::accept); + connect(buttons, &QDialogButtonBox::rejected, this, &QDialog::reject); + buttonLayout->addWidget(buttons); + + layout->addLayout(buttonLayout); + + setWindowTitle(dialogTitle()); + setWindowModality(Qt::WindowModal); + resize(520, 347); + + for (BasePage* page : container->getPages()) { + if (page->id() == uid) + container->selectPage(page->id()); + + connect(pageCast(page), &VersionSelectWidget::selectedVersionChanged, this, [this, page] { + if (page->id() == container->selectedPage()->id()) + validate(container->selectedPage()); + }); + } + connect(container, &PageContainer::selectedPageChanged, this, [this](BasePage* previous, BasePage* current) { validate(current); }); + pageCast(container->selectedPage())->selectSearch(); + validate(container->selectedPage()); +} + +QList InstallLoaderDialog::getPages() +{ + return { // NeoForge + new InstallLoaderPage("net.neoforged", "neoforged", tr("NeoForge"), {}, profile), + // Forge + new InstallLoaderPage("net.minecraftforge", "forge", tr("Forge"), {}, profile), + // Fabric + new InstallLoaderPage("net.fabricmc.fabric-loader", "fabricmc", tr("Fabric"), Version("1.14"), profile), + // Quilt + new InstallLoaderPage("org.quiltmc.quilt-loader", "quiltmc", tr("Quilt"), Version("1.14"), profile), + // LiteLoader + new InstallLoaderPage("com.mumfrey.liteloader", "liteloader", tr("LiteLoader"), {}, profile) + }; +} + +QString InstallLoaderDialog::dialogTitle() +{ + return tr("Install Loader"); +} + +void InstallLoaderDialog::validate(BasePage* page) +{ + buttons->button(QDialogButtonBox::Ok)->setEnabled(pageCast(page)->selectedVersion() != nullptr); +} + +void InstallLoaderDialog::done(int result) +{ + if (result == Accepted) { + auto* page = pageCast(container->selectedPage()); + if (page->selectedVersion()) { + profile->setComponentVersion(page->id(), page->selectedVersion()->descriptor()); + profile->resolve(Net::Mode::Online); + } + } + + QDialog::done(result); +} diff --git a/launcher/ui/dialogs/InstallLoaderDialog.h b/launcher/ui/dialogs/InstallLoaderDialog.h new file mode 100644 index 000000000..86cb3bdd2 --- /dev/null +++ b/launcher/ui/dialogs/InstallLoaderDialog.h @@ -0,0 +1,45 @@ +// SPDX-License-Identifier: GPL-3.0-only +/* + * Prism Launcher - Minecraft Launcher + * Copyright (C) 2023 TheKodeToad + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, version 3. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +#pragma once + +#include +#include "ui/pages/BasePageProvider.h" + +class MinecraftInstance; +class PageContainer; +class PackProfile; +class QDialogButtonBox; + +class InstallLoaderDialog final : public QDialog, protected BasePageProvider { + Q_OBJECT + + public: + explicit InstallLoaderDialog(std::shared_ptr instance, const QString& uid = QString(), QWidget* parent = nullptr); + + QList getPages() override; + QString dialogTitle() override; + + void validate(BasePage* page); + void done(int result) override; + + private: + std::shared_ptr profile; + PageContainer* container; + QDialogButtonBox* buttons; +}; diff --git a/launcher/ui/dialogs/ModUpdateDialog.cpp b/launcher/ui/dialogs/ModUpdateDialog.cpp index 79622ff74..0e11ff1a6 100644 --- a/launcher/ui/dialogs/ModUpdateDialog.cpp +++ b/launcher/ui/dialogs/ModUpdateDialog.cpp @@ -5,8 +5,6 @@ #include "ScrollMessageBox.h" #include "ui_ReviewMessageBox.h" -#include "FileSystem.h" -#include "Json.h" #include "Markdown.h" #include "tasks/ConcurrentTask.h" @@ -30,9 +28,9 @@ static std::list mcVersions(BaseInstance* inst) return { static_cast(inst)->getPackProfile()->getComponent("net.minecraft")->getVersion() }; } -static std::optional mcLoaders(BaseInstance* inst) +static std::optional mcLoaders(BaseInstance* inst) { - return { static_cast(inst)->getPackProfile()->getModLoaders() }; + return { static_cast(inst)->getPackProfile()->getSupportedModLoaders() }; } ModUpdateDialog::ModUpdateDialog(QWidget* parent, @@ -238,6 +236,10 @@ auto ModUpdateDialog::ensureMetadata() -> bool if (skip_rest) continue; + if (candidate->type() == ResourceType::FOLDER) { + continue; + } + if (confirm_rest) { addToTmp(candidate, provider_rest); should_try_others.insert(candidate->internal_id(), try_others_rest); diff --git a/launcher/ui/dialogs/NewInstanceDialog.cpp b/launcher/ui/dialogs/NewInstanceDialog.cpp index 6e5a41efc..9613c6b00 100644 --- a/launcher/ui/dialogs/NewInstanceDialog.cpp +++ b/launcher/ui/dialogs/NewInstanceDialog.cpp @@ -62,8 +62,10 @@ #include "ui/pages/modplatform/modrinth/ModrinthPage.h" #include "ui/pages/modplatform/technic/TechnicPage.h" #include "ui/widgets/PageContainer.h" - -NewInstanceDialog::NewInstanceDialog(const QString& initialGroup, const QString& url, QWidget* parent) +NewInstanceDialog::NewInstanceDialog(const QString& initialGroup, + const QString& url, + const QMap& extra_info, + QWidget* parent) : QDialog(parent), ui(new Ui::NewInstanceDialog) { ui->setupUi(this); @@ -125,6 +127,7 @@ NewInstanceDialog::NewInstanceDialog(const QString& initialGroup, const QString& QUrl actualUrl(url); m_container->selectPage("import"); importPage->setUrl(url); + importPage->setExtraInfo(extra_info); } updateDialogState(); @@ -290,7 +293,7 @@ void NewInstanceDialog::on_iconButton_clicked() } } -void NewInstanceDialog::on_instNameTextBox_textChanged(const QString& arg1) +void NewInstanceDialog::on_instNameTextBox_textChanged([[maybe_unused]] const QString& arg1) { updateDialogState(); } @@ -299,7 +302,7 @@ void NewInstanceDialog::importIconNow() { if (importIcon) { APPLICATION->icons()->installIcon(importIconPath, importIconName); - InstIconKey = importIconName; + InstIconKey = importIconName.mid(0, importIconName.lastIndexOf('.')); importIcon = false; } APPLICATION->settings()->set("NewInstanceGeometry", saveGeometry().toBase64()); diff --git a/launcher/ui/dialogs/NewInstanceDialog.h b/launcher/ui/dialogs/NewInstanceDialog.h index b348649fe..923579567 100644 --- a/launcher/ui/dialogs/NewInstanceDialog.h +++ b/launcher/ui/dialogs/NewInstanceDialog.h @@ -53,7 +53,10 @@ class NewInstanceDialog : public QDialog, public BasePageProvider { Q_OBJECT public: - explicit NewInstanceDialog(const QString& initialGroup, const QString& url = QString(), QWidget* parent = 0); + explicit NewInstanceDialog(const QString& initialGroup, + const QString& url = QString(), + const QMap& extra_info = {}, + QWidget* parent = 0); ~NewInstanceDialog(); void updateDialogState(); diff --git a/launcher/ui/dialogs/ProfileSetupDialog.cpp b/launcher/ui/dialogs/ProfileSetupDialog.cpp index d7758d6d8..4b0c5b768 100644 --- a/launcher/ui/dialogs/ProfileSetupDialog.cpp +++ b/launcher/ui/dialogs/ProfileSetupDialog.cpp @@ -163,13 +163,15 @@ void ProfileSetupDialog::checkName(const QString& name) requestor->get(request); } -void ProfileSetupDialog::checkFinished(QNetworkReply::NetworkError error, QByteArray data, QList headers) +void ProfileSetupDialog::checkFinished(QNetworkReply::NetworkError error, + QByteArray profileData, + [[maybe_unused]] QList headers) { auto requestor = qobject_cast(QObject::sender()); requestor->deleteLater(); if (error == QNetworkReply::NoError) { - auto doc = QJsonDocument::fromJson(data); + auto doc = QJsonDocument::fromJson(profileData); auto root = doc.object(); auto statusValue = root.value("status").toString("INVALID"); if (statusValue == "AVAILABLE") { @@ -202,11 +204,11 @@ void ProfileSetupDialog::setupProfile(const QString& profileName) request.setRawHeader("Authorization", QString("Bearer %1").arg(token).toUtf8()); QString payloadTemplate("{\"profileName\":\"%1\"}"); - auto data = payloadTemplate.arg(profileName).toUtf8(); + auto profileData = payloadTemplate.arg(profileName).toUtf8(); AuthRequest* requestor = new AuthRequest(this); connect(requestor, &AuthRequest::finished, this, &ProfileSetupDialog::setupProfileFinished); - requestor->post(request, data); + requestor->post(request, profileData); isWorking = true; auto button = ui->buttonBox->button(QDialogButtonBox::Cancel); @@ -243,8 +245,8 @@ struct MojangError { } // namespace void ProfileSetupDialog::setupProfileFinished(QNetworkReply::NetworkError error, - QByteArray data, - QList headers) + QByteArray errorData, + [[maybe_unused]] QList headers) { auto requestor = qobject_cast(QObject::sender()); requestor->deleteLater(); @@ -257,7 +259,7 @@ void ProfileSetupDialog::setupProfileFinished(QNetworkReply::NetworkError error, */ accept(); } else { - auto parsedError = MojangError::fromJSON(data); + auto parsedError = MojangError::fromJSON(errorData); ui->errorLabel->setVisible(true); ui->errorLabel->setText(tr("The server returned the following error:") + "\n\n" + parsedError.errorMessage); qDebug() << parsedError.rawError; diff --git a/launcher/ui/dialogs/ProgressDialog.cpp b/launcher/ui/dialogs/ProgressDialog.cpp index ba22e334f..0ca3a1bd9 100644 --- a/launcher/ui/dialogs/ProgressDialog.cpp +++ b/launcher/ui/dialogs/ProgressDialog.cpp @@ -85,7 +85,7 @@ void ProgressDialog::on_skipButton_clicked(bool checked) { Q_UNUSED(checked); if (ui->skipButton->isEnabled()) // prevent other triggers from aborting - task->abort(); + m_task->abort(); } ProgressDialog::~ProgressDialog() @@ -127,7 +127,7 @@ void ProgressDialog::updateSize(bool recenterParent) int ProgressDialog::execWithTask(Task* task) { - this->task = task; + this->m_task = task; if (!task) { qDebug() << "Programmer error: Progress dialog created with null task."; @@ -179,8 +179,8 @@ int ProgressDialog::execWithTask(std::unique_ptr& task) bool ProgressDialog::handleImmediateResult(QDialog::DialogCode& result) { - if (task->isFinished()) { - if (task->wasSuccessful()) { + if (m_task->isFinished()) { + if (m_task->wasSuccessful()) { result = QDialog::Accepted; } else { result = QDialog::Rejected; @@ -192,12 +192,12 @@ bool ProgressDialog::handleImmediateResult(QDialog::DialogCode& result) Task* ProgressDialog::getTask() { - return task; + return m_task; } void ProgressDialog::onTaskStarted() {} -void ProgressDialog::onTaskFailed(QString failure) +void ProgressDialog::onTaskFailed([[maybe_unused]] QString failure) { reject(); hide(); @@ -209,11 +209,11 @@ void ProgressDialog::onTaskSucceeded() hide(); } -void ProgressDialog::changeStatus(const QString& status) +void ProgressDialog::changeStatus([[maybe_unused]] const QString& status) { - ui->globalStatusLabel->setText(task->getStatus()); + ui->globalStatusLabel->setText(m_task->getStatus()); ui->globalStatusLabel->adjustSize(); - ui->globalStatusDetailsLabel->setText(task->getDetails()); + ui->globalStatusDetailsLabel->setText(m_task->getDetails()); ui->globalStatusDetailsLabel->adjustSize(); updateSize(); @@ -279,7 +279,7 @@ void ProgressDialog::keyPressEvent(QKeyEvent* e) void ProgressDialog::closeEvent(QCloseEvent* e) { - if (task && task->isRunning()) { + if (m_task && m_task->isRunning()) { e->ignore(); } else { QDialog::closeEvent(e); diff --git a/launcher/ui/dialogs/ProgressDialog.h b/launcher/ui/dialogs/ProgressDialog.h index 82067271d..15eadf4e7 100644 --- a/launcher/ui/dialogs/ProgressDialog.h +++ b/launcher/ui/dialogs/ProgressDialog.h @@ -92,7 +92,7 @@ class ProgressDialog : public QDialog { private: Ui::ProgressDialog* ui; - Task* task; + Task* m_task; bool m_is_multi_step = false; QHash taskProgress; diff --git a/launcher/ui/dialogs/ResourceDownloadDialog.cpp b/launcher/ui/dialogs/ResourceDownloadDialog.cpp index 16bdc9b09..cfd292f4a 100644 --- a/launcher/ui/dialogs/ResourceDownloadDialog.cpp +++ b/launcher/ui/dialogs/ResourceDownloadDialog.cpp @@ -2,7 +2,7 @@ /* * Prism Launcher - Minecraft Launcher * Copyright (C) 2022 Sefa Eyeoglu - * Copyright (C) 2022 TheKodeToad + * Copyright (C) 2023 TheKodeToad * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by @@ -210,15 +210,17 @@ bool ResourceDownloadDialog::selectPage(QString pageId) return m_container->selectPage(pageId); } -ResourcePage* ResourceDownloadDialog::getSelectedPage() +ResourcePage* ResourceDownloadDialog::selectedPage() { - return m_selectedPage; + ResourcePage* result = dynamic_cast(m_container->selectedPage()); + Q_ASSERT(result != nullptr); + return result; } void ResourceDownloadDialog::addResource(ModPlatform::IndexedPack::Ptr pack, ModPlatform::IndexedVersion& ver) { removeResource(pack->name); - m_selectedPage->addResourceToPage(pack, ver, getBaseModel()); + selectedPage()->addResourceToPage(pack, ver, getBaseModel()); setButtonStatus(); } @@ -258,14 +260,8 @@ void ResourceDownloadDialog::selectedPageChanged(BasePage* previous, BasePage* s return; } - m_selectedPage = dynamic_cast(selected); - if (!m_selectedPage) { - qCritical() << "Page '" << selected->displayName() << "' in ResourceDownloadDialog is not a ResourcePage!"; - return; - } - // Same effect as having a global search bar - m_selectedPage->setSearchTerm(prev_page->getSearchTerm()); + selectedPage()->setSearchTerm(prev_page->getSearchTerm()); } ModDownloadDialog::ModDownloadDialog(QWidget* parent, const std::shared_ptr& mods, BaseInstance* instance) @@ -284,15 +280,13 @@ QList ModDownloadDialog::getPages() { QList pages; - auto loaders = static_cast(m_instance)->getPackProfile()->getModLoaders().value(); + auto loaders = static_cast(m_instance)->getPackProfile()->getSupportedModLoaders().value(); if (ModrinthAPI::validateModLoaders(loaders)) pages.append(ModrinthModPage::create(this, *m_instance)); if (APPLICATION->capabilities() & Application::SupportsFlame && FlameAPI::validateModLoaders(loaders)) pages.append(FlameModPage::create(this, *m_instance)); - m_selectedPage = dynamic_cast(pages[0]); - return pages; } @@ -307,7 +301,7 @@ GetModDependenciesTask::Ptr ModDownloadDialog::getModDependenciesTask() return makeShared(this, m_instance, model, selectedVers); } return nullptr; -}; +} ResourcePackDownloadDialog::ResourcePackDownloadDialog(QWidget* parent, const std::shared_ptr& resource_packs, @@ -331,8 +325,6 @@ QList ResourcePackDownloadDialog::getPages() if (APPLICATION->capabilities() & Application::SupportsFlame) pages.append(FlameResourcePackPage::create(this, *m_instance)); - m_selectedPage = dynamic_cast(pages[0]); - return pages; } @@ -358,8 +350,6 @@ QList TexturePackDownloadDialog::getPages() if (APPLICATION->capabilities() & Application::SupportsFlame) pages.append(FlameTexturePackPage::create(this, *m_instance)); - m_selectedPage = dynamic_cast(pages[0]); - return pages; } @@ -380,11 +370,9 @@ ShaderPackDownloadDialog::ShaderPackDownloadDialog(QWidget* parent, QList ShaderPackDownloadDialog::getPages() { QList pages; - pages.append(ModrinthShaderPackPage::create(this, *m_instance)); - - m_selectedPage = dynamic_cast(pages[0]); - + if (APPLICATION->capabilities() & Application::SupportsFlame) + pages.append(FlameShaderPackPage::create(this, *m_instance)); return pages; } diff --git a/launcher/ui/dialogs/ResourceDownloadDialog.h b/launcher/ui/dialogs/ResourceDownloadDialog.h index f65daaa3f..e9d2cfbe6 100644 --- a/launcher/ui/dialogs/ResourceDownloadDialog.h +++ b/launcher/ui/dialogs/ResourceDownloadDialog.h @@ -2,7 +2,7 @@ /* * Prism Launcher - Minecraft Launcher * Copyright (C) 2022 Sefa Eyeoglu - * Copyright (C) 2022 TheKodeToad + * Copyright (C) 2023 TheKodeToad * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by @@ -61,7 +61,7 @@ class ResourceDownloadDialog : public QDialog, public BasePageProvider { QString dialogTitle() override { return tr("Download %1").arg(resourcesString()); }; bool selectPage(QString pageId); - ResourcePage* getSelectedPage(); + ResourcePage* selectedPage(); void addResource(ModPlatform::IndexedPack::Ptr, ModPlatform::IndexedVersion&); void removeResource(const QString&); @@ -88,7 +88,6 @@ class ResourceDownloadDialog : public QDialog, public BasePageProvider { const std::shared_ptr m_base_model; PageContainer* m_container = nullptr; - ResourcePage* m_selectedPage = nullptr; QDialogButtonBox m_buttons; QVBoxLayout m_vertical_layout; diff --git a/launcher/ui/dialogs/ReviewMessageBox.cpp b/launcher/ui/dialogs/ReviewMessageBox.cpp index d1b7bd156..aa668f8c2 100644 --- a/launcher/ui/dialogs/ReviewMessageBox.cpp +++ b/launcher/ui/dialogs/ReviewMessageBox.cpp @@ -5,7 +5,7 @@ #include -ReviewMessageBox::ReviewMessageBox(QWidget* parent, QString const& title, QString const& icon) +ReviewMessageBox::ReviewMessageBox(QWidget* parent, [[maybe_unused]] QString const& title, [[maybe_unused]] QString const& icon) : QDialog(parent), ui(new Ui::ReviewMessageBox) { ui->setupUi(this); diff --git a/launcher/ui/dialogs/SkinUploadDialog.cpp b/launcher/ui/dialogs/SkinUploadDialog.cpp index 8f0c8fa49..5b3ebfa23 100644 --- a/launcher/ui/dialogs/SkinUploadDialog.cpp +++ b/launcher/ui/dialogs/SkinUploadDialog.cpp @@ -133,15 +133,15 @@ SkinUploadDialog::SkinUploadDialog(MinecraftAccountPtr acct, QWidget* parent) : ui->setupUi(this); // FIXME: add a model for this, download/refresh the capes on demand - auto& data = *acct->accountData(); + auto& accountData = *acct->accountData(); int index = 0; ui->capeCombo->addItem(tr("No Cape"), QVariant()); - auto currentCape = data.minecraftProfile.currentCape; + auto currentCape = accountData.minecraftProfile.currentCape; if (currentCape.isEmpty()) { ui->capeCombo->setCurrentIndex(index); } - for (auto& cape : data.minecraftProfile.capes) { + for (auto& cape : accountData.minecraftProfile.capes) { index++; if (cape.data.size()) { QPixmap capeImage; diff --git a/launcher/ui/dialogs/SkinUploadDialog.ui b/launcher/ui/dialogs/SkinUploadDialog.ui index c7b166455..c6df92df3 100644 --- a/launcher/ui/dialogs/SkinUploadDialog.ui +++ b/launcher/ui/dialogs/SkinUploadDialog.ui @@ -35,14 +35,8 @@ 0 - - - 28 - 16777215 - - - ... + Browse diff --git a/launcher/ui/dialogs/VersionSelectDialog.cpp b/launcher/ui/dialogs/VersionSelectDialog.cpp index 61254c46f..c61d10578 100644 --- a/launcher/ui/dialogs/VersionSelectDialog.cpp +++ b/launcher/ui/dialogs/VersionSelectDialog.cpp @@ -54,7 +54,7 @@ VersionSelectDialog::VersionSelectDialog(BaseVersionList* vlist, QString title, m_verticalLayout = new QVBoxLayout(this); m_verticalLayout->setObjectName(QStringLiteral("verticalLayout")); - m_versionWidget = new VersionSelectWidget(true, parent); + m_versionWidget = new VersionSelectWidget(parent); m_verticalLayout->addWidget(m_versionWidget); m_horizontalLayout = new QHBoxLayout(); @@ -74,8 +74,9 @@ VersionSelectDialog::VersionSelectDialog(BaseVersionList* vlist, QString title, retranslate(); - QObject::connect(m_buttonBox, SIGNAL(accepted()), this, SLOT(accept())); - QObject::connect(m_buttonBox, SIGNAL(rejected()), this, SLOT(reject())); + connect(m_buttonBox, &QDialogButtonBox::accepted, this, &QDialog::accept); + connect(m_versionWidget->view(), &QAbstractItemView::doubleClicked, this, &QDialog::accept); + connect(m_buttonBox, &QDialogButtonBox::rejected, this, &QDialog::reject); QMetaObject::connectSlotsByName(this); setWindowModality(Qt::WindowModal); @@ -121,6 +122,7 @@ int VersionSelectDialog::exec() { QDialog::open(); m_versionWidget->initialize(m_vlist); + m_versionWidget->selectSearch(); if (resizeOnColumn != -1) { m_versionWidget->setResizeOn(resizeOnColumn); } @@ -147,6 +149,11 @@ void VersionSelectDialog::setExactFilter(BaseVersionList::ModelRoles role, QStri m_versionWidget->setExactFilter(role, filter); } +void VersionSelectDialog::setExactIfPresentFilter(BaseVersionList::ModelRoles role, QString filter) +{ + m_versionWidget->setExactIfPresentFilter(role, filter); +} + void VersionSelectDialog::setFuzzyFilter(BaseVersionList::ModelRoles role, QString filter) { m_versionWidget->setFuzzyFilter(role, filter); diff --git a/launcher/ui/dialogs/VersionSelectDialog.h b/launcher/ui/dialogs/VersionSelectDialog.h index 701020fa9..0ccd45e74 100644 --- a/launcher/ui/dialogs/VersionSelectDialog.h +++ b/launcher/ui/dialogs/VersionSelectDialog.h @@ -46,6 +46,7 @@ class VersionSelectDialog : public QDialog { void setCurrentVersion(const QString& version); void setFuzzyFilter(BaseVersionList::ModelRoles role, QString filter); void setExactFilter(BaseVersionList::ModelRoles role, QString filter); + void setExactIfPresentFilter(BaseVersionList::ModelRoles role, QString filter); void setEmptyString(QString emptyString); void setEmptyErrorString(QString emptyErrorString); void setResizeOn(int column); diff --git a/launcher/ui/instanceview/InstanceDelegate.cpp b/launcher/ui/instanceview/InstanceDelegate.cpp index 915b41d84..d947163bc 100644 --- a/launcher/ui/instanceview/InstanceDelegate.cpp +++ b/launcher/ui/instanceview/InstanceDelegate.cpp @@ -367,7 +367,9 @@ class NoReturnTextEdit : public QTextEdit { void editingDone(); }; -void ListViewDelegate::updateEditorGeometry(QWidget* editor, const QStyleOptionViewItem& option, const QModelIndex& index) const +void ListViewDelegate::updateEditorGeometry(QWidget* editor, + const QStyleOptionViewItem& option, + [[maybe_unused]] const QModelIndex& index) const { const int iconSize = 48; QRect textRect = option.rect; @@ -379,17 +381,17 @@ void ListViewDelegate::updateEditorGeometry(QWidget* editor, const QStyleOptionV void ListViewDelegate::setEditorData(QWidget* editor, const QModelIndex& index) const { auto text = index.data(Qt::EditRole).toString(); - QTextEdit* realeditor = qobject_cast(editor); - realeditor->setAlignment(Qt::AlignHCenter | Qt::AlignTop); - realeditor->append(text); - realeditor->selectAll(); - realeditor->document()->clearUndoRedoStacks(); + QTextEdit* realEditor = qobject_cast(editor); + realEditor->setAlignment(Qt::AlignHCenter | Qt::AlignTop); + realEditor->append(text); + realEditor->selectAll(); + realEditor->document()->clearUndoRedoStacks(); } void ListViewDelegate::setModelData(QWidget* editor, QAbstractItemModel* model, const QModelIndex& index) const { - QTextEdit* realeditor = qobject_cast(editor); - QString text = realeditor->toPlainText(); + QTextEdit* realEditor = qobject_cast(editor); + QString text = realEditor->toPlainText(); text.replace(QChar('\n'), QChar(' ')); text = text.trimmed(); // Prevent instance names longer than 128 chars @@ -399,7 +401,9 @@ void ListViewDelegate::setModelData(QWidget* editor, QAbstractItemModel* model, } } -QWidget* ListViewDelegate::createEditor(QWidget* parent, const QStyleOptionViewItem& option, const QModelIndex& index) const +QWidget* ListViewDelegate::createEditor(QWidget* parent, + [[maybe_unused]] const QStyleOptionViewItem& option, + [[maybe_unused]] const QModelIndex& index) const { auto editor = new NoReturnTextEdit(parent); connect(editor, &NoReturnTextEdit::editingDone, this, &ListViewDelegate::editingDone); diff --git a/launcher/ui/instanceview/InstanceView.cpp b/launcher/ui/instanceview/InstanceView.cpp index a9442546a..690dc917b 100644 --- a/launcher/ui/instanceview/InstanceView.cpp +++ b/launcher/ui/instanceview/InstanceView.cpp @@ -47,8 +47,8 @@ #include #include -#include #include "VisualGroup.h" +#include "ui/themes/ThemeManager.h" #include #include @@ -86,16 +86,18 @@ void InstanceView::setModel(QAbstractItemModel* model) connect(model, &QAbstractItemModel::rowsRemoved, this, &InstanceView::rowsRemoved); } -void InstanceView::dataChanged(const QModelIndex& topLeft, const QModelIndex& bottomRight, const QVector& roles) +void InstanceView::dataChanged([[maybe_unused]] const QModelIndex& topLeft, + [[maybe_unused]] const QModelIndex& bottomRight, + [[maybe_unused]] const QVector& roles) { scheduleDelayedItemsLayout(); } -void InstanceView::rowsInserted(const QModelIndex& parent, int start, int end) +void InstanceView::rowsInserted([[maybe_unused]] const QModelIndex& parent, [[maybe_unused]] int start, [[maybe_unused]] int end) { scheduleDelayedItemsLayout(); } -void InstanceView::rowsAboutToBeRemoved(const QModelIndex& parent, int start, int end) +void InstanceView::rowsAboutToBeRemoved([[maybe_unused]] const QModelIndex& parent, [[maybe_unused]] int start, [[maybe_unused]] int end) { scheduleDelayedItemsLayout(); } @@ -239,9 +241,9 @@ QString InstanceView::groupNameAt(const QPoint& point) { executeDelayedItemsLayout(); - VisualGroup::HitResults hitresult; - auto group = categoryAt(point + offset(), hitresult); - if (group && (hitresult & (VisualGroup::HeaderHit | VisualGroup::BodyHit))) { + VisualGroup::HitResults hitResult; + auto group = categoryAt(point + offset(), hitResult); + if (group && (hitResult & (VisualGroup::HeaderHit | VisualGroup::BodyHit))) { return group->text; } return QString(); @@ -275,9 +277,9 @@ void InstanceView::mousePressEvent(QMouseEvent* event) m_pressedAlreadySelected = selectionModel()->isSelected(m_pressedIndex); m_pressedPosition = geometryPos; - VisualGroup::HitResults hitresult; - m_pressedCategory = categoryAt(geometryPos, hitresult); - if (m_pressedCategory && hitresult & VisualGroup::CheckboxHit) { + VisualGroup::HitResults hitResult; + m_pressedCategory = categoryAt(geometryPos, hitResult); + if (m_pressedCategory && hitResult & VisualGroup::CheckboxHit) { setState(m_pressedCategory->collapsed ? ExpandingState : CollapsingState); event->accept(); return; @@ -361,10 +363,10 @@ void InstanceView::mouseReleaseEvent(QMouseEvent* event) QPoint geometryPos = event->pos() + offset(); QPersistentModelIndex index = indexAt(visualPos); - VisualGroup::HitResults hitresult; + VisualGroup::HitResults hitResult; bool click = - (index == m_pressedIndex && index.isValid()) || (m_pressedCategory && m_pressedCategory == categoryAt(geometryPos, hitresult)); + (index == m_pressedIndex && index.isValid()) || (m_pressedCategory && m_pressedCategory == categoryAt(geometryPos, hitResult)); if (click && m_pressedCategory) { if (state() == ExpandingState) { @@ -444,12 +446,12 @@ void InstanceView::setPaintCat(bool visible) { m_catVisible = visible; if (visible) - m_catPixmap.load(APPLICATION->getCatPack()); + m_catPixmap.load(APPLICATION->themeManager()->getCatPack()); else m_catPixmap = QPixmap(); } -void InstanceView::paintEvent(QPaintEvent* event) +void InstanceView::paintEvent([[maybe_unused]] QPaintEvent* event) { executeDelayedItemsLayout(); @@ -545,7 +547,7 @@ void InstanceView::paintEvent(QPaintEvent* event) #endif } -void InstanceView::resizeEvent(QResizeEvent* event) +void InstanceView::resizeEvent([[maybe_unused]] QResizeEvent* event) { int newItemsPerRow = calculateItemsPerRow(); if (newItemsPerRow != m_currentItemsPerRow) { @@ -581,7 +583,7 @@ void InstanceView::dragMoveEvent(QDragMoveEvent* event) event->accept(); } -void InstanceView::dragLeaveEvent(QDragLeaveEvent* event) +void InstanceView::dragLeaveEvent([[maybe_unused]] QDragLeaveEvent* event) { executeDelayedItemsLayout(); @@ -604,9 +606,9 @@ void InstanceView::dropEvent(QDropEvent* event) if (event->possibleActions() & Qt::MoveAction) { std::pair dropPos = rowDropPos(event->pos()); const VisualGroup* group = dropPos.first; - auto hitresult = dropPos.second; + auto hitResult = dropPos.second; - if (hitresult == VisualGroup::HitResult::NoHit) { + if (hitResult == VisualGroup::HitResult::NoHit) { viewport()->update(); return; } @@ -643,15 +645,15 @@ void InstanceView::startDrag(Qt::DropActions supportedActions) if (indexes.count() == 0) return; - QMimeData* data = model()->mimeData(indexes); - if (!data) { + QMimeData* mimeData = model()->mimeData(indexes); + if (!mimeData) { return; } QRect rect; QPixmap pixmap = renderToPixmap(indexes, &rect); QDrag* drag = new QDrag(this); drag->setPixmap(pixmap); - drag->setMimeData(data); + drag->setMimeData(mimeData); drag->setHotSpot(m_pressedPosition - rect.topLeft()); Qt::DropAction defaultDropAction = Qt::IgnoreAction; if (this->defaultDropAction() != Qt::IgnoreAction && (supportedActions & this->defaultDropAction())) { @@ -767,16 +769,16 @@ QList> InstanceView::draggablePaintPairs(const QMo return ret; } -bool InstanceView::isDragEventAccepted(QDropEvent* event) +bool InstanceView::isDragEventAccepted([[maybe_unused]] QDropEvent* event) { return true; } std::pair InstanceView::rowDropPos(const QPoint& pos) { - VisualGroup::HitResults hitresult; - auto group = categoryAt(pos + offset(), hitresult); - return std::make_pair(group, hitresult); + VisualGroup::HitResults hitResult; + auto group = categoryAt(pos + offset(), hitResult); + return std::make_pair(group, hitResult); } QPoint InstanceView::offset() const @@ -802,7 +804,7 @@ QRegion InstanceView::visualRegionForSelection(const QItemSelection& selection) return region; } -QModelIndex InstanceView::moveCursor(QAbstractItemView::CursorAction cursorAction, Qt::KeyboardModifiers modifiers) +QModelIndex InstanceView::moveCursor(QAbstractItemView::CursorAction cursorAction, [[maybe_unused]] Qt::KeyboardModifiers modifiers) { auto current = currentIndex(); if (!current.isValid()) { @@ -822,20 +824,20 @@ QModelIndex InstanceView::moveCursor(QAbstractItemView::CursorAction cursorActio switch (cursorAction) { case MoveUp: { if (row == 0) { - int prevgroupindex = group_index - 1; - while (prevgroupindex >= 0) { - auto prevgroup = m_groups[prevgroupindex]; - if (prevgroup->collapsed) { - prevgroupindex--; + int prevGroupIndex = group_index - 1; + while (prevGroupIndex >= 0) { + auto prevGroup = m_groups[prevGroupIndex]; + if (prevGroup->collapsed) { + prevGroupIndex--; continue; } - int newRow = prevgroup->numRows() - 1; - int newRowSize = prevgroup->rows[newRow].size(); + int newRow = prevGroup->numRows() - 1; + int newRowSize = prevGroup->rows[newRow].size(); int newColumn = m_currentCursorColumn; if (m_currentCursorColumn >= newRowSize) { newColumn = newRowSize - 1; } - return prevgroup->rows[newRow][newColumn]; + return prevGroup->rows[newRow][newColumn]; } } else { int newRow = row - 1; @@ -850,19 +852,19 @@ QModelIndex InstanceView::moveCursor(QAbstractItemView::CursorAction cursorActio } case MoveDown: { if (row == cat->rows.size() - 1) { - int nextgroupindex = group_index + 1; - while (nextgroupindex < m_groups.size()) { - auto nextgroup = m_groups[nextgroupindex]; - if (nextgroup->collapsed) { - nextgroupindex++; + int nextGroupIndex = group_index + 1; + while (nextGroupIndex < m_groups.size()) { + auto nextGroup = m_groups[nextGroupIndex]; + if (nextGroup->collapsed) { + nextGroupIndex++; continue; } - int newRowSize = nextgroup->rows[0].size(); + int newRowSize = nextGroup->rows[0].size(); int newColumn = m_currentCursorColumn; if (m_currentCursorColumn >= newRowSize) { newColumn = newRowSize - 1; } - return nextgroup->rows[0][newColumn]; + return nextGroup->rows[0][newColumn]; } } else { int newRow = row + 1; @@ -936,7 +938,7 @@ void InstanceView::scrollTo(const QModelIndex& index, ScrollHint hint) verticalScrollBar()->setValue(verticalScrollToValue(index, rect, hint)); } -int InstanceView::verticalScrollToValue(const QModelIndex& index, const QRect& rect, QListView::ScrollHint hint) const +int InstanceView::verticalScrollToValue([[maybe_unused]] const QModelIndex& index, const QRect& rect, QListView::ScrollHint hint) const { const QRect area = viewport()->rect(); const bool above = (hint == QListView::EnsureVisible && rect.top() < area.top()); diff --git a/launcher/ui/pages/BasePageContainer.h b/launcher/ui/pages/BasePageContainer.h index b750e8272..a497ef7b3 100644 --- a/launcher/ui/pages/BasePageContainer.h +++ b/launcher/ui/pages/BasePageContainer.h @@ -6,6 +6,7 @@ class BasePageContainer { public: virtual ~BasePageContainer(){}; virtual bool selectPage(QString pageId) = 0; + virtual BasePage* selectedPage() const = 0; virtual BasePage* getPage(QString pageId) { return nullptr; }; virtual void refreshContainer() = 0; virtual bool requestClose() = 0; diff --git a/launcher/ui/pages/global/AccountListPage.cpp b/launcher/ui/pages/global/AccountListPage.cpp index 1d5ecb8d1..c95bfabdd 100644 --- a/launcher/ui/pages/global/AccountListPage.cpp +++ b/launcher/ui/pages/global/AccountListPage.cpp @@ -64,7 +64,8 @@ AccountListPage::AccountListPage(QWidget* parent) : QMainWindow(parent), ui(new ui->setupUi(this); ui->listView->setEmptyString( tr("Welcome!\n" - "If you're new here, you can click the \"Add\" button to add your Mojang or Minecraft account.")); + "If you're new here, you can select the \"Add Microsoft\" or \"Add Mojang\" buttons to link your Microsoft and/or Mojang " + "accounts.")); ui->listView->setEmptyMode(VersionListView::String); ui->listView->setContextMenuPolicy(Qt::CustomContextMenu); @@ -83,8 +84,10 @@ AccountListPage::AccountListPage(QWidget* parent) : QMainWindow(parent), ui(new QItemSelectionModel* selectionModel = ui->listView->selectionModel(); connect(selectionModel, &QItemSelectionModel::selectionChanged, - [this](const QItemSelection& sel, const QItemSelection& dsel) { updateButtonStates(); }); + [this]([[maybe_unused]] const QItemSelection& sel, [[maybe_unused]] const QItemSelection& dsel) { updateButtonStates(); }); connect(ui->listView, &VersionListView::customContextMenuRequested, this, &AccountListPage::ShowContextMenu); + connect(ui->listView, &VersionListView::activated, this, + [this](const QModelIndex& index) { m_accounts->setDefaultAccount(m_accounts->at(index.row())); }); connect(m_accounts.get(), &AccountList::listChanged, this, &AccountListPage::listChanged); connect(m_accounts.get(), &AccountList::listActivityChanged, this, &AccountListPage::listChanged); diff --git a/launcher/ui/pages/global/ExternalToolsPage.ui b/launcher/ui/pages/global/ExternalToolsPage.ui index 3643094df..47c77842a 100644 --- a/launcher/ui/pages/global/ExternalToolsPage.ui +++ b/launcher/ui/pages/global/ExternalToolsPage.ui @@ -47,7 +47,7 @@ - ... + Browse @@ -84,7 +84,7 @@ - ... + Browse @@ -121,7 +121,7 @@ - ... + Browse @@ -166,7 +166,7 @@ - ... + Browse diff --git a/launcher/ui/pages/global/JavaPage.cpp b/launcher/ui/pages/global/JavaPage.cpp index a9ede8ed2..ac50319ec 100644 --- a/launcher/ui/pages/global/JavaPage.cpp +++ b/launcher/ui/pages/global/JavaPage.cpp @@ -166,7 +166,7 @@ void JavaPage::on_javaTestBtn_clicked() checker->run(); } -void JavaPage::on_maxMemSpinBox_valueChanged(int i) +void JavaPage::on_maxMemSpinBox_valueChanged([[maybe_unused]] int i) { updateThresholds(); } @@ -185,6 +185,7 @@ void JavaPage::updateThresholds() { auto sysMiB = Sys::getSystemRam() / Sys::mebibyte; unsigned int maxMem = ui->maxMemSpinBox->value(); + unsigned int minMem = ui->minMemSpinBox->value(); QString iconName; @@ -194,6 +195,9 @@ void JavaPage::updateThresholds() } else if (maxMem > (sysMiB * 0.9)) { iconName = "status-yellow"; ui->labelMaxMemIcon->setToolTip(tr("Your maximum memory allocation approaches your system memory capacity.")); + } else if (maxMem < minMem) { + iconName = "status-yellow"; + ui->labelMaxMemIcon->setToolTip(tr("Your maximum memory allocation is smaller than the minimum value")); } else { iconName = "status-good"; ui->labelMaxMemIcon->setToolTip(""); diff --git a/launcher/ui/pages/global/JavaPage.ui b/launcher/ui/pages/global/JavaPage.ui index 561cf79b7..5a547637c 100644 --- a/launcher/ui/pages/global/JavaPage.ui +++ b/launcher/ui/pages/global/JavaPage.ui @@ -160,117 +160,7 @@ Java Runtime - - - - - 0 - 0 - - - - &Auto-detect... - - - - - - - - 0 - 0 - - - - JVM arguments: - - - Qt::AlignLeading|Qt::AlignLeft|Qt::AlignVCenter - - - - - - - - 0 - 0 - - - - &Java path: - - - javaPathTextBox - - - - - - - - 0 - 0 - - - - &Test - - - - - - - - - - - - - 0 - 0 - - - - - 28 - 16777215 - - - - ... - - - - - - - - - - 0 - 0 - - - - If enabled, the launcher will not check if an instance is compatible with the selected Java version. - - - &Skip Java compatibility checks - - - - - - - If enabled, the launcher will not prompt you to choose a Java version if one isn't found. - - - Skip Java &Wizard - - - - + true @@ -289,6 +179,114 @@ + + + + + 0 + 0 + + + + If enabled, the launcher will not check if an instance is compatible with the selected Java version. + + + &Skip Java compatibility checks + + + + + + + + + + 0 + 0 + + + + &Auto-detect... + + + + + + + + 0 + 0 + + + + &Test + + + + + + + + + + 0 + 0 + + + + JVM arguments: + + + Qt::AlignLeading|Qt::AlignLeft|Qt::AlignVCenter + + + + + + + + + + 0 + 0 + + + + &Java path: + + + javaPathTextBox + + + + + + + + + + + 0 + 0 + + + + Browse + + + + + + + + + If enabled, the launcher will not prompt you to choose a Java version if one isn't found. + + + Skip Java &Wizard + + + @@ -317,8 +315,6 @@ permGenSpinBox javaBrowseBtn javaPathTextBox - javaDetectBtn - javaTestBtn tabWidget diff --git a/launcher/ui/pages/global/LauncherPage.ui b/launcher/ui/pages/global/LauncherPage.ui index 26408f44f..bc259a9b8 100644 --- a/launcher/ui/pages/global/LauncherPage.ui +++ b/launcher/ui/pages/global/LauncherPage.ui @@ -99,7 +99,7 @@ - ... + Browse @@ -109,7 +109,7 @@ - ... + Browse @@ -126,14 +126,14 @@ - ... + Browse - ... + Browse diff --git a/launcher/ui/pages/global/MinecraftPage.cpp b/launcher/ui/pages/global/MinecraftPage.cpp index 866a4121c..553cefd83 100644 --- a/launcher/ui/pages/global/MinecraftPage.cpp +++ b/launcher/ui/pages/global/MinecraftPage.cpp @@ -2,7 +2,6 @@ /* * Prism Launcher - Minecraft Launcher * Copyright (c) 2022 Jamie Mansfield - * Copyright (C) 2023 seth * * 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 @@ -35,6 +34,7 @@ */ #include "MinecraftPage.h" +#include "BuildConfig.h" #include "ui_MinecraftPage.h" #include @@ -44,9 +44,15 @@ #include "Application.h" #include "settings/SettingsObject.h" +#ifdef Q_OS_LINUX +#include "MangoHud.h" +#endif + MinecraftPage::MinecraftPage(QWidget* parent) : QWidget(parent), ui(new Ui::MinecraftPage) { ui->setupUi(this); + connect(ui->useNativeGLFWCheck, &QAbstractButton::toggled, this, &MinecraftPage::onUseNativeGLFWChanged); + connect(ui->useNativeOpenALCheck, &QAbstractButton::toggled, this, &MinecraftPage::onUseNativeOpenALChanged); loadSettings(); updateCheckboxStuff(); } @@ -74,6 +80,16 @@ void MinecraftPage::on_maximizedCheckBox_clicked(bool checked) updateCheckboxStuff(); } +void MinecraftPage::onUseNativeGLFWChanged(bool checked) +{ + ui->lineEditGLFWPath->setEnabled(checked); +} + +void MinecraftPage::onUseNativeOpenALChanged(bool checked) +{ + ui->lineEditOpenALPath->setEnabled(checked); +} + void MinecraftPage::applySettings() { auto s = APPLICATION->settings(); @@ -84,8 +100,10 @@ void MinecraftPage::applySettings() s->set("MinecraftWinHeight", ui->windowHeightSpinBox->value()); // Native library workarounds - s->set("UseNativeOpenAL", ui->useNativeOpenALCheck->isChecked()); s->set("UseNativeGLFW", ui->useNativeGLFWCheck->isChecked()); + s->set("CustomGLFWPath", ui->lineEditGLFWPath->text()); + s->set("UseNativeOpenAL", ui->useNativeOpenALCheck->isChecked()); + s->set("CustomOpenALPath", ui->lineEditOpenALPath->text()); // Peformance related options s->set("EnableFeralGamemode", ui->enableFeralGamemodeCheck->isChecked()); @@ -96,13 +114,11 @@ void MinecraftPage::applySettings() s->set("ShowGameTime", ui->showGameTime->isChecked()); s->set("ShowGlobalGameTime", ui->showGlobalGameTime->isChecked()); s->set("RecordGameTime", ui->recordGameTime->isChecked()); + s->set("ShowGameTimeWithoutDays", ui->showGameTimeWithoutDays->isChecked()); // Miscellaneous s->set("CloseAfterLaunch", ui->closeAfterLaunchCheck->isChecked()); s->set("QuitAfterGameStop", ui->quitAfterGameStopCheck->isChecked()); - - // Mod loader settings - s->set("DisableQuiltBeacon", ui->disableQuiltBeaconCheckBox->isChecked()); } void MinecraftPage::loadSettings() @@ -114,8 +130,20 @@ void MinecraftPage::loadSettings() ui->windowWidthSpinBox->setValue(s->get("MinecraftWinWidth").toInt()); ui->windowHeightSpinBox->setValue(s->get("MinecraftWinHeight").toInt()); - ui->useNativeOpenALCheck->setChecked(s->get("UseNativeOpenAL").toBool()); ui->useNativeGLFWCheck->setChecked(s->get("UseNativeGLFW").toBool()); + ui->lineEditGLFWPath->setText(s->get("CustomGLFWPath").toString()); + ui->lineEditGLFWPath->setPlaceholderText(tr("Path to %1 library file").arg(BuildConfig.GLFW_LIBRARY_NAME)); +#ifdef Q_OS_LINUX + if (!APPLICATION->m_detectedGLFWPath.isEmpty()) + ui->lineEditGLFWPath->setPlaceholderText(tr("Auto detected path: %1").arg(APPLICATION->m_detectedGLFWPath)); +#endif + ui->useNativeOpenALCheck->setChecked(s->get("UseNativeOpenAL").toBool()); + ui->lineEditOpenALPath->setText(s->get("CustomOpenALPath").toString()); + ui->lineEditOpenALPath->setPlaceholderText(tr("Path to %1 library file").arg(BuildConfig.OPENAL_LIBRARY_NAME)); +#ifdef Q_OS_LINUX + if (!APPLICATION->m_detectedOpenALPath.isEmpty()) + ui->lineEditOpenALPath->setPlaceholderText(tr("Auto detected path: %1").arg(APPLICATION->m_detectedOpenALPath)); +#endif ui->enableFeralGamemodeCheck->setChecked(s->get("EnableFeralGamemode").toBool()); ui->enableMangoHud->setChecked(s->get("EnableMangoHud").toBool()); @@ -138,11 +166,10 @@ void MinecraftPage::loadSettings() ui->showGameTime->setChecked(s->get("ShowGameTime").toBool()); ui->showGlobalGameTime->setChecked(s->get("ShowGlobalGameTime").toBool()); ui->recordGameTime->setChecked(s->get("RecordGameTime").toBool()); + ui->showGameTimeWithoutDays->setChecked(s->get("ShowGameTimeWithoutDays").toBool()); ui->closeAfterLaunchCheck->setChecked(s->get("CloseAfterLaunch").toBool()); ui->quitAfterGameStopCheck->setChecked(s->get("QuitAfterGameStop").toBool()); - - ui->disableQuiltBeaconCheckBox->setChecked(s->get("DisableQuiltBeacon").toBool()); } void MinecraftPage::retranslate() diff --git a/launcher/ui/pages/global/MinecraftPage.h b/launcher/ui/pages/global/MinecraftPage.h index 28c31b5d8..5facfbb3f 100644 --- a/launcher/ui/pages/global/MinecraftPage.h +++ b/launcher/ui/pages/global/MinecraftPage.h @@ -70,6 +70,9 @@ class MinecraftPage : public QWidget, public BasePage { private slots: void on_maximizedCheckBox_clicked(bool checked); + void onUseNativeGLFWChanged(bool checked); + void onUseNativeOpenALChanged(bool checked); + private: Ui::MinecraftPage* ui; }; diff --git a/launcher/ui/pages/global/MinecraftPage.ui b/launcher/ui/pages/global/MinecraftPage.ui index 393b0f358..b5cfa659f 100644 --- a/launcher/ui/pages/global/MinecraftPage.ui +++ b/launcher/ui/pages/global/MinecraftPage.ui @@ -138,6 +138,13 @@ + + + + Show time spent playing in hours + + + @@ -190,45 +197,60 @@ Tweaks - - - - Mod loader settings - - - - - - Disable Quilt Loader Beacon - - - Disable Quilt loader's beacon for counting monthly active users - - - - - - Native library workarounds - - + + Use system installation of &GLFW - + + + + &GLFW library path + + + lineEditGLFWPath + + + + Use system installation of &OpenAL + + + + &OpenAL library path + + + lineEditOpenALPath + + + + + + + false + + + + + + + false + + + diff --git a/launcher/ui/pages/global/ProxyPage.cpp b/launcher/ui/pages/global/ProxyPage.cpp index 19b2bcea1..9caffcb37 100644 --- a/launcher/ui/pages/global/ProxyPage.cpp +++ b/launcher/ui/pages/global/ProxyPage.cpp @@ -71,7 +71,7 @@ void ProxyPage::updateCheckboxStuff() ui->proxyAuthBox->setEnabled(enableEditing); } -void ProxyPage::proxyGroupChanged(QAbstractButton* button) +void ProxyPage::proxyGroupChanged([[maybe_unused]] QAbstractButton* button) { updateCheckboxStuff(); } diff --git a/launcher/ui/pages/instance/ExternalResourcesPage.cpp b/launcher/ui/pages/instance/ExternalResourcesPage.cpp index 12038f88f..1a8fafa9b 100644 --- a/launcher/ui/pages/instance/ExternalResourcesPage.cpp +++ b/launcher/ui/pages/instance/ExternalResourcesPage.cpp @@ -152,6 +152,7 @@ void ExternalResourcesPage::retranslate() void ExternalResourcesPage::itemActivated(const QModelIndex&) { auto selection = m_filterModel->mapSelectionToSource(ui->treeView->selectionModel()->selection()); + m_model->setResourceEnabled(selection.indexes(), EnableAction::TOGGLE); } void ExternalResourcesPage::filterTextChanged(const QString& newContents) @@ -305,7 +306,7 @@ bool ExternalResourcesPage::current(const QModelIndex& current, const QModelInde return onSelectionChanged(current, previous); } -bool ExternalResourcesPage::onSelectionChanged(const QModelIndex& current, const QModelIndex& previous) +bool ExternalResourcesPage::onSelectionChanged(const QModelIndex& current, [[maybe_unused]] const QModelIndex& previous) { auto sourceCurrent = m_filterModel->mapToSource(current); int row = sourceCurrent.row(); diff --git a/launcher/ui/pages/instance/ExternalResourcesPage.ui b/launcher/ui/pages/instance/ExternalResourcesPage.ui index 3c8366917..ba703f77d 100644 --- a/launcher/ui/pages/instance/ExternalResourcesPage.ui +++ b/launcher/ui/pages/instance/ExternalResourcesPage.ui @@ -168,6 +168,17 @@ Go to mods home page + + + false + + + Remove metadata + + + Remove mod's metadata + + diff --git a/launcher/ui/pages/instance/InstanceSettingsPage.cpp b/launcher/ui/pages/instance/InstanceSettingsPage.cpp index 687b82d73..7aa6bd322 100644 --- a/launcher/ui/pages/instance/InstanceSettingsPage.cpp +++ b/launcher/ui/pages/instance/InstanceSettingsPage.cpp @@ -3,7 +3,6 @@ * Prism Launcher - Minecraft Launcher * Copyright (c) 2022 Jamie Mansfield * Copyright (C) 2022 Sefa Eyeoglu - * Copyright (C) 2023 seth * * 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 @@ -48,6 +47,7 @@ #include "ui/widgets/CustomCommands.h" #include "Application.h" +#include "BuildConfig.h" #include "JavaCommon.h" #include "minecraft/auth/AccountList.h" @@ -66,6 +66,10 @@ InstanceSettingsPage::InstanceSettingsPage(BaseInstance* inst, QWidget* parent) connect(APPLICATION, &Application::globalSettingsClosed, this, &InstanceSettingsPage::loadSettings); connect(ui->instanceAccountSelector, QOverload::of(&QComboBox::currentIndexChanged), this, &InstanceSettingsPage::changeInstanceAccount); + + connect(ui->useNativeGLFWCheck, &QAbstractButton::toggled, this, &InstanceSettingsPage::onUseNativeGLFWChanged); + connect(ui->useNativeOpenALCheck, &QAbstractButton::toggled, this, &InstanceSettingsPage::onUseNativeOpenALChanged); + loadSettings(); updateThresholds(); @@ -198,11 +202,15 @@ void InstanceSettingsPage::applySettings() bool workarounds = ui->nativeWorkaroundsGroupBox->isChecked(); m_settings->set("OverrideNativeWorkarounds", workarounds); if (workarounds) { - m_settings->set("UseNativeOpenAL", ui->useNativeOpenALCheck->isChecked()); m_settings->set("UseNativeGLFW", ui->useNativeGLFWCheck->isChecked()); + m_settings->set("CustomGLFWPath", ui->lineEditGLFWPath->text()); + m_settings->set("UseNativeOpenAL", ui->useNativeOpenALCheck->isChecked()); + m_settings->set("CustomOpenALPath", ui->lineEditOpenALPath->text()); } else { - m_settings->reset("UseNativeOpenAL"); m_settings->reset("UseNativeGLFW"); + m_settings->reset("CustomGLFWPath"); + m_settings->reset("UseNativeOpenAL"); + m_settings->reset("CustomOpenALPath"); } // Performance @@ -245,14 +253,6 @@ void InstanceSettingsPage::applySettings() m_settings->reset("InstanceAccountId"); } - bool overrideModLoaderSettings = ui->modLoaderSettingsGroupBox->isChecked(); - m_settings->set("OverrideModLoaderSettings", overrideModLoaderSettings); - if (overrideModLoaderSettings) { - m_settings->set("DisableQuiltBeacon", ui->disableQuiltBeaconCheckBox->isChecked()); - } else { - m_settings->reset("DisableQuiltBeacon"); - } - // FIXME: This should probably be called by a signal instead m_instance->updateRuntimeContext(); } @@ -312,7 +312,19 @@ void InstanceSettingsPage::loadSettings() // Workarounds ui->nativeWorkaroundsGroupBox->setChecked(m_settings->get("OverrideNativeWorkarounds").toBool()); ui->useNativeGLFWCheck->setChecked(m_settings->get("UseNativeGLFW").toBool()); + ui->lineEditGLFWPath->setText(m_settings->get("CustomGLFWPath").toString()); +#ifdef Q_OS_LINUX + ui->lineEditGLFWPath->setPlaceholderText(APPLICATION->m_detectedGLFWPath); +#else + ui->lineEditGLFWPath->setPlaceholderText(tr("Path to %1 library file").arg(BuildConfig.GLFW_LIBRARY_NAME)); +#endif ui->useNativeOpenALCheck->setChecked(m_settings->get("UseNativeOpenAL").toBool()); + ui->lineEditOpenALPath->setText(m_settings->get("CustomOpenALPath").toString()); +#ifdef Q_OS_LINUX + ui->lineEditOpenALPath->setPlaceholderText(APPLICATION->m_detectedOpenALPath); +#else + ui->lineEditGLFWPath->setPlaceholderText(tr("Path to %1 library file").arg(BuildConfig.OPENAL_LIBRARY_NAME)); +#endif // Performance ui->perfomanceGroupBox->setChecked(m_settings->get("OverridePerformance").toBool()); @@ -344,10 +356,6 @@ void InstanceSettingsPage::loadSettings() ui->instanceAccountGroupBox->setChecked(m_settings->get("UseAccountForInstance").toBool()); updateAccountsMenu(); - - // Mod loader specific settings - ui->modLoaderSettingsGroupBox->setChecked(m_settings->get("OverrideModLoaderSettings").toBool()); - ui->disableQuiltBeaconCheckBox->setChecked(m_settings->get("DisableQuiltBeacon").toBool()); } void InstanceSettingsPage::on_javaDetectBtn_clicked() @@ -408,6 +416,16 @@ void InstanceSettingsPage::on_javaTestBtn_clicked() checker->run(); } +void InstanceSettingsPage::onUseNativeGLFWChanged(bool checked) +{ + ui->lineEditGLFWPath->setEnabled(checked); +} + +void InstanceSettingsPage::onUseNativeOpenALChanged(bool checked) +{ + ui->lineEditOpenALPath->setEnabled(checked); +} + void InstanceSettingsPage::updateAccountsMenu() { ui->instanceAccountSelector->clear(); @@ -440,7 +458,7 @@ void InstanceSettingsPage::changeInstanceAccount(int index) } } -void InstanceSettingsPage::on_maxMemSpinBox_valueChanged(int i) +void InstanceSettingsPage::on_maxMemSpinBox_valueChanged([[maybe_unused]] int i) { updateThresholds(); } @@ -460,6 +478,7 @@ void InstanceSettingsPage::updateThresholds() { auto sysMiB = Sys::getSystemRam() / Sys::mebibyte; unsigned int maxMem = ui->maxMemSpinBox->value(); + unsigned int minMem = ui->minMemSpinBox->value(); QString iconName; @@ -469,6 +488,9 @@ void InstanceSettingsPage::updateThresholds() } else if (maxMem > (sysMiB * 0.9)) { iconName = "status-yellow"; ui->labelMaxMemIcon->setToolTip(tr("Your maximum memory allocation approaches your system memory capacity.")); + } else if (maxMem < minMem) { + iconName = "status-yellow"; + ui->labelMaxMemIcon->setToolTip(tr("Your maximum memory allocation is smaller than the minimum value")); } else { iconName = "status-good"; ui->labelMaxMemIcon->setToolTip(""); diff --git a/launcher/ui/pages/instance/InstanceSettingsPage.h b/launcher/ui/pages/instance/InstanceSettingsPage.h index 21ecbaf8e..8b78dcb7f 100644 --- a/launcher/ui/pages/instance/InstanceSettingsPage.h +++ b/launcher/ui/pages/instance/InstanceSettingsPage.h @@ -71,6 +71,9 @@ class InstanceSettingsPage : public QWidget, public BasePage { void on_javaBrowseBtn_clicked(); void on_maxMemSpinBox_valueChanged(int i); + void onUseNativeGLFWChanged(bool checked); + void onUseNativeOpenALChanged(bool checked); + void applySettings(); void loadSettings(); diff --git a/launcher/ui/pages/instance/InstanceSettingsPage.ui b/launcher/ui/pages/instance/InstanceSettingsPage.ui index 245433fe8..81cf7093e 100644 --- a/launcher/ui/pages/instance/InstanceSettingsPage.ui +++ b/launcher/ui/pages/instance/InstanceSettingsPage.ui @@ -61,31 +61,7 @@ false - - - - - - - Auto-detect... - - - - - - - Browse... - - - - - - - Test - - - - + If enabled, the launcher will not check if an instance is compatible with the selected Java version. @@ -95,6 +71,38 @@ + + + + + + + + + Browse + + + + + + + + + + + Auto-detect... + + + + + + + Test + + + + + @@ -435,18 +443,52 @@ false - - + + + + + Use system installation of OpenAL + + + + + + + &GLFW library path + + + lineEditGLFWPath + + + + Use system installation of GLFW - - + + + + false + + + + + - Use system installation of OpenAL + &OpenAL library path + + + lineEditOpenALPath + + + + + + + false @@ -541,31 +583,6 @@ Miscellaneous - - - - true - - - false - - - Mod loader settings - - - - - - Disable Quilt Loader Beacon - - - Disable Quilt loader's beacon for counting monthly active users - - - - - - @@ -699,10 +716,6 @@ openGlobalJavaSettingsButton settingsTabs javaSettingsGroupBox - javaPathTextBox - javaDetectBtn - javaBrowseBtn - javaTestBtn memoryGroupBox minMemSpinBox maxMemSpinBox diff --git a/launcher/ui/pages/instance/ManagedPackPage.cpp b/launcher/ui/pages/instance/ManagedPackPage.cpp index 0443baf19..8fdaf065d 100644 --- a/launcher/ui/pages/instance/ManagedPackPage.cpp +++ b/launcher/ui/pages/instance/ManagedPackPage.cpp @@ -26,6 +26,8 @@ #include "ui/dialogs/CustomMessageBox.h" #include "ui/dialogs/ProgressDialog.h" +#include "net/ApiDownload.h" + /** This is just to override the combo box popup behavior so that the combo box doesn't take the whole screen. * ... thanks Qt. */ @@ -242,7 +244,7 @@ void ModrinthManagedPackPage::parseManagedPack() QString id = m_inst->getManagedPackID(); m_fetch_job->addNetAction( - Net::Download::makeByteArray(QString("%1/project/%2/version").arg(BuildConfig.MODRINTH_PROD_URL, id), response)); + Net::ApiDownload::makeByteArray(QString("%1/project/%2/version").arg(BuildConfig.MODRINTH_PROD_URL, id), response)); QObject::connect(m_fetch_job.get(), &NetJob::succeeded, this, [this, response, id] { QJsonParseError parse_error{}; @@ -392,7 +394,7 @@ void FlameManagedPackPage::parseManagedPack() QString id = m_inst->getManagedPackID(); - m_fetch_job->addNetAction(Net::Download::makeByteArray(QString("%1/mods/%2/files").arg(BuildConfig.FLAME_BASE_URL, id), response)); + m_fetch_job->addNetAction(Net::ApiDownload::makeByteArray(QString("%1/mods/%2/files").arg(BuildConfig.FLAME_BASE_URL, id), response)); QObject::connect(m_fetch_job.get(), &NetJob::succeeded, this, [this, response, id] { QJsonParseError parse_error{}; diff --git a/launcher/ui/pages/instance/ModFolderPage.cpp b/launcher/ui/pages/instance/ModFolderPage.cpp index cef292bd9..b42bbf466 100644 --- a/launcher/ui/pages/instance/ModFolderPage.cpp +++ b/launcher/ui/pages/instance/ModFolderPage.cpp @@ -92,6 +92,10 @@ ModFolderPage::ModFolderPage(BaseInstance* inst, std::shared_ptr ui->actionsToolbar->addAction(ui->actionVisitItemPage); connect(ui->actionVisitItemPage, &QAction::triggered, this, &ModFolderPage::visitModPages); + ui->actionRemoveItemMetadata->setToolTip(tr("Remove mod's metadata")); + ui->actionsToolbar->insertActionAfter(ui->actionRemoveItem, ui->actionRemoveItemMetadata); + connect(ui->actionRemoveItemMetadata, &QAction::triggered, this, &ModFolderPage::deleteModMetadata); + auto check_allow_update = [this] { return ui->treeView->selectionModel()->hasSelection() || !m_model->empty(); }; connect(ui->treeView->selectionModel(), &QItemSelectionModel::selectionChanged, this, [this, check_allow_update] { @@ -104,11 +108,16 @@ ModFolderPage::ModFolderPage(BaseInstance* inst, std::shared_ptr if (selected <= 1) { ui->actionVisitItemPage->setText(tr("Visit mod's page")); ui->actionVisitItemPage->setToolTip(tr("Go to mod's home page")); + + ui->actionRemoveItemMetadata->setToolTip(tr("Remove mod's metadata")); } else { ui->actionVisitItemPage->setText(tr("Visit mods' pages")); ui->actionVisitItemPage->setToolTip(tr("Go to the pages of the selected mods")); + + ui->actionRemoveItemMetadata->setToolTip(tr("Remove mods' metadata")); } ui->actionVisitItemPage->setEnabled(selected != 0); + ui->actionRemoveItemMetadata->setEnabled(selected != 0); }); connect(mods.get(), &ModFolderModel::rowsInserted, this, @@ -127,7 +136,7 @@ bool ModFolderPage::shouldDisplay() const return true; } -bool ModFolderPage::onSelectionChanged(const QModelIndex& current, const QModelIndex& previous) +bool ModFolderPage::onSelectionChanged(const QModelIndex& current, [[maybe_unused]] const QModelIndex& previous) { auto sourceCurrent = m_filterModel->mapToSource(current); int row = sourceCurrent.row(); @@ -297,3 +306,24 @@ void ModFolderPage::visitModPages() DesktopServices::openUrl(url); } } + +void ModFolderPage::deleteModMetadata() +{ + auto selection = m_filterModel->mapSelectionToSource(ui->treeView->selectionModel()->selection()).indexes(); + auto selectionCount = m_model->selectedMods(selection).length(); + if (selectionCount == 0) + return; + if (selectionCount > 1) { + auto response = CustomMessageBox::selectable(this, tr("Confirm Removal"), + tr("You are about to remove the metadata for %1 mods.\n" + "Are you sure?") + .arg(selectionCount), + QMessageBox::Warning, QMessageBox::Yes | QMessageBox::No, QMessageBox::No) + ->exec(); + + if (response != QMessageBox::Yes) + return; + } + + m_model->deleteModsMetadata(selection); +} diff --git a/launcher/ui/pages/instance/ModFolderPage.h b/launcher/ui/pages/instance/ModFolderPage.h index a23dcae18..0c654d0d1 100644 --- a/launcher/ui/pages/instance/ModFolderPage.h +++ b/launcher/ui/pages/instance/ModFolderPage.h @@ -61,6 +61,7 @@ class ModFolderPage : public ExternalResourcesPage { private slots: void removeItems(const QItemSelection& selection) override; + void deleteModMetadata(); void installMods(); void updateMods(); diff --git a/launcher/ui/pages/instance/OtherLogsPage.cpp b/launcher/ui/pages/instance/OtherLogsPage.cpp index b80c08e1b..ab5d98289 100644 --- a/launcher/ui/pages/instance/OtherLogsPage.cpp +++ b/launcher/ui/pages/instance/OtherLogsPage.cpp @@ -246,20 +246,20 @@ void OtherLogsPage::on_btnClean_clicked() } } if (!failed.empty()) { - QMessageBox* messageBox = new QMessageBox(this); - messageBox->setWindowTitle(tr("Error")); + QMessageBox* messageBoxFailure = new QMessageBox(this); + messageBoxFailure->setWindowTitle(tr("Error")); if (failed.size() > 5) { - messageBox->setText(tr("Couldn't delete some files!")); - messageBox->setDetailedText(failed.join('\n')); + messageBoxFailure->setText(tr("Couldn't delete some files!")); + messageBoxFailure->setDetailedText(failed.join('\n')); } else { - messageBox->setText(tr("Couldn't delete some files:\n%1").arg(failed.join('\n'))); + messageBoxFailure->setText(tr("Couldn't delete some files:\n%1").arg(failed.join('\n'))); } - messageBox->setStandardButtons(QMessageBox::Ok); - messageBox->setDefaultButton(QMessageBox::Ok); - messageBox->setTextInteractionFlags(Qt::TextSelectableByMouse); - messageBox->setIcon(QMessageBox::Critical); - messageBox->setTextInteractionFlags(Qt::TextBrowserInteraction); - messageBox->exec(); + messageBoxFailure->setStandardButtons(QMessageBox::Ok); + messageBoxFailure->setDefaultButton(QMessageBox::Ok); + messageBoxFailure->setTextInteractionFlags(Qt::TextSelectableByMouse); + messageBoxFailure->setIcon(QMessageBox::Critical); + messageBoxFailure->setTextInteractionFlags(Qt::TextBrowserInteraction); + messageBoxFailure->exec(); } } diff --git a/launcher/ui/pages/instance/ResourcePackPage.cpp b/launcher/ui/pages/instance/ResourcePackPage.cpp index 12b371df4..26c14ca4d 100644 --- a/launcher/ui/pages/instance/ResourcePackPage.cpp +++ b/launcher/ui/pages/instance/ResourcePackPage.cpp @@ -55,7 +55,7 @@ ResourcePackPage::ResourcePackPage(MinecraftInstance* instance, std::shared_ptr< ui->actionViewConfigs->setVisible(false); } -bool ResourcePackPage::onSelectionChanged(const QModelIndex& current, const QModelIndex& previous) +bool ResourcePackPage::onSelectionChanged(const QModelIndex& current, [[maybe_unused]] const QModelIndex& previous) { auto sourceCurrent = m_filterModel->mapToSource(current); int row = sourceCurrent.row(); diff --git a/launcher/ui/pages/instance/ServersPage.cpp b/launcher/ui/pages/instance/ServersPage.cpp index 07daca219..2142e6c9f 100644 --- a/launcher/ui/pages/instance/ServersPage.cpp +++ b/launcher/ui/pages/instance/ServersPage.cpp @@ -3,7 +3,7 @@ * Prism Launcher - Minecraft Launcher * Copyright (c) 2022 Jamie Mansfield * Copyright (C) 2022 Sefa Eyeoglu - * Copyright (C) 2022 TheKodeToad + * Copyright (C) 2023 TheKodeToad * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by @@ -354,14 +354,8 @@ class ServersModel : public QAbstractListModel { } } - virtual int rowCount(const QModelIndex& parent = QModelIndex()) const override - { - return parent.isValid() ? 0 : m_servers.size(); - } - int columnCount(const QModelIndex& parent) const override - { - return parent.isValid() ? 0 : COLUMN_COUNT; - } + virtual int rowCount(const QModelIndex& parent = QModelIndex()) const override { return parent.isValid() ? 0 : m_servers.size(); } + int columnCount(const QModelIndex& parent) const override { return parent.isValid() ? 0 : COLUMN_COUNT; } Server* at(int index) { @@ -445,10 +439,7 @@ class ServersModel : public QAbstractListModel { qDebug() << "Changed:" << path; load(); } - void fileChanged(const QString& path) - { - qDebug() << "Changed:" << path; - } + void fileChanged(const QString& path) { qDebug() << "Changed:" << path; } private slots: void save_internal() @@ -492,10 +483,7 @@ class ServersModel : public QAbstractListModel { m_saveTimer.stop(); } - bool saveIsScheduled() const - { - return m_dirty; - } + bool saveIsScheduled() const { return m_dirty; } void updateFSObserver() { @@ -607,7 +595,7 @@ void ServersPage::runningStateChanged(bool running) updateState(); } -void ServersPage::currentChanged(const QModelIndex& current, const QModelIndex& previous) +void ServersPage::currentChanged(const QModelIndex& current, [[maybe_unused]] const QModelIndex& previous) { int nextServer = -1; if (!current.isValid()) { @@ -620,7 +608,7 @@ void ServersPage::currentChanged(const QModelIndex& current, const QModelIndex& } // WARNING: this is here because currentChanged is not accurate when removing rows. the current item needs to be fixed up after removal. -void ServersPage::rowsRemoved(const QModelIndex& parent, int first, int last) +void ServersPage::rowsRemoved([[maybe_unused]] const QModelIndex& parent, int first, int last) { if (currentServer < first) { // current was before the removal @@ -743,7 +731,7 @@ void ServersPage::on_actionMove_Down_triggered() void ServersPage::on_actionJoin_triggered() { const auto& address = m_model->at(currentServer)->m_address; - APPLICATION->launch(m_inst, true, false, nullptr, std::make_shared(MinecraftServerTarget::parse(address))); + APPLICATION->launch(m_inst, true, false, std::make_shared(MinecraftServerTarget::parse(address))); } #include "ServersPage.moc" diff --git a/launcher/ui/pages/instance/TexturePackPage.cpp b/launcher/ui/pages/instance/TexturePackPage.cpp index e477ceda3..fa478a9a2 100644 --- a/launcher/ui/pages/instance/TexturePackPage.cpp +++ b/launcher/ui/pages/instance/TexturePackPage.cpp @@ -57,7 +57,7 @@ TexturePackPage::TexturePackPage(MinecraftInstance* instance, std::shared_ptractionViewConfigs->setVisible(false); } -bool TexturePackPage::onSelectionChanged(const QModelIndex& current, const QModelIndex& previous) +bool TexturePackPage::onSelectionChanged(const QModelIndex& current, [[maybe_unused]] const QModelIndex& previous) { auto sourceCurrent = m_filterModel->mapToSource(current); int row = sourceCurrent.row(); diff --git a/launcher/ui/pages/instance/VersionPage.cpp b/launcher/ui/pages/instance/VersionPage.cpp index a180c8041..e22c764ca 100644 --- a/launcher/ui/pages/instance/VersionPage.cpp +++ b/launcher/ui/pages/instance/VersionPage.cpp @@ -6,7 +6,7 @@ * Prism Launcher - Minecraft Launcher * Copyright (c) 2022 Jamie Mansfield * Copyright (C) 2022-2023 Sefa Eyeoglu - * Copyright (C) 2022 TheKodeToad + * Copyright (C) 2023 TheKodeToad * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by @@ -51,6 +51,7 @@ #include #include "VersionPage.h" +#include "ui/dialogs/InstallLoaderDialog.h" #include "ui_VersionPage.h" #include "ui/dialogs/CustomMessageBox.h" @@ -165,14 +166,17 @@ VersionPage::VersionPage(MinecraftInstance* inst, QWidget* parent) : QMainWindow ui->packageView->setSelectionMode(QAbstractItemView::SingleSelection); ui->packageView->setContextMenuPolicy(Qt::CustomContextMenu); - connect(ui->packageView->selectionModel(), &QItemSelectionModel::currentChanged, this, &VersionPage::versionCurrent); auto smodel = ui->packageView->selectionModel(); + connect(smodel, &QItemSelectionModel::currentChanged, this, &VersionPage::versionCurrent); connect(smodel, &QItemSelectionModel::currentChanged, this, &VersionPage::packageCurrent); - connect(m_profile.get(), &PackProfile::minecraftChanged, this, &VersionPage::updateVersionControls); updateVersionControls(); preselect(0); connect(ui->packageView, &ModListView::customContextMenuRequested, this, &VersionPage::showContextMenu); + connect(ui->packageView, &QAbstractItemView::activated, this, [this](const QModelIndex& index) { + auto component = m_profile->getComponent(index.row()); + component->setEnabled(!component->isEnabled()); + }); connect(ui->filterEdit, &QLineEdit::textChanged, this, &VersionPage::onFilterTextChanged); } @@ -188,7 +192,7 @@ void VersionPage::showContextMenu(const QPoint& pos) delete menu; } -void VersionPage::packageCurrent(const QModelIndex& current, const QModelIndex& previous) +void VersionPage::packageCurrent(const QModelIndex& current, [[maybe_unused]] const QModelIndex& previous) { if (!current.isValid()) { ui->frame->clear(); @@ -226,18 +230,6 @@ void VersionPage::packageCurrent(const QModelIndex& current, const QModelIndex& void VersionPage::updateVersionControls() { - // FIXME: this is a dirty hack - auto minecraftVersion = Version(m_profile->getComponentVersion("net.minecraft")); - - bool supportsFabric = minecraftVersion >= Version("1.14"); - ui->actionInstall_Fabric->setEnabled(supportsFabric); - - bool supportsQuilt = minecraftVersion >= Version("1.14"); - ui->actionInstall_Quilt->setEnabled(supportsQuilt); - - bool supportsLiteLoader = minecraftVersion <= Version("1.12.2"); - ui->actionInstall_LiteLoader->setEnabled(supportsLiteLoader); - updateButtons(); } @@ -389,20 +381,14 @@ void VersionPage::on_actionChange_version_triggered() return; } auto uid = list->uid(); - // FIXME: this is a horrible HACK. Get version filtering information from the actual metadata... - if (uid == "net.minecraftforge") { - on_actionInstall_Forge_triggered(); - return; - } else if (uid == "com.mumfrey.liteloader") { - on_actionInstall_LiteLoader_triggered(); - return; - } + VersionSelectDialog vselect(list.get(), tr("Change %1 version").arg(name), this); if (uid == "net.fabricmc.intermediary" || uid == "org.quiltmc.hashed") { vselect.setEmptyString(tr("No intermediary mappings versions are currently available.")); vselect.setEmptyErrorString(tr("Couldn't load or download the intermediary mappings version lists!")); - vselect.setExactFilter(BaseVersionList::ParentVersionRole, m_profile->getComponentVersion("net.minecraft")); } + vselect.setExactIfPresentFilter(BaseVersionList::ParentVersionRole, m_profile->getComponentVersion("net.minecraft")); + auto currentVersion = patch->getVersion(); if (!currentVersion.isEmpty()) { vselect.setCurrentVersion(currentVersion); @@ -425,7 +411,7 @@ void VersionPage::on_actionDownload_All_triggered() if (!APPLICATION->accounts()->anyAccountIsValid()) { CustomMessageBox::selectable(this, tr("Error"), tr("Cannot download Minecraft or update instances unless you have at least " - "one account added.\nPlease add your Mojang or Minecraft account."), + "one account added.\nPlease add your Microsoft or Mojang account."), QMessageBox::Warning) ->show(); return; @@ -443,79 +429,11 @@ void VersionPage::on_actionDownload_All_triggered() m_container->refreshContainer(); } -void VersionPage::on_actionInstall_Forge_triggered() +void VersionPage::on_actionInstall_Loader_triggered() { - auto vlist = APPLICATION->metadataIndex()->get("net.minecraftforge"); - if (!vlist) { - return; - } - VersionSelectDialog vselect(vlist.get(), tr("Select Forge version"), this); - vselect.setExactFilter(BaseVersionList::ParentVersionRole, m_profile->getComponentVersion("net.minecraft")); - vselect.setEmptyString(tr("No Forge versions are currently available for Minecraft ") + - m_profile->getComponentVersion("net.minecraft")); - vselect.setEmptyErrorString(tr("Couldn't load or download the Forge version lists!")); - - auto currentVersion = m_profile->getComponentVersion("net.minecraftforge"); - if (!currentVersion.isEmpty()) { - vselect.setCurrentVersion(currentVersion); - } - - if (vselect.exec() && vselect.selectedVersion()) { - auto vsn = vselect.selectedVersion(); - m_profile->setComponentVersion("net.minecraftforge", vsn->descriptor()); - m_profile->resolve(Net::Mode::Online); - // m_profile->installVersion(); - preselect(m_profile->rowCount(QModelIndex()) - 1); - m_container->refreshContainer(); - } -} - -void VersionPage::on_actionInstall_Fabric_triggered() -{ - auto vlist = APPLICATION->metadataIndex()->get("net.fabricmc.fabric-loader"); - if (!vlist) { - return; - } - VersionSelectDialog vselect(vlist.get(), tr("Select Fabric Loader version"), this); - vselect.setEmptyString(tr("No Fabric Loader versions are currently available.")); - vselect.setEmptyErrorString(tr("Couldn't load or download the Fabric Loader version lists!")); - - auto currentVersion = m_profile->getComponentVersion("net.fabricmc.fabric-loader"); - if (!currentVersion.isEmpty()) { - vselect.setCurrentVersion(currentVersion); - } - - if (vselect.exec() && vselect.selectedVersion()) { - auto vsn = vselect.selectedVersion(); - m_profile->setComponentVersion("net.fabricmc.fabric-loader", vsn->descriptor()); - m_profile->resolve(Net::Mode::Online); - preselect(m_profile->rowCount(QModelIndex()) - 1); - m_container->refreshContainer(); - } -} - -void VersionPage::on_actionInstall_Quilt_triggered() -{ - auto vlist = APPLICATION->metadataIndex()->get("org.quiltmc.quilt-loader"); - if (!vlist) { - return; - } - VersionSelectDialog vselect(vlist.get(), tr("Select Quilt Loader version"), this); - vselect.setEmptyString(tr("No Quilt Loader versions are currently available.")); - vselect.setEmptyErrorString(tr("Couldn't load or download the Quilt Loader version lists!")); - - auto currentVersion = m_profile->getComponentVersion("org.quiltmc.quilt-loader"); - if (!currentVersion.isEmpty()) { - vselect.setCurrentVersion(currentVersion); - } - - if (vselect.exec() && vselect.selectedVersion()) { - auto vsn = vselect.selectedVersion(); - m_profile->setComponentVersion("org.quiltmc.quilt-loader", vsn->descriptor()); - m_profile->resolve(Net::Mode::Online); - preselect(m_profile->rowCount(QModelIndex()) - 1); - m_container->refreshContainer(); - } + InstallLoaderDialog dialog(m_inst->getPackProfile(), QString(), this); + dialog.exec(); + m_container->refreshContainer(); } void VersionPage::on_actionAdd_Empty_triggered() @@ -534,33 +452,6 @@ void VersionPage::on_actionAdd_Empty_triggered() } } -void VersionPage::on_actionInstall_LiteLoader_triggered() -{ - auto vlist = APPLICATION->metadataIndex()->get("com.mumfrey.liteloader"); - if (!vlist) { - return; - } - VersionSelectDialog vselect(vlist.get(), tr("Select LiteLoader version"), this); - vselect.setExactFilter(BaseVersionList::ParentVersionRole, m_profile->getComponentVersion("net.minecraft")); - vselect.setEmptyString(tr("No LiteLoader versions are currently available for Minecraft ") + - m_profile->getComponentVersion("net.minecraft")); - vselect.setEmptyErrorString(tr("Couldn't load or download the LiteLoader version lists!")); - - auto currentVersion = m_profile->getComponentVersion("com.mumfrey.liteloader"); - if (!currentVersion.isEmpty()) { - vselect.setCurrentVersion(currentVersion); - } - - if (vselect.exec() && vselect.selectedVersion()) { - auto vsn = vselect.selectedVersion(); - m_profile->setComponentVersion("com.mumfrey.liteloader", vsn->descriptor()); - m_profile->resolve(Net::Mode::Online); - // m_profile->installVersion(vselect.selectedVersion()); - preselect(m_profile->rowCount(QModelIndex()) - 1); - m_container->refreshContainer(); - } -} - void VersionPage::on_actionLibrariesFolder_triggered() { DesktopServices::openDirectory(m_inst->getLocalLibraryPath(), true); @@ -571,7 +462,7 @@ void VersionPage::on_actionMinecraftFolder_triggered() DesktopServices::openDirectory(m_inst->gameRoot(), true); } -void VersionPage::versionCurrent(const QModelIndex& current, const QModelIndex& previous) +void VersionPage::versionCurrent(const QModelIndex& current, [[maybe_unused]] const QModelIndex& previous) { currentIdx = current.row(); updateButtons(currentIdx); diff --git a/launcher/ui/pages/instance/VersionPage.h b/launcher/ui/pages/instance/VersionPage.h index 45d383f41..6915be883 100644 --- a/launcher/ui/pages/instance/VersionPage.h +++ b/launcher/ui/pages/instance/VersionPage.h @@ -68,11 +68,8 @@ class VersionPage : public QMainWindow, public BasePage { private slots: void on_actionChange_version_triggered(); - void on_actionInstall_Forge_triggered(); - void on_actionInstall_Fabric_triggered(); - void on_actionInstall_Quilt_triggered(); + void on_actionInstall_Loader_triggered(); void on_actionAdd_Empty_triggered(); - void on_actionInstall_LiteLoader_triggered(); void on_actionReload_triggered(); void on_actionRemove_triggered(); void on_actionMove_up_triggered(); diff --git a/launcher/ui/pages/instance/VersionPage.ui b/launcher/ui/pages/instance/VersionPage.ui index a73c42d6c..9be21d499 100644 --- a/launcher/ui/pages/instance/VersionPage.ui +++ b/launcher/ui/pages/instance/VersionPage.ui @@ -98,11 +98,7 @@ - - - - - + @@ -116,26 +112,26 @@ - Change version + Change Version - Change version of the selected package. + Change version of the selected component. - Move up + Move Up - Make the selected package apply sooner. + Make the selected component apply sooner. - Move down + Move Down - Make the selected package apply later. + Make the selected component apply later. @@ -143,7 +139,7 @@ Remove - Remove selected package from the instance. + Remove selected component from the instance. @@ -151,7 +147,7 @@ Customize - Customize selected package. + Customize selected component. @@ -159,7 +155,7 @@ Edit - Edit selected package. + Edit selected component. @@ -167,39 +163,15 @@ Revert - Revert the selected package to default. + Revert the selected component to default. - + - Install Forge + Install Loader - Install the Minecraft Forge package. - - - - - Install Fabric - - - Install the Fabric Loader package. - - - - - Install Quilt - - - Install the Quilt Loader package. - - - - - Install LiteLoader - - - Install the LiteLoader package. + Install a mod loader. @@ -228,7 +200,7 @@ Add Empty - Add an empty custom package. + Add an empty custom component. @@ -236,12 +208,12 @@ Reload - Reload all packages. + Reload all components. - Download All + Download all Download the files needed to launch the instance now. diff --git a/launcher/ui/pages/instance/WorldListPage.cpp b/launcher/ui/pages/instance/WorldListPage.cpp index fe4776163..587bb6ce6 100644 --- a/launcher/ui/pages/instance/WorldListPage.cpp +++ b/launcher/ui/pages/instance/WorldListPage.cpp @@ -327,7 +327,7 @@ void WorldListPage::mceditState(LoggedProcess::State state) } } -void WorldListPage::worldChanged(const QModelIndex& current, const QModelIndex& previous) +void WorldListPage::worldChanged([[maybe_unused]] const QModelIndex& current, [[maybe_unused]] const QModelIndex& previous) { QModelIndex index = getSelectedWorld(); bool enable = index.isValid(); diff --git a/launcher/ui/pages/modplatform/CustomPage.cpp b/launcher/ui/pages/modplatform/CustomPage.cpp index 4ac21b012..068fb3a36 100644 --- a/launcher/ui/pages/modplatform/CustomPage.cpp +++ b/launcher/ui/pages/modplatform/CustomPage.cpp @@ -127,6 +127,9 @@ void CustomPage::loaderFilterChanged() ui->loaderVersionList->setEmptyString(tr("No mod loader is selected.")); ui->loaderVersionList->setEmptyMode(VersionListView::String); return; + } else if (ui->neoForgeFilter->isChecked()) { + ui->loaderVersionList->setExactFilter(BaseVersionList::ParentVersionRole, minecraftVersion); + m_selectedLoader = "net.neoforged"; } else if (ui->forgeFilter->isChecked()) { ui->loaderVersionList->setExactFilter(BaseVersionList::ParentVersionRole, minecraftVersion); m_selectedLoader = "net.minecraftforge"; diff --git a/launcher/ui/pages/modplatform/CustomPage.ui b/launcher/ui/pages/modplatform/CustomPage.ui index 0d89b5956..23351ccd4 100644 --- a/launcher/ui/pages/modplatform/CustomPage.ui +++ b/launcher/ui/pages/modplatform/CustomPage.ui @@ -194,6 +194,16 @@ + + + + NeoForge + + + loaderBtnGroup + + + diff --git a/launcher/ui/pages/modplatform/ImportPage.cpp b/launcher/ui/pages/modplatform/ImportPage.cpp index ba53d0330..3e3c36b7b 100644 --- a/launcher/ui/pages/modplatform/ImportPage.cpp +++ b/launcher/ui/pages/modplatform/ImportPage.cpp @@ -35,20 +35,28 @@ */ #include "ImportPage.h" + +#include "ui/dialogs/ProgressDialog.h" #include "ui_ImportPage.h" #include #include +#include +#include "ui/dialogs/CustomMessageBox.h" #include "ui/dialogs/NewInstanceDialog.h" +#include "modplatform/flame/FlameAPI.h" + +#include "Json.h" + #include "InstanceImportTask.h" class UrlValidator : public QValidator { public: using QValidator::QValidator; - State validate(QString& in, int& pos) const + State validate(QString& in, [[maybe_unused]] int& pos) const { const QUrl url(in); if (url.isValid() && !url.isRelative() && !url.isEmpty()) { @@ -106,10 +114,61 @@ void ImportPage::updateState() bool isMRPack = fi.suffix() == "mrpack"; if (fi.exists() && (isZip || isMRPack)) { - QFileInfo fi(url.fileName()); - dialog->setSuggestedPack(fi.completeBaseName(), new InstanceImportTask(url, this)); + auto extra_info = QMap(m_extra_info); + qDebug() << "Pack Extra Info" << extra_info << m_extra_info; + dialog->setSuggestedPack(fi.completeBaseName(), new InstanceImportTask(url, this, std::move(extra_info))); dialog->setSuggestedIcon("default"); } + } else if (url.scheme() == "curseforge") { + // need to find the download link for the modpack + // format of url curseforge://install?addonId=IDHERE&fileId=IDHERE + QUrlQuery query(url); + auto addonId = query.allQueryItemValues("addonId")[0]; + auto fileId = query.allQueryItemValues("fileId")[0]; + auto array = std::make_shared(); + + auto api = FlameAPI(); + auto job = api.getFile(addonId, fileId, array); + + connect(job.get(), &NetJob::failed, this, + [this](QString reason) { CustomMessageBox::selectable(this, tr("Error"), reason, QMessageBox::Critical)->show(); }); + connect(job.get(), &NetJob::succeeded, this, [this, array, addonId, fileId] { + qDebug() << "Returned CFURL Json:\n" << array->toStdString().c_str(); + auto doc = Json::requireDocument(*array); + auto data = Json::ensureObject(Json::ensureObject(doc.object()), "data"); + // No way to find out if it's a mod or a modpack before here + // And also we need to check if it ends with .zip, instead of any better way + auto fileName = Json::ensureString(data, "fileName"); + if (fileName.endsWith(".zip")) { + // Have to use ensureString then use QUrl to get proper url encoding + auto dl_url = QUrl(Json::ensureString(data, "downloadUrl", "", "downloadUrl")); + if (!dl_url.isValid()) { + CustomMessageBox::selectable( + this, tr("Error"), + tr("The modpack %1 is blocked for third-parties! Please download it manually.").arg(fileName), + QMessageBox::Critical) + ->show(); + return; + } + + QFileInfo dl_file(dl_url.fileName()); + QString pack_name = Json::ensureString(data, "displayName", dl_file.completeBaseName(), "displayName"); + + QMap extra_info; + extra_info.insert("pack_id", addonId); + extra_info.insert("pack_version_id", fileId); + + dialog->setSuggestedPack(pack_name, new InstanceImportTask(dl_url, this, std::move(extra_info))); + dialog->setSuggestedIcon("default"); + + } else { + CustomMessageBox::selectable(this, tr("Error"), tr("This url isn't a valid modpack !"), QMessageBox::Critical)->show(); + } + }); + ProgressDialog dlUrlDialod(this); + dlUrlDialod.setSkipButton(true, tr("Abort")); + dlUrlDialod.execWithTask(job.get()); + return; } else { if (input.endsWith("?client=y")) { input.chop(9); @@ -118,7 +177,8 @@ void ImportPage::updateState() } // hook, line and sinker. QFileInfo fi(url.fileName()); - dialog->setSuggestedPack(fi.completeBaseName(), new InstanceImportTask(url, this)); + auto extra_info = QMap(m_extra_info); + dialog->setSuggestedPack(fi.completeBaseName(), new InstanceImportTask(url, this, std::move(extra_info))); dialog->setSuggestedIcon("default"); } } else { @@ -132,6 +192,12 @@ void ImportPage::setUrl(const QString& url) updateState(); } +void ImportPage::setExtraInfo(const QMap& extra_info) +{ + m_extra_info = extra_info; + updateState(); +} + void ImportPage::on_modpackBtn_clicked() { auto filter = QMimeDatabase().mimeTypeForName("application/zip").filterString(); diff --git a/launcher/ui/pages/modplatform/ImportPage.h b/launcher/ui/pages/modplatform/ImportPage.h index d846d566d..70d7736eb 100644 --- a/launcher/ui/pages/modplatform/ImportPage.h +++ b/launcher/ui/pages/modplatform/ImportPage.h @@ -62,7 +62,7 @@ class ImportPage : public QWidget, public BasePage { void setUrl(const QString& url); void openedImpl() override; - + void setExtraInfo(const QMap& extra_info); private slots: void on_modpackBtn_clicked(); void updateState(); @@ -73,4 +73,5 @@ class ImportPage : public QWidget, public BasePage { private: Ui::ImportPage* ui = nullptr; NewInstanceDialog* dialog = nullptr; + QMap m_extra_info = {}; }; diff --git a/launcher/ui/pages/modplatform/ImportPage.ui b/launcher/ui/pages/modplatform/ImportPage.ui index 3583cf90a..9a9736b8a 100644 --- a/launcher/ui/pages/modplatform/ImportPage.ui +++ b/launcher/ui/pages/modplatform/ImportPage.ui @@ -40,7 +40,7 @@ - - CurseForge modpacks (ZIP) + - CurseForge modpacks (ZIP / curseforge:// URL) Qt::AlignCenter diff --git a/launcher/ui/pages/modplatform/ModModel.cpp b/launcher/ui/pages/modplatform/ModModel.cpp index b75378905..c628f74ac 100644 --- a/launcher/ui/pages/modplatform/ModModel.cpp +++ b/launcher/ui/pages/modplatform/ModModel.cpp @@ -33,7 +33,7 @@ ResourceAPI::SearchArgs ModModel::createSearchArguments() auto sort = getCurrentSortingMethodByIndex(); - return { ModPlatform::ResourceType::MOD, m_next_search_offset, m_search_term, sort, profile->getModLoaders(), versions }; + return { ModPlatform::ResourceType::MOD, m_next_search_offset, m_search_term, sort, profile->getSupportedModLoaders(), versions }; } ResourceAPI::VersionSearchArgs ModModel::createVersionsArguments(QModelIndex& entry) @@ -48,7 +48,7 @@ ResourceAPI::VersionSearchArgs ModModel::createVersionsArguments(QModelIndex& en if (!m_filter->versions.empty()) versions = m_filter->versions; - return { pack, versions, profile->getModLoaders() }; + return { pack, versions, profile->getSupportedModLoaders() }; } ResourceAPI::ProjectInfoArgs ModModel::createInfoArguments(QModelIndex& entry) diff --git a/launcher/ui/pages/modplatform/ModPage.cpp b/launcher/ui/pages/modplatform/ModPage.cpp index 9b178048f..d6cc1fdcc 100644 --- a/launcher/ui/pages/modplatform/ModPage.cpp +++ b/launcher/ui/pages/modplatform/ModPage.cpp @@ -124,8 +124,7 @@ void ModPage::updateVersionList() auto version = current_pack->versions[i]; bool valid = false; for (auto& mcVer : m_filter->versions) { - // NOTE: Flame doesn't care about loader, so passing it changes nothing. - if (validateVersion(version, mcVer.toString(), packProfile->getModLoaders())) { + if (validateVersion(version, mcVer.toString(), packProfile->getSupportedModLoaders())) { valid = true; break; } diff --git a/launcher/ui/pages/modplatform/ModPage.h b/launcher/ui/pages/modplatform/ModPage.h index 5510c1911..5a43e49a6 100644 --- a/launcher/ui/pages/modplatform/ModPage.h +++ b/launcher/ui/pages/modplatform/ModPage.h @@ -55,7 +55,7 @@ class ModPage : public ResourcePage { virtual auto validateVersion(ModPlatform::IndexedVersion& ver, QString mineVer, - std::optional loaders = {}) const -> bool = 0; + std::optional loaders = {}) const -> bool = 0; [[nodiscard]] bool supportsFiltering() const override { return true; }; auto getFilter() const -> const std::shared_ptr { return m_filter; } diff --git a/launcher/ui/pages/modplatform/ResourceModel.cpp b/launcher/ui/pages/modplatform/ResourceModel.cpp index 49405a02b..cb8f1920f 100644 --- a/launcher/ui/pages/modplatform/ResourceModel.cpp +++ b/launcher/ui/pages/modplatform/ResourceModel.cpp @@ -17,7 +17,7 @@ #include "BuildConfig.h" #include "Json.h" -#include "net/Download.h" +#include "net/ApiDownload.h" #include "net/NetJob.h" #include "modplatform/ModIndex.h" @@ -102,7 +102,7 @@ QHash ResourceModel::roleNames() const return roles; } -bool ResourceModel::setData(const QModelIndex& index, const QVariant& value, int role) +bool ResourceModel::setData(const QModelIndex& index, const QVariant& value, [[maybe_unused]] int role) { int pos = index.row(); if (pos >= m_packs.size() || pos < 0 || !index.isValid()) @@ -132,6 +132,32 @@ void ResourceModel::search() if (hasActiveSearchJob()) return; + if (m_search_term.startsWith("#")) { + auto projectId = m_search_term.mid(1); + if (!projectId.isEmpty()) { + ResourceAPI::ProjectInfoCallbacks callbacks; + + callbacks.on_fail = [this](QString reason) { + if (!s_running_models.constFind(this).value()) + return; + searchRequestFailed(reason, -1); + }; + callbacks.on_abort = [this] { + if (!s_running_models.constFind(this).value()) + return; + searchRequestAborted(); + }; + + callbacks.on_succeed = [this](auto& doc, auto& pack) { + if (!s_running_models.constFind(this).value()) + return; + searchRequestForOneSucceeded(doc); + }; + if (auto job = m_api->getProjectInfo({ projectId }, std::move(callbacks)); job) + runSearchJob(job); + return; + } + } auto args{ createSearchArguments() }; auto callbacks{ createSearchCallbacks() }; @@ -189,11 +215,18 @@ void ResourceModel::loadEntry(QModelIndex& entry) // Use default if no callbacks are set if (!callbacks.on_succeed) - callbacks.on_succeed = [this, entry](auto& doc, auto pack) { + callbacks.on_succeed = [this, entry](auto& doc, auto& newpack) { if (!s_running_models.constFind(this).value()) return; + auto pack = newpack; infoRequestSucceeded(doc, pack, entry); }; + if (!callbacks.on_fail) + callbacks.on_fail = [this](QString reason) { + if (!s_running_models.constFind(this).value()) + return; + QMessageBox::critical(nullptr, tr("Error"), tr("A network error occurred. Could not load project info:%1").arg(reason)); + }; if (auto job = m_api->getProjectInfo(std::move(args), std::move(callbacks)); job) runInfoJob(job); @@ -281,7 +314,7 @@ std::optional ResourceModel::getIcon(QModelIndex& index, const QUrl& url) auto cache_entry = APPLICATION->metacache()->resolveEntry( metaEntryBase(), QString("logos/%1").arg(QString(QCryptographicHash::hash(url.toEncoded(), QCryptographicHash::Algorithm::Sha1).toHex()))); - auto icon_fetch_action = Net::Download::makeCached(url, cache_entry); + auto icon_fetch_action = Net::ApiDownload::makeCached(url, cache_entry); auto full_file_path = cache_entry->getFullPath(); connect(icon_fetch_action.get(), &NetAction::succeeded, this, [=] { @@ -310,7 +343,7 @@ std::optional ResourceModel::getIcon(QModelIndex& index, const QUrl& url) #define NEED_FOR_CALLBACK_ASSERT(name) \ Q_ASSERT_X(0 != 0, #name, "You NEED to re-implement this if you intend on using the default callbacks.") -QJsonArray ResourceModel::documentToArray(QJsonDocument& doc) const +QJsonArray ResourceModel::documentToArray([[maybe_unused]] QJsonDocument& doc) const { NEED_FOR_CALLBACK_ASSERT("documentToArray"); return {}; @@ -372,7 +405,28 @@ void ResourceModel::searchRequestSucceeded(QJsonDocument& doc) endInsertRows(); } -void ResourceModel::searchRequestFailed(QString reason, int network_error_code) +void ResourceModel::searchRequestForOneSucceeded(QJsonDocument& doc) +{ + ModPlatform::IndexedPack::Ptr pack = std::make_shared(); + + try { + auto obj = Json::requireObject(doc); + if (obj.contains("data")) + obj = Json::requireObject(obj, "data"); + loadIndexedPack(*pack, obj); + } catch (const JSONValidationError& e) { + qDebug() << doc; + qWarning() << "Error while reading " << debugName() << " resource info: " << e.cause(); + } + + m_search_state = SearchState::Finished; + + beginInsertRows(QModelIndex(), m_packs.size(), m_packs.size() + 1); + m_packs.append(pack); + endInsertRows(); +} + +void ResourceModel::searchRequestFailed([[maybe_unused]] QString reason, int network_error_code) { switch (network_error_code) { default: diff --git a/launcher/ui/pages/modplatform/ResourceModel.h b/launcher/ui/pages/modplatform/ResourceModel.h index 6533d9c6a..ecf4f8f79 100644 --- a/launcher/ui/pages/modplatform/ResourceModel.h +++ b/launcher/ui/pages/modplatform/ResourceModel.h @@ -42,7 +42,10 @@ class ResourceModel : public QAbstractListModel { [[nodiscard]] virtual auto debugName() const -> QString; [[nodiscard]] virtual auto metaEntryBase() const -> QString = 0; - [[nodiscard]] inline int rowCount(const QModelIndex& parent) const override { return parent.isValid() ? 0 : m_packs.size(); } + [[nodiscard]] inline int rowCount(const QModelIndex& parent) const override + { + return parent.isValid() ? 0 : static_cast(m_packs.size()); + } [[nodiscard]] inline int columnCount(const QModelIndex& parent) const override { return parent.isValid() ? 0 : 1; } [[nodiscard]] inline auto flags(const QModelIndex& index) const -> Qt::ItemFlags override { return QAbstractListModel::flags(index); } @@ -146,6 +149,7 @@ class ResourceModel : public QAbstractListModel { private: /* Default search request callbacks */ void searchRequestSucceeded(QJsonDocument&); + void searchRequestForOneSucceeded(QJsonDocument&); void searchRequestFailed(QString reason, int network_error_code); void searchRequestAborted(); diff --git a/launcher/ui/pages/modplatform/ResourcePackModel.cpp b/launcher/ui/pages/modplatform/ResourcePackModel.cpp index 18c14bf81..d436f320f 100644 --- a/launcher/ui/pages/modplatform/ResourcePackModel.cpp +++ b/launcher/ui/pages/modplatform/ResourcePackModel.cpp @@ -9,7 +9,8 @@ namespace ResourceDownload { ResourcePackResourceModel::ResourcePackResourceModel(BaseInstance const& base_inst, ResourceAPI* api) - : ResourceModel(api), m_base_instance(base_inst){}; + : ResourceModel(api), m_base_instance(base_inst) +{} /******** Make data requests ********/ diff --git a/launcher/ui/pages/modplatform/ResourcePage.cpp b/launcher/ui/pages/modplatform/ResourcePage.cpp index 2925ec87e..44a91003d 100644 --- a/launcher/ui/pages/modplatform/ResourcePage.cpp +++ b/launcher/ui/pages/modplatform/ResourcePage.cpp @@ -4,7 +4,7 @@ /* * Prism Launcher - Minecraft Launcher * Copyright (C) 2022 Sefa Eyeoglu - * Copyright (C) 2022 TheKodeToad + * Copyright (C) 2023 TheKodeToad * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by @@ -44,9 +44,6 @@ #include #include "Markdown.h" -#include "ResourceDownloadTask.h" - -#include "minecraft/MinecraftInstance.h" #include "ui/dialogs/ResourceDownloadDialog.h" #include "ui/pages/modplatform/ResourceModel.h" @@ -283,7 +280,7 @@ void ResourcePage::updateVersionList() updateSelectionButton(); } -void ResourcePage::onSelectionChanged(QModelIndex curr, QModelIndex prev) +void ResourcePage::onSelectionChanged(QModelIndex curr, [[maybe_unused]] QModelIndex prev) { if (!curr.isValid()) { return; @@ -310,9 +307,9 @@ void ResourcePage::onSelectionChanged(QModelIndex curr, QModelIndex prev) updateUi(); } -void ResourcePage::onVersionSelectionChanged(QString data) +void ResourcePage::onVersionSelectionChanged(QString versionData) { - if (data.isNull() || data.isEmpty()) { + if (versionData.isNull() || versionData.isEmpty()) { m_selected_version_index = -1; return; } @@ -398,7 +395,7 @@ void ResourcePage::openUrl(const QUrl& url) if (auto current_pack = getCurrentPack(); current_pack && slug != current_pack->slug) { m_parent_dialog->selectPage(page); - auto newPage = m_parent_dialog->getSelectedPage(); + auto newPage = m_parent_dialog->selectedPage(); QLineEdit* searchEdit = newPage->m_ui->searchEdit; auto model = newPage->m_model; diff --git a/launcher/ui/pages/modplatform/ResourcePage.h b/launcher/ui/pages/modplatform/ResourcePage.h index b4a87f573..7bec0a375 100644 --- a/launcher/ui/pages/modplatform/ResourcePage.h +++ b/launcher/ui/pages/modplatform/ResourcePage.h @@ -97,7 +97,11 @@ class ResourcePage : public QWidget, public BasePage { virtual void openUrl(const QUrl&); /** Whether the version is opted out or not. Currently only makes sense in CF. */ - virtual bool optedOut(ModPlatform::IndexedVersion& ver) const { return false; }; + virtual bool optedOut(ModPlatform::IndexedVersion& ver) const + { + Q_UNUSED(ver); + return false; + }; public: BaseInstance& m_base_instance; diff --git a/launcher/ui/pages/modplatform/ShaderPackModel.cpp b/launcher/ui/pages/modplatform/ShaderPackModel.cpp index aabd3be6e..8c913657a 100644 --- a/launcher/ui/pages/modplatform/ShaderPackModel.cpp +++ b/launcher/ui/pages/modplatform/ShaderPackModel.cpp @@ -9,7 +9,8 @@ namespace ResourceDownload { ShaderPackResourceModel::ShaderPackResourceModel(BaseInstance const& base_inst, ResourceAPI* api) - : ResourceModel(api), m_base_instance(base_inst){}; + : ResourceModel(api), m_base_instance(base_inst) +{} /******** Make data requests ********/ diff --git a/launcher/ui/pages/modplatform/ShaderPackPage.cpp b/launcher/ui/pages/modplatform/ShaderPackPage.cpp index fbf94e844..586dffc55 100644 --- a/launcher/ui/pages/modplatform/ShaderPackPage.cpp +++ b/launcher/ui/pages/modplatform/ShaderPackPage.cpp @@ -3,6 +3,7 @@ // SPDX-License-Identifier: GPL-3.0-only #include "ShaderPackPage.h" +#include "modplatform/ModIndex.h" #include "ui_ResourcePage.h" #include "ShaderPackModel.h" @@ -48,7 +49,7 @@ void ShaderPackResourcePage::addResourceToPage(ModPlatform::IndexedPack::Ptr pac const std::shared_ptr base_model) { QString custom_target_folder; - if (version.loaders.contains(QStringLiteral("canvas"))) + if (version.loaders & ModPlatform::Cauldron) custom_target_folder = QStringLiteral("resourcepacks"); m_model->addPack(pack, version, base_model, false, custom_target_folder); } diff --git a/launcher/ui/pages/modplatform/atlauncher/AtlFilterModel.cpp b/launcher/ui/pages/modplatform/atlauncher/AtlFilterModel.cpp index 9cd5eed53..dee3784e5 100644 --- a/launcher/ui/pages/modplatform/atlauncher/AtlFilterModel.cpp +++ b/launcher/ui/pages/modplatform/atlauncher/AtlFilterModel.cpp @@ -67,9 +67,10 @@ bool FilterModel::filterAcceptsRow(int sourceRow, const QModelIndex& sourceParen if (searchTerm.isEmpty()) { return true; } - QModelIndex index = sourceModel()->index(sourceRow, 0, sourceParent); ATLauncher::IndexedPack pack = sourceModel()->data(index, Qt::UserRole).value(); + if (searchTerm.startsWith("#")) + return QString::number(pack.id) == searchTerm.mid(1); return pack.name.contains(searchTerm, Qt::CaseInsensitive); } diff --git a/launcher/ui/pages/modplatform/atlauncher/AtlListModel.cpp b/launcher/ui/pages/modplatform/atlauncher/AtlListModel.cpp index c6b087d67..b6fb71535 100644 --- a/launcher/ui/pages/modplatform/atlauncher/AtlListModel.cpp +++ b/launcher/ui/pages/modplatform/atlauncher/AtlListModel.cpp @@ -20,6 +20,9 @@ #include #include +#include "net/ApiDownload.h" +#include "ui/widgets/ProjectItem.h" + namespace Atl { ListModel::ListModel(QObject* parent) : QAbstractListModel(parent) {} @@ -44,27 +47,50 @@ QVariant ListModel::data(const QModelIndex& index, int role) const } ATLauncher::IndexedPack pack = modpacks.at(pos); - if (role == Qt::DisplayRole) { - return pack.name; - } else if (role == Qt::ToolTipRole) { - return pack.name; - } else if (role == Qt::DecorationRole) { - if (m_logoMap.contains(pack.safeName)) { - return (m_logoMap.value(pack.safeName)); + switch (role) { + case Qt::ToolTipRole: { + if (pack.description.length() > 100) { + // some magic to prevent to long tooltips and replace html linebreaks + QString edit = pack.description.left(97); + edit = edit.left(edit.lastIndexOf("
")).left(edit.lastIndexOf(" ")).append("..."); + return edit; + } + return pack.description; } - auto icon = APPLICATION->getThemedIcon("atlauncher-placeholder"); + case Qt::DecorationRole: { + if (m_logoMap.contains(pack.safeName)) { + return (m_logoMap.value(pack.safeName)); + } + auto icon = APPLICATION->getThemedIcon("atlauncher-placeholder"); - auto url = QString(BuildConfig.ATL_DOWNLOAD_SERVER_URL + "launcher/images/%1.png").arg(pack.safeName.toLower()); - ((ListModel*)this)->requestLogo(pack.safeName, url); + auto url = QString(BuildConfig.ATL_DOWNLOAD_SERVER_URL + "launcher/images/%1.png").arg(pack.safeName.toLower()); + ((ListModel*)this)->requestLogo(pack.safeName, url); - return icon; - } else if (role == Qt::UserRole) { - QVariant v; - v.setValue(pack); - return v; + return icon; + } + case Qt::UserRole: { + QVariant v; + v.setValue(pack); + return v; + } + case Qt::DisplayRole: + return pack.name; + case Qt::SizeHintRole: + return QSize(0, 58); + // Custom data + case UserDataTypes::TITLE: + return pack.name; + case UserDataTypes::DESCRIPTION: + return pack.description; + case UserDataTypes::SELECTED: + return false; + case UserDataTypes::INSTALLED: + return false; + default: + break; } - return QVariant(); + return {}; } void ListModel::request() @@ -75,7 +101,7 @@ void ListModel::request() auto netJob = makeShared("Atl::Request", APPLICATION->network()); auto url = QString(BuildConfig.ATL_DOWNLOAD_SERVER_URL + "launcher/json/packsnew.json"); - netJob->addNetAction(Net::Download::makeByteArray(QUrl(url), response)); + netJob->addNetAction(Net::ApiDownload::makeByteArray(QUrl(url), response)); jobPtr = netJob; jobPtr->start(); @@ -137,8 +163,7 @@ void ListModel::requestFailed(QString reason) void ListModel::getLogo(const QString& logo, const QString& logoUrl, LogoCallback callback) { if (m_logoMap.contains(logo)) { - callback( - APPLICATION->metacache()->resolveEntry("ATLauncherPacks", QString("logos/%1").arg(logo.section(".", 0, 0)))->getFullPath()); + callback(APPLICATION->metacache()->resolveEntry("ATLauncherPacks", QString("logos/%1").arg(logo))->getFullPath()); } else { requestLogo(logo, logoUrl); } @@ -168,9 +193,9 @@ void ListModel::requestLogo(QString file, QString url) return; } - MetaEntryPtr entry = APPLICATION->metacache()->resolveEntry("ATLauncherPacks", QString("logos/%1").arg(file.section(".", 0, 0))); + MetaEntryPtr entry = APPLICATION->metacache()->resolveEntry("ATLauncherPacks", QString("logos/%1").arg(file)); auto job = new NetJob(QString("ATLauncher Icon Download %1").arg(file), APPLICATION->network()); - job->addNetAction(Net::Download::makeCached(QUrl(url), entry)); + job->addNetAction(Net::ApiDownload::makeCached(QUrl(url), entry)); auto fullPath = entry->getFullPath(); QObject::connect(job, &NetJob::succeeded, this, [this, file, fullPath, job] { diff --git a/launcher/ui/pages/modplatform/atlauncher/AtlOptionalModDialog.cpp b/launcher/ui/pages/modplatform/atlauncher/AtlOptionalModDialog.cpp index 62e406d38..6fb867733 100644 --- a/launcher/ui/pages/modplatform/atlauncher/AtlOptionalModDialog.cpp +++ b/launcher/ui/pages/modplatform/atlauncher/AtlOptionalModDialog.cpp @@ -43,6 +43,8 @@ #include "Json.h" #include "modplatform/atlauncher/ATLShareCode.h" +#include "net/ApiDownload.h" + AtlOptionalModListModel::AtlOptionalModListModel(QWidget* parent, ATLauncher::PackVersion version, QVector mods) : QAbstractListModel(parent), m_version(version), m_mods(mods) { @@ -113,7 +115,7 @@ QVariant AtlOptionalModListModel::data(const QModelIndex& index, int role) const return {}; } -bool AtlOptionalModListModel::setData(const QModelIndex& index, const QVariant& value, int role) +bool AtlOptionalModListModel::setData(const QModelIndex& index, [[maybe_unused]] const QVariant& value, int role) { if (role == Qt::CheckStateRole) { auto row = index.row(); @@ -155,7 +157,7 @@ void AtlOptionalModListModel::useShareCode(const QString& code) { m_jobPtr.reset(new NetJob("Atl::Request", APPLICATION->network())); auto url = QString(BuildConfig.ATL_API_BASE_URL + "share-codes/" + code); - m_jobPtr->addNetAction(Net::Download::makeByteArray(QUrl(url), m_response)); + m_jobPtr->addNetAction(Net::ApiDownload::makeByteArray(QUrl(url), m_response)); connect(m_jobPtr.get(), &NetJob::succeeded, this, &AtlOptionalModListModel::shareCodeSuccess); connect(m_jobPtr.get(), &NetJob::failed, this, &AtlOptionalModListModel::shareCodeFailure); @@ -206,7 +208,7 @@ void AtlOptionalModListModel::shareCodeSuccess() emit dataChanged(AtlOptionalModListModel::index(0, EnabledColumn), AtlOptionalModListModel::index(m_mods.size() - 1, EnabledColumn)); } -void AtlOptionalModListModel::shareCodeFailure(const QString& reason) +void AtlOptionalModListModel::shareCodeFailure([[maybe_unused]] const QString& reason) { m_jobPtr.reset(); @@ -281,15 +283,15 @@ void AtlOptionalModListModel::setMod(ATLauncher::VersionMod mod, int index, bool // if the dependency is 'effectively hidden', then track which mods // depend on it - so we can efficiently disable it when no more dependents // depend on it. - auto dependants = m_dependants[dependencyName]; + auto dependents = m_dependents[dependencyName]; if (enable) { - dependants.append(mod.name); + dependents.append(mod.name); } else { - dependants.removeAll(mod.name); + dependents.removeAll(mod.name); // if there are no longer any dependents, let's disable the mod - if (dependencyMod.effectively_hidden && dependants.isEmpty()) { + if (dependencyMod.effectively_hidden && dependents.isEmpty()) { setMod(dependencyMod, dependencyIndex, false, shouldEmit); } } @@ -297,8 +299,8 @@ void AtlOptionalModListModel::setMod(ATLauncher::VersionMod mod, int index, bool // disable mods that depend on this one, if disabling if (!enable) { - auto dependants = m_dependants[mod.name]; - for (const auto& dependencyName : dependants) { + auto dependents = m_dependents[mod.name]; + for (const auto& dependencyName : dependents) { auto dependencyIndex = m_index[dependencyName]; auto dependencyMod = m_mods.at(dependencyIndex); diff --git a/launcher/ui/pages/modplatform/atlauncher/AtlOptionalModDialog.h b/launcher/ui/pages/modplatform/atlauncher/AtlOptionalModDialog.h index 72a946dad..55903003b 100644 --- a/launcher/ui/pages/modplatform/atlauncher/AtlOptionalModDialog.h +++ b/launcher/ui/pages/modplatform/atlauncher/AtlOptionalModDialog.h @@ -90,7 +90,7 @@ class AtlOptionalModListModel : public QAbstractListModel { QMap m_selection; QMap m_index; - QMap> m_dependants; + QMap> m_dependents; }; class AtlOptionalModDialog : public QDialog { diff --git a/launcher/ui/pages/modplatform/atlauncher/AtlPage.cpp b/launcher/ui/pages/modplatform/atlauncher/AtlPage.cpp index 4c9dec581..c7e800278 100644 --- a/launcher/ui/pages/modplatform/atlauncher/AtlPage.cpp +++ b/launcher/ui/pages/modplatform/atlauncher/AtlPage.cpp @@ -35,11 +35,11 @@ */ #include "AtlPage.h" +#include "ui/widgets/ProjectItem.h" #include "ui_AtlPage.h" #include "BuildConfig.h" -#include "AtlOptionalModDialog.h" #include "AtlUserInteractionSupportImpl.h" #include "modplatform/atlauncher/ATLPackInstallTask.h" #include "ui/dialogs/NewInstanceDialog.h" @@ -71,6 +71,8 @@ AtlPage::AtlPage(NewInstanceDialog* dialog, QWidget* parent) : QWidget(parent), connect(ui->sortByBox, &QComboBox::currentTextChanged, this, &AtlPage::onSortingSelectionChanged); connect(ui->packView->selectionModel(), &QItemSelectionModel::currentChanged, this, &AtlPage::onSelectionChanged); connect(ui->versionSelectionBox, &QComboBox::currentTextChanged, this, &AtlPage::onVersionSelectionChanged); + + ui->packView->setItemDelegate(new ProjectItemDelegate(this)); } AtlPage::~AtlPage() @@ -123,13 +125,13 @@ void AtlPage::triggerSearch() filterModel->setSearchTerm(ui->searchEdit->text()); } -void AtlPage::onSortingSelectionChanged(QString data) +void AtlPage::onSortingSelectionChanged(QString sort) { - auto toSet = filterModel->getAvailableSortings().value(data); + auto toSet = filterModel->getAvailableSortings().value(sort); filterModel->setSorting(toSet); } -void AtlPage::onSelectionChanged(QModelIndex first, QModelIndex second) +void AtlPage::onSelectionChanged(QModelIndex first, [[maybe_unused]] QModelIndex second) { ui->versionSelectionBox->clear(); @@ -151,13 +153,13 @@ void AtlPage::onSelectionChanged(QModelIndex first, QModelIndex second) suggestCurrent(); } -void AtlPage::onVersionSelectionChanged(QString data) +void AtlPage::onVersionSelectionChanged(QString version) { - if (data.isNull() || data.isEmpty()) { + if (version.isNull() || version.isEmpty()) { selectedVersion = ""; return; } - selectedVersion = data; + selectedVersion = version; suggestCurrent(); } diff --git a/launcher/ui/pages/modplatform/atlauncher/AtlPage.ui b/launcher/ui/pages/modplatform/atlauncher/AtlPage.ui index 746aa6d1c..8b6747331 100644 --- a/launcher/ui/pages/modplatform/atlauncher/AtlPage.ui +++ b/launcher/ui/pages/modplatform/atlauncher/AtlPage.ui @@ -11,44 +11,7 @@
- - - - - - true - - - - 96 - 48 - - - - - - - - true - - - true - - - - - - - Warning: This is still a work in progress. If you run into issues with the imported modpack, it may be a bug. - - - true - - - - - - + @@ -68,7 +31,34 @@ - + + + + + + true + + + true + + + + + + + true + + + + 96 + 48 + + + + + + + Search and filter... @@ -78,6 +68,31 @@ + + + + Search + + + + + + + + true + + + + Warning: This is still a work in progress. If you run into issues with the imported modpack, it may be a bug. + + + Qt::AlignCenter + + + true + + +
diff --git a/launcher/ui/pages/modplatform/flame/FlameModel.cpp b/launcher/ui/pages/modplatform/flame/FlameModel.cpp index 83c1270bd..8875a9452 100644 --- a/launcher/ui/pages/modplatform/flame/FlameModel.cpp +++ b/launcher/ui/pages/modplatform/flame/FlameModel.cpp @@ -1,8 +1,12 @@ #include "FlameModel.h" #include #include "Application.h" +#include "modplatform/ResourceAPI.h" +#include "modplatform/flame/FlameAPI.h" #include "ui/widgets/ProjectItem.h" +#include "net/ApiDownload.h" + #include #include @@ -70,7 +74,7 @@ QVariant ListModel::data(const QModelIndex& index, int role) const return QVariant(); } -bool ListModel::setData(const QModelIndex& index, const QVariant& value, int role) +bool ListModel::setData(const QModelIndex& index, const QVariant& value, [[maybe_unused]] int role) { int pos = index.row(); if (pos >= modpacks.size() || pos < 0 || !index.isValid()) @@ -104,9 +108,9 @@ void ListModel::requestLogo(QString logo, QString url) return; } - MetaEntryPtr entry = APPLICATION->metacache()->resolveEntry("FlamePacks", QString("logos/%1").arg(logo.section(".", 0, 0))); + MetaEntryPtr entry = APPLICATION->metacache()->resolveEntry("FlamePacks", QString("logos/%1").arg(logo)); auto job = new NetJob(QString("Flame Icon Download %1").arg(logo), APPLICATION->network()); - job->addNetAction(Net::Download::makeCached(QUrl(url), entry)); + job->addNetAction(Net::ApiDownload::makeCached(QUrl(url), entry)); auto fullPath = entry->getFullPath(); QObject::connect(job, &NetJob::succeeded, this, [this, logo, fullPath, job] { @@ -130,7 +134,7 @@ void ListModel::requestLogo(QString logo, QString url) void ListModel::getLogo(const QString& logo, const QString& logoUrl, LogoCallback callback) { if (m_logoMap.contains(logo)) { - callback(APPLICATION->metacache()->resolveEntry("FlamePacks", QString("logos/%1").arg(logo.section(".", 0, 0)))->getFullPath()); + callback(APPLICATION->metacache()->resolveEntry("FlamePacks", QString("logos/%1").arg(logo))->getFullPath()); } else { requestLogo(logo, logoUrl); } @@ -141,7 +145,7 @@ Qt::ItemFlags ListModel::flags(const QModelIndex& index) const return QAbstractListModel::flags(index); } -bool ListModel::canFetchMore(const QModelIndex& parent) const +bool ListModel::canFetchMore([[maybe_unused]] const QModelIndex& parent) const { return searchState == CanPossiblyFetchMore; } @@ -159,6 +163,21 @@ void ListModel::fetchMore(const QModelIndex& parent) void ListModel::performPaginatedSearch() { + if (currentSearchTerm.startsWith("#")) { + auto projectId = currentSearchTerm.mid(1); + if (!projectId.isEmpty()) { + ResourceAPI::ProjectInfoCallbacks callbacks; + + callbacks.on_fail = [this](QString reason) { searchRequestFailed(reason); }; + callbacks.on_succeed = [this](auto& doc, auto& pack) { searchRequestForOneSucceeded(doc); }; + static const FlameAPI api; + if (auto job = api.getProjectInfo({ projectId }, std::move(callbacks)); job) { + jobPtr = job; + jobPtr->start(); + } + return; + } + } auto netJob = makeShared("Flame::Search", APPLICATION->network()); auto searchUrl = QString( "https://api.curseforge.com/v1/mods/search?" @@ -173,7 +192,7 @@ void ListModel::performPaginatedSearch() .arg(currentSearchTerm) .arg(currentSort + 1); - netJob->addNetAction(Net::Download::makeByteArray(QUrl(searchUrl), response)); + netJob->addNetAction(Net::ApiDownload::makeByteArray(QUrl(searchUrl), response)); jobPtr = netJob; jobPtr->start(); QObject::connect(netJob.get(), &NetJob::succeeded, this, &ListModel::searchRequestFinished); @@ -187,23 +206,24 @@ void ListModel::searchWithTerm(const QString& term, int sort) } currentSearchTerm = term; currentSort = sort; - if (jobPtr) { + if (hasActiveSearchJob()) { jobPtr->abort(); searchState = ResetRequested; return; - } else { - beginResetModel(); - modpacks.clear(); - endResetModel(); - searchState = None; } + beginResetModel(); + modpacks.clear(); + endResetModel(); + searchState = None; + nextSearchOffset = 0; performPaginatedSearch(); } void Flame::ListModel::searchRequestFinished() { - jobPtr.reset(); + if (hasActiveSearchJob()) + return; QJsonParseError parse_error; QJsonDocument doc = QJsonDocument::fromJson(*response, &parse_error); @@ -244,6 +264,25 @@ void Flame::ListModel::searchRequestFinished() endInsertRows(); } +void Flame::ListModel::searchRequestForOneSucceeded(QJsonDocument& doc) +{ + jobPtr.reset(); + + auto packObj = Json::ensureObject(doc.object(), "data"); + + Flame::IndexedPack pack; + try { + Flame::loadIndexedPack(pack, packObj); + } catch (const JSONValidationError& e) { + qWarning() << "Error while loading pack from CurseForge: " << e.cause(); + return; + } + + beginInsertRows(QModelIndex(), modpacks.size(), modpacks.size() + 1); + modpacks.append({ pack }); + endInsertRows(); +} + void Flame::ListModel::searchRequestFailed(QString reason) { jobPtr.reset(); diff --git a/launcher/ui/pages/modplatform/flame/FlameModel.h b/launcher/ui/pages/modplatform/flame/FlameModel.h index b3bc96b8c..fd8496dfb 100644 --- a/launcher/ui/pages/modplatform/flame/FlameModel.h +++ b/launcher/ui/pages/modplatform/flame/FlameModel.h @@ -40,6 +40,9 @@ class ListModel : public QAbstractListModel { void getLogo(const QString& logo, const QString& logoUrl, LogoCallback callback); void searchWithTerm(const QString& term, const int sort); + [[nodiscard]] bool hasActiveSearchJob() const { return jobPtr && jobPtr->isRunning(); } + [[nodiscard]] Task::Ptr activeSearchJob() { return hasActiveSearchJob() ? jobPtr : nullptr; } + private slots: void performPaginatedSearch(); @@ -48,6 +51,7 @@ class ListModel : public QAbstractListModel { void searchRequestFinished(); void searchRequestFailed(QString reason); + void searchRequestForOneSucceeded(QJsonDocument&); private: void requestLogo(QString file, QString url); @@ -63,7 +67,7 @@ class ListModel : public QAbstractListModel { int currentSort = 0; int nextSearchOffset = 0; enum SearchState { None, CanPossiblyFetchMore, ResetRequested, Finished } searchState = None; - NetJob::Ptr jobPtr; + Task::Ptr jobPtr; std::shared_ptr response = std::make_shared(); }; diff --git a/launcher/ui/pages/modplatform/flame/FlamePage.cpp b/launcher/ui/pages/modplatform/flame/FlamePage.cpp index 9be7ce8b8..584d94ad9 100644 --- a/launcher/ui/pages/modplatform/flame/FlamePage.cpp +++ b/launcher/ui/pages/modplatform/flame/FlamePage.cpp @@ -46,9 +46,12 @@ #include "ui/dialogs/NewInstanceDialog.h" #include "ui/widgets/ProjectItem.h" +#include "net/ApiDownload.h" + static FlameAPI api; -FlamePage::FlamePage(NewInstanceDialog* dialog, QWidget* parent) : QWidget(parent), ui(new Ui::FlamePage), dialog(dialog) +FlamePage::FlamePage(NewInstanceDialog* dialog, QWidget* parent) + : QWidget(parent), ui(new Ui::FlamePage), dialog(dialog), m_fetch_progress(this, false) { ui->setupUi(this); connect(ui->searchButton, &QPushButton::clicked, this, &FlamePage::triggerSearch); @@ -59,6 +62,17 @@ FlamePage::FlamePage(NewInstanceDialog* dialog, QWidget* parent) : QWidget(paren ui->versionSelectionBox->view()->setVerticalScrollBarPolicy(Qt::ScrollBarAsNeeded); ui->versionSelectionBox->view()->parentWidget()->setMaximumHeight(300); + m_search_timer.setTimerType(Qt::TimerType::CoarseTimer); + m_search_timer.setSingleShot(true); + + connect(&m_search_timer, &QTimer::timeout, this, &FlamePage::triggerSearch); + + m_fetch_progress.hideIfInactive(true); + m_fetch_progress.setFixedHeight(24); + m_fetch_progress.progressFormat(""); + + ui->gridLayout->addWidget(&m_fetch_progress, 2, 0, 1, ui->gridLayout->columnCount()); + // index is used to set the sorting with the curseforge api ui->sortByBox->addItem(tr("Sort by Featured")); ui->sortByBox->addItem(tr("Sort by Popularity")); @@ -88,6 +102,11 @@ bool FlamePage::eventFilter(QObject* watched, QEvent* event) triggerSearch(); keyEvent->accept(); return true; + } else { + if (m_search_timer.isActive()) + m_search_timer.stop(); + + m_search_timer.start(350); } } return QWidget::eventFilter(watched, event); @@ -112,9 +131,10 @@ void FlamePage::openedImpl() void FlamePage::triggerSearch() { listModel->searchWithTerm(ui->searchEdit->text(), ui->sortByBox->currentIndex()); + m_fetch_progress.watch(listModel->activeSearchJob().get()); } -void FlamePage::onSelectionChanged(QModelIndex curr, QModelIndex prev) +void FlamePage::onSelectionChanged(QModelIndex curr, [[maybe_unused]] QModelIndex prev) { ui->versionSelectionBox->clear(); @@ -132,7 +152,8 @@ void FlamePage::onSelectionChanged(QModelIndex curr, QModelIndex prev) auto netJob = new NetJob(QString("Flame::PackVersions(%1)").arg(current.name), APPLICATION->network()); auto response = std::make_shared(); int addonId = current.addonId; - netJob->addNetAction(Net::Download::makeByteArray(QString("https://api.curseforge.com/v1/mods/%1/files").arg(addonId), response)); + netJob->addNetAction( + Net::ApiDownload::makeByteArray(QString("https://api.curseforge.com/v1/mods/%1/files").arg(addonId), response)); QObject::connect(netJob, &NetJob::succeeded, this, [this, response, addonId, curr] { if (addonId != current.addonId) { @@ -208,17 +229,17 @@ void FlamePage::suggestCurrent() dialog->setSuggestedPack(current.name, new InstanceImportTask(version.downloadUrl, this, std::move(extra_info))); QString editedLogoName; - editedLogoName = "curseforge_" + current.logoName.section(".", 0, 0); + editedLogoName = "curseforge_" + current.logoName; listModel->getLogo(current.logoName, current.logoUrl, [this, editedLogoName](QString logo) { dialog->setSuggestedIconFromFile(logo, editedLogoName); }); } -void FlamePage::onVersionSelectionChanged(QString data) +void FlamePage::onVersionSelectionChanged(QString version) { bool is_blocked = false; ui->versionSelectionBox->currentData().toInt(&is_blocked); - if (data.isNull() || data.isEmpty() || is_blocked) { + if (version.isNull() || version.isEmpty() || is_blocked) { m_selected_version_index = -1; return; } diff --git a/launcher/ui/pages/modplatform/flame/FlamePage.h b/launcher/ui/pages/modplatform/flame/FlamePage.h index ff5c79750..d35858fbc 100644 --- a/launcher/ui/pages/modplatform/flame/FlamePage.h +++ b/launcher/ui/pages/modplatform/flame/FlamePage.h @@ -39,8 +39,9 @@ #include #include -#include "tasks/Task.h" +#include #include "ui/pages/BasePage.h" +#include "ui/widgets/ProgressWidget.h" namespace Ui { class FlamePage; @@ -86,4 +87,9 @@ class FlamePage : public QWidget, public BasePage { Flame::IndexedPack current; int m_selected_version_index = -1; + + ProgressWidget m_fetch_progress; + + // Used to do instant searching with a delay to cache quick changes + QTimer m_search_timer; }; diff --git a/launcher/ui/pages/modplatform/flame/FlamePage.ui b/launcher/ui/pages/modplatform/flame/FlamePage.ui index 71d195135..f9e1fe67f 100644 --- a/launcher/ui/pages/modplatform/flame/FlamePage.ui +++ b/launcher/ui/pages/modplatform/flame/FlamePage.ui @@ -47,7 +47,7 @@
- + @@ -77,7 +77,7 @@ - + diff --git a/launcher/ui/pages/modplatform/flame/FlameResourceModels.cpp b/launcher/ui/pages/modplatform/flame/FlameResourceModels.cpp index 0fb67c507..7d18e72a6 100644 --- a/launcher/ui/pages/modplatform/flame/FlameResourceModels.cpp +++ b/launcher/ui/pages/modplatform/flame/FlameResourceModels.cpp @@ -31,8 +31,8 @@ void FlameModModel::loadIndexedPackVersions(ModPlatform::IndexedPack& m, QJsonAr auto FlameModModel::loadDependencyVersions(const ModPlatform::Dependency& m, QJsonArray& arr) -> ModPlatform::IndexedVersion { - return FlameMod::loadDependencyVersions(m, arr); -}; + return FlameMod::loadDependencyVersions(m, arr, &m_base_instance); +} auto FlameModModel::documentToArray(QJsonDocument& obj) const -> QJsonArray { @@ -121,4 +121,27 @@ auto FlameTexturePackModel::documentToArray(QJsonDocument& obj) const -> QJsonAr return Json::ensureArray(obj.object(), "data"); } +FlameShaderPackModel::FlameShaderPackModel(const BaseInstance& base) : ShaderPackResourceModel(base, new FlameAPI) {} + +void FlameShaderPackModel::loadIndexedPack(ModPlatform::IndexedPack& m, QJsonObject& obj) +{ + FlameMod::loadIndexedPack(m, obj); +} + +// We already deal with the URLs when initializing the pack, due to the API response's structure +void FlameShaderPackModel::loadExtraPackInfo(ModPlatform::IndexedPack& m, QJsonObject& obj) +{ + FlameMod::loadBody(m, obj); +} + +void FlameShaderPackModel::loadIndexedPackVersions(ModPlatform::IndexedPack& m, QJsonArray& arr) +{ + FlameMod::loadIndexedPackVersions(m, arr, APPLICATION->network(), &m_base_instance); +} + +auto FlameShaderPackModel::documentToArray(QJsonDocument& obj) const -> QJsonArray +{ + return Json::ensureArray(obj.object(), "data"); +} + } // namespace ResourceDownload diff --git a/launcher/ui/pages/modplatform/flame/FlameResourceModels.h b/launcher/ui/pages/modplatform/flame/FlameResourceModels.h index 6cfd6a6f8..76dbd7b3d 100644 --- a/launcher/ui/pages/modplatform/flame/FlameResourceModels.h +++ b/launcher/ui/pages/modplatform/flame/FlameResourceModels.h @@ -68,4 +68,21 @@ class FlameTexturePackModel : public TexturePackResourceModel { auto documentToArray(QJsonDocument& obj) const -> QJsonArray override; }; +class FlameShaderPackModel : public ShaderPackResourceModel { + Q_OBJECT + + public: + FlameShaderPackModel(const BaseInstance&); + ~FlameShaderPackModel() override = default; + + private: + [[nodiscard]] QString debugName() const override { return Flame::debugName() + " (Model)"; } + [[nodiscard]] QString metaEntryBase() const override { return Flame::metaEntryBase(); } + + void loadIndexedPack(ModPlatform::IndexedPack& m, QJsonObject& obj) override; + void loadExtraPackInfo(ModPlatform::IndexedPack& m, QJsonObject& obj) override; + void loadIndexedPackVersions(ModPlatform::IndexedPack& m, QJsonArray& arr) override; + auto documentToArray(QJsonDocument& obj) const -> QJsonArray override; +}; + } // namespace ResourceDownload diff --git a/launcher/ui/pages/modplatform/flame/FlameResourcePages.cpp b/launcher/ui/pages/modplatform/flame/FlameResourcePages.cpp index dc17e705a..23373ec9d 100644 --- a/launcher/ui/pages/modplatform/flame/FlameResourcePages.cpp +++ b/launcher/ui/pages/modplatform/flame/FlameResourcePages.cpp @@ -68,10 +68,10 @@ FlameModPage::FlameModPage(ModDownloadDialog* dialog, BaseInstance& instance) : auto FlameModPage::validateVersion(ModPlatform::IndexedVersion& ver, QString mineVer, - std::optional loaders) const -> bool + std::optional loaders) const -> bool { - Q_UNUSED(loaders); - return ver.mcVersion.contains(mineVer) && !ver.downloadUrl.isEmpty(); + return ver.mcVersion.contains(mineVer) && !ver.downloadUrl.isEmpty() && + (!loaders.has_value() || !ver.loaders || loaders.value() & ver.loaders); } bool FlameModPage::optedOut(ModPlatform::IndexedVersion& ver) const @@ -173,6 +173,45 @@ void FlameTexturePackPage::openUrl(const QUrl& url) TexturePackResourcePage::openUrl(url); } +FlameShaderPackPage::FlameShaderPackPage(ShaderPackDownloadDialog* dialog, BaseInstance& instance) + : ShaderPackResourcePage(dialog, instance) +{ + m_model = new FlameShaderPackModel(instance); + m_ui->packView->setModel(m_model); + + addSortings(); + + // sometimes Qt just ignores virtual slots and doesn't work as intended it seems, + // so it's best not to connect them in the parent's constructor... + connect(m_ui->sortByBox, SIGNAL(currentIndexChanged(int)), this, SLOT(triggerSearch())); + connect(m_ui->packView->selectionModel(), &QItemSelectionModel::currentChanged, this, &FlameShaderPackPage::onSelectionChanged); + connect(m_ui->versionSelectionBox, &QComboBox::currentTextChanged, this, &FlameShaderPackPage::onVersionSelectionChanged); + connect(m_ui->resourceSelectionButton, &QPushButton::clicked, this, &FlameShaderPackPage::onResourceSelected); + + m_ui->packDescription->setMetaEntry(metaEntryBase()); +} + +bool FlameShaderPackPage::optedOut(ModPlatform::IndexedVersion& ver) const +{ + return isOptedOut(ver); +} + +void FlameShaderPackPage::openUrl(const QUrl& url) +{ + if (url.scheme().isEmpty()) { + QString query = url.query(QUrl::FullyDecoded); + + if (query.startsWith("remoteUrl=")) { + // attempt to resolve url from warning page + query.remove(0, 10); + ShaderPackResourcePage::openUrl({ QUrl::fromPercentEncoding(query.toUtf8()) }); // double decoding is necessary + return; + } + } + + ShaderPackResourcePage::openUrl(url); +} + // I don't know why, but doing this on the parent class makes it so that // other mod providers start loading before being selected, at least with // my Qt, so we need to implement this in every derived class... @@ -188,5 +227,9 @@ auto FlameTexturePackPage::shouldDisplay() const -> bool { return true; } +auto FlameShaderPackPage::shouldDisplay() const -> bool +{ + return true; +} } // namespace ResourceDownload diff --git a/launcher/ui/pages/modplatform/flame/FlameResourcePages.h b/launcher/ui/pages/modplatform/flame/FlameResourcePages.h index c6ebc1eac..f2f5cecad 100644 --- a/launcher/ui/pages/modplatform/flame/FlameResourcePages.h +++ b/launcher/ui/pages/modplatform/flame/FlameResourcePages.h @@ -44,6 +44,7 @@ #include "ui/pages/modplatform/ModPage.h" #include "ui/pages/modplatform/ResourcePackPage.h" +#include "ui/pages/modplatform/ShaderPackPage.h" #include "ui/pages/modplatform/TexturePackPage.h" namespace ResourceDownload { @@ -95,7 +96,7 @@ class FlameModPage : public ModPage { bool validateVersion(ModPlatform::IndexedVersion& ver, QString mineVer, - std::optional loaders = {}) const override; + std::optional loaders = {}) const override; bool optedOut(ModPlatform::IndexedVersion& ver) const override; void openUrl(const QUrl& url) override; @@ -155,4 +156,31 @@ class FlameTexturePackPage : public TexturePackResourcePage { void openUrl(const QUrl& url) override; }; +class FlameShaderPackPage : public ShaderPackResourcePage { + Q_OBJECT + + public: + static FlameShaderPackPage* create(ShaderPackDownloadDialog* dialog, BaseInstance& instance) + { + return ShaderPackResourcePage::create(dialog, instance); + } + + FlameShaderPackPage(ShaderPackDownloadDialog* dialog, BaseInstance& instance); + ~FlameShaderPackPage() override = default; + + [[nodiscard]] bool shouldDisplay() const override; + + [[nodiscard]] inline auto displayName() const -> QString override { return Flame::displayName(); } + [[nodiscard]] inline auto icon() const -> QIcon override { return Flame::icon(); } + [[nodiscard]] inline auto id() const -> QString override { return Flame::id(); } + [[nodiscard]] inline auto debugName() const -> QString override { return Flame::debugName(); } + [[nodiscard]] inline auto metaEntryBase() const -> QString override { return Flame::metaEntryBase(); } + + [[nodiscard]] inline auto helpPage() const -> QString override { return ""; } + + bool optedOut(ModPlatform::IndexedVersion& ver) const override; + + void openUrl(const QUrl& url) override; +}; + } // namespace ResourceDownload diff --git a/launcher/ui/pages/modplatform/import_ftb/ImportFTBPage.cpp b/launcher/ui/pages/modplatform/import_ftb/ImportFTBPage.cpp index 5c9ff63b2..d3ead0837 100644 --- a/launcher/ui/pages/modplatform/import_ftb/ImportFTBPage.cpp +++ b/launcher/ui/pages/modplatform/import_ftb/ImportFTBPage.cpp @@ -17,6 +17,7 @@ */ #include "ImportFTBPage.h" +#include "ui/widgets/ProjectItem.h" #include "ui_ImportFTBPage.h" #include @@ -32,17 +33,30 @@ ImportFTBPage::ImportFTBPage(NewInstanceDialog* dialog, QWidget* parent) : QWidg ui->setupUi(this); { + currentModel = new FilterModel(this); listModel = new ListModel(this); + currentModel->setSourceModel(listModel); - ui->modpackList->setModel(listModel); + ui->modpackList->setModel(currentModel); ui->modpackList->setSortingEnabled(true); ui->modpackList->header()->hide(); ui->modpackList->setIndentation(0); ui->modpackList->setIconSize(QSize(42, 42)); + + for (int i = 0; i < currentModel->getAvailableSortings().size(); i++) { + ui->sortByBox->addItem(currentModel->getAvailableSortings().keys().at(i)); + } + + ui->sortByBox->setCurrentText(currentModel->translateCurrentSorting()); } connect(ui->modpackList->selectionModel(), &QItemSelectionModel::currentChanged, this, &ImportFTBPage::onPublicPackSelectionChanged); + connect(ui->sortByBox, &QComboBox::currentTextChanged, this, &ImportFTBPage::onSortingSelectionChanged); + + connect(ui->searchEdit, &QLineEdit::textChanged, this, &ImportFTBPage::triggerSearch); + + ui->modpackList->setItemDelegate(new ProjectItemDelegate(this)); ui->modpackList->selectionModel()->reset(); } @@ -86,7 +100,7 @@ void ImportFTBPage::onPublicPackSelectionChanged(QModelIndex now, QModelIndex pr onPackSelectionChanged(); return; } - Modpack selectedPack = listModel->data(now, Qt::UserRole).value(); + Modpack selectedPack = currentModel->data(now, Qt::UserRole).value(); onPackSelectionChanged(&selectedPack); } @@ -101,4 +115,15 @@ void ImportFTBPage::onPackSelectionChanged(Modpack* pack) dialog->setSuggestedPack(); } +void ImportFTBPage::onSortingSelectionChanged(QString sort) +{ + FilterModel::Sorting toSet = currentModel->getAvailableSortings().value(sort); + currentModel->setSorting(toSet); +} + +void ImportFTBPage::triggerSearch() +{ + currentModel->setSearchTerm(ui->searchEdit->text()); +} + } // namespace FTBImportAPP diff --git a/launcher/ui/pages/modplatform/import_ftb/ImportFTBPage.h b/launcher/ui/pages/modplatform/import_ftb/ImportFTBPage.h index 54c49f7b7..8e9661272 100644 --- a/launcher/ui/pages/modplatform/import_ftb/ImportFTBPage.h +++ b/launcher/ui/pages/modplatform/import_ftb/ImportFTBPage.h @@ -53,12 +53,15 @@ class ImportFTBPage : public QWidget, public BasePage { void suggestCurrent(); void onPackSelectionChanged(Modpack* pack = nullptr); private slots: + void onSortingSelectionChanged(QString data); void onPublicPackSelectionChanged(QModelIndex first, QModelIndex second); + void triggerSearch(); private: bool initialized = false; Modpack selected; ListModel* listModel = nullptr; + FilterModel* currentModel = nullptr; NewInstanceDialog* dialog = nullptr; Ui::ImportFTBPage* ui = nullptr; diff --git a/launcher/ui/pages/modplatform/import_ftb/ImportFTBPage.ui b/launcher/ui/pages/modplatform/import_ftb/ImportFTBPage.ui index 32d548b0d..5e09fb6d1 100644 --- a/launcher/ui/pages/modplatform/import_ftb/ImportFTBPage.ui +++ b/launcher/ui/pages/modplatform/import_ftb/ImportFTBPage.ui @@ -10,8 +10,8 @@ 1011 - - + + @@ -21,6 +21,54 @@ + + + + + + Search and filter... + + + true + + + + + + + Search + + + + + + + + + + + + 265 + 0 + + + + + + + + Qt::Horizontal + + + + 40 + 20 + + + + + + diff --git a/launcher/ui/pages/modplatform/import_ftb/ListModel.cpp b/launcher/ui/pages/modplatform/import_ftb/ListModel.cpp index dc78f451c..134bdc0c3 100644 --- a/launcher/ui/pages/modplatform/import_ftb/ListModel.cpp +++ b/launcher/ui/pages/modplatform/import_ftb/ListModel.cpp @@ -23,7 +23,9 @@ #include #include #include "FileSystem.h" +#include "StringUtils.h" #include "modplatform/import_ftb/PackHelpers.h" +#include "ui/widgets/ProjectItem.h" namespace FTBImportAPP { @@ -71,18 +73,99 @@ QVariant ListModel::data(const QModelIndex& index, int role) const } auto pack = modpacks.at(pos); - if (role == Qt::DisplayRole) { - return pack.name; - } else if (role == Qt::DecorationRole) { - return pack.icon; - } else if (role == Qt::UserRole) { - QVariant v; - v.setValue(pack); - return v; - } else if (role == Qt::ToolTipRole) { - return tr("Minecraft %1").arg(pack.mcVersion); + if (role == Qt::ToolTipRole) { } - return QVariant(); + switch (role) { + case Qt::ToolTipRole: + return tr("Minecraft %1").arg(pack.mcVersion); + case Qt::DecorationRole: + return pack.icon; + case Qt::UserRole: { + QVariant v; + v.setValue(pack); + return v; + } + case Qt::DisplayRole: + return pack.name; + case Qt::SizeHintRole: + return QSize(0, 58); + // Custom data + case UserDataTypes::TITLE: + return pack.name; + case UserDataTypes::DESCRIPTION: + return tr("Minecraft %1").arg(pack.mcVersion); + case UserDataTypes::SELECTED: + return false; + case UserDataTypes::INSTALLED: + return false; + default: + break; + } + + return {}; +} + +FilterModel::FilterModel(QObject* parent) : QSortFilterProxyModel(parent) +{ + currentSorting = Sorting::ByGameVersion; + sortings.insert(tr("Sort by Name"), Sorting::ByName); + sortings.insert(tr("Sort by Game Version"), Sorting::ByGameVersion); +} + +bool FilterModel::lessThan(const QModelIndex& left, const QModelIndex& right) const +{ + Modpack leftPack = sourceModel()->data(left, Qt::UserRole).value(); + Modpack rightPack = sourceModel()->data(right, Qt::UserRole).value(); + + if (currentSorting == Sorting::ByGameVersion) { + Version lv(leftPack.mcVersion); + Version rv(rightPack.mcVersion); + return lv < rv; + + } else if (currentSorting == Sorting::ByName) { + return StringUtils::naturalCompare(leftPack.name, rightPack.name, Qt::CaseSensitive) >= 0; + } + + // UHM, some inavlid value set?! + qWarning() << "Invalid sorting set!"; + return true; +} + +bool FilterModel::filterAcceptsRow([[maybe_unused]] int sourceRow, [[maybe_unused]] const QModelIndex& sourceParent) const +{ + if (searchTerm.isEmpty()) { + return true; + } + QModelIndex index = sourceModel()->index(sourceRow, 0, sourceParent); + Modpack pack = sourceModel()->data(index, Qt::UserRole).value(); + return pack.name.contains(searchTerm, Qt::CaseInsensitive); +} + +void FilterModel::setSearchTerm(const QString term) +{ + searchTerm = term.trimmed(); + invalidate(); +} + +const QMap FilterModel::getAvailableSortings() +{ + return sortings; +} + +QString FilterModel::translateCurrentSorting() +{ + return sortings.key(currentSorting); +} + +void FilterModel::setSorting(Sorting s) +{ + currentSorting = s; + invalidate(); +} + +FilterModel::Sorting FilterModel::getCurrentSorting() +{ + return currentSorting; } } // namespace FTBImportAPP \ No newline at end of file diff --git a/launcher/ui/pages/modplatform/import_ftb/ListModel.h b/launcher/ui/pages/modplatform/import_ftb/ListModel.h index c67aa8963..111928276 100644 --- a/launcher/ui/pages/modplatform/import_ftb/ListModel.h +++ b/launcher/ui/pages/modplatform/import_ftb/ListModel.h @@ -20,11 +20,33 @@ #include #include +#include #include #include "modplatform/import_ftb/PackHelpers.h" namespace FTBImportAPP { +class FilterModel : public QSortFilterProxyModel { + Q_OBJECT + public: + FilterModel(QObject* parent = Q_NULLPTR); + enum Sorting { ByName, ByGameVersion }; + const QMap getAvailableSortings(); + QString translateCurrentSorting(); + void setSorting(Sorting sorting); + Sorting getCurrentSorting(); + void setSearchTerm(QString term); + + protected: + bool filterAcceptsRow(int sourceRow, const QModelIndex& sourceParent) const override; + bool lessThan(const QModelIndex& left, const QModelIndex& right) const override; + + private: + QMap sortings; + Sorting currentSorting; + QString searchTerm; +}; + class ListModel : public QAbstractListModel { Q_OBJECT diff --git a/launcher/ui/pages/modplatform/legacy_ftb/ListModel.cpp b/launcher/ui/pages/modplatform/legacy_ftb/ListModel.cpp index 1731ff2cb..49666cf6e 100644 --- a/launcher/ui/pages/modplatform/legacy_ftb/ListModel.cpp +++ b/launcher/ui/pages/modplatform/legacy_ftb/ListModel.cpp @@ -35,11 +35,13 @@ #include "ListModel.h" #include "Application.h" +#include "net/ApiDownload.h" #include "net/HttpMetaCache.h" #include "net/NetJob.h" #include #include "StringUtils.h" +#include "ui/widgets/ProjectItem.h" #include #include @@ -76,9 +78,22 @@ bool FilterModel::lessThan(const QModelIndex& left, const QModelIndex& right) co return true; } -bool FilterModel::filterAcceptsRow(int sourceRow, const QModelIndex& sourceParent) const +bool FilterModel::filterAcceptsRow([[maybe_unused]] int sourceRow, [[maybe_unused]] const QModelIndex& sourceParent) const { - return true; + if (searchTerm.isEmpty()) { + return true; + } + QModelIndex index = sourceModel()->index(sourceRow, 0, sourceParent); + Modpack pack = sourceModel()->data(index, Qt::UserRole).value(); + if (searchTerm.startsWith("#")) + return pack.packCode == searchTerm.mid(1); + return pack.name.contains(searchTerm, Qt::CaseInsensitive); +} + +void FilterModel::setSearchTerm(const QString term) +{ + searchTerm = term.trimmed(); + invalidate(); } const QMap FilterModel::getAvailableSortings() @@ -138,45 +153,63 @@ QVariant ListModel::data(const QModelIndex& index, int role) const } Modpack pack = modpacks.at(pos); - if (role == Qt::DisplayRole) { - return pack.name + "\n" + translatePackType(pack.type); - } else if (role == Qt::ToolTipRole) { - if (pack.description.length() > 100) { - // some magic to prevent to long tooltips and replace html linebreaks - QString edit = pack.description.left(97); - edit = edit.left(edit.lastIndexOf("
")).left(edit.lastIndexOf(" ")).append("..."); - return edit; + switch (role) { + case Qt::ToolTipRole: { + if (pack.description.length() > 100) { + // some magic to prevent to long tooltips and replace html linebreaks + QString edit = pack.description.left(97); + edit = edit.left(edit.lastIndexOf("
")).left(edit.lastIndexOf(" ")).append("..."); + return edit; + } + return pack.description; } - return pack.description; - } else if (role == Qt::DecorationRole) { - if (m_logoMap.contains(pack.logo)) { - return (m_logoMap.value(pack.logo)); + case Qt::DecorationRole: { + if (m_logoMap.contains(pack.logo)) { + return (m_logoMap.value(pack.logo)); + } + QIcon icon = APPLICATION->getThemedIcon("screenshot-placeholder"); + ((ListModel*)this)->requestLogo(pack.logo); + return icon; } - QIcon icon = APPLICATION->getThemedIcon("screenshot-placeholder"); - ((ListModel*)this)->requestLogo(pack.logo); - return icon; - } else if (role == Qt::ForegroundRole) { - if (pack.broken) { - // FIXME: Hardcoded color - return QColor(255, 0, 50); - } else if (pack.bugged) { - // FIXME: Hardcoded color - // bugged pack, currently only indicates bugged xml - return QColor(244, 229, 66); + case Qt::UserRole: { + QVariant v; + v.setValue(pack); + return v; } - } else if (role == Qt::UserRole) { - QVariant v; - v.setValue(pack); - return v; + case Qt::ForegroundRole: { + if (pack.broken) { + // FIXME: Hardcoded color + return QColor(255, 0, 50); + } else if (pack.bugged) { + // FIXME: Hardcoded color + // bugged pack, currently only indicates bugged xml + return QColor(244, 229, 66); + } + } + case Qt::DisplayRole: + return pack.name; + case Qt::SizeHintRole: + return QSize(0, 58); + // Custom data + case UserDataTypes::TITLE: + return pack.name; + case UserDataTypes::DESCRIPTION: + return pack.description; + case UserDataTypes::SELECTED: + return false; + case UserDataTypes::INSTALLED: + return false; + default: + break; } - return QVariant(); + return {}; } -void ListModel::fill(ModpackList modpacks) +void ListModel::fill(ModpackList modpacks_) { beginResetModel(); - this->modpacks = modpacks; + this->modpacks = modpacks_; endResetModel(); } @@ -229,9 +262,9 @@ void ListModel::requestLogo(QString file) return; } - MetaEntryPtr entry = APPLICATION->metacache()->resolveEntry("FTBPacks", QString("logos/%1").arg(file.section(".", 0, 0))); + MetaEntryPtr entry = APPLICATION->metacache()->resolveEntry("FTBPacks", QString("logos/%1").arg(file)); NetJob* job = new NetJob(QString("FTB Icon Download for %1").arg(file), APPLICATION->network()); - job->addNetAction(Net::Download::makeCached(QUrl(QString(BuildConfig.LEGACY_FTB_CDN_BASE_URL + "static/%1").arg(file)), entry)); + job->addNetAction(Net::ApiDownload::makeCached(QUrl(QString(BuildConfig.LEGACY_FTB_CDN_BASE_URL + "static/%1").arg(file)), entry)); auto fullPath = entry->getFullPath(); QObject::connect(job, &NetJob::finished, this, [this, file, fullPath, job] { @@ -255,7 +288,7 @@ void ListModel::requestLogo(QString file) void ListModel::getLogo(const QString& logo, LogoCallback callback) { if (m_logoMap.contains(logo)) { - callback(APPLICATION->metacache()->resolveEntry("FTBPacks", QString("logos/%1").arg(logo.section(".", 0, 0)))->getFullPath()); + callback(APPLICATION->metacache()->resolveEntry("FTBPacks", QString("logos/%1").arg(logo))->getFullPath()); } else { requestLogo(logo); } diff --git a/launcher/ui/pages/modplatform/legacy_ftb/ListModel.h b/launcher/ui/pages/modplatform/legacy_ftb/ListModel.h index 51a58d991..c802a4b56 100644 --- a/launcher/ui/pages/modplatform/legacy_ftb/ListModel.h +++ b/launcher/ui/pages/modplatform/legacy_ftb/ListModel.h @@ -25,6 +25,7 @@ class FilterModel : public QSortFilterProxyModel { QString translateCurrentSorting(); void setSorting(Sorting sorting); Sorting getCurrentSorting(); + void setSearchTerm(QString term); protected: bool filterAcceptsRow(int sourceRow, const QModelIndex& sourceParent) const override; @@ -33,6 +34,7 @@ class FilterModel : public QSortFilterProxyModel { private: QMap sortings; Sorting currentSorting; + QString searchTerm; }; class ListModel : public QAbstractListModel { diff --git a/launcher/ui/pages/modplatform/legacy_ftb/Page.cpp b/launcher/ui/pages/modplatform/legacy_ftb/Page.cpp index ef8e9892c..4104f1391 100644 --- a/launcher/ui/pages/modplatform/legacy_ftb/Page.cpp +++ b/launcher/ui/pages/modplatform/legacy_ftb/Page.cpp @@ -35,6 +35,7 @@ */ #include "Page.h" +#include "ui/widgets/ProjectItem.h" #include "ui_Page.h" #include @@ -110,6 +111,8 @@ Page::Page(NewInstanceDialog* dialog, QWidget* parent) : QWidget(parent), dialog connect(ui->sortByBox, &QComboBox::currentTextChanged, this, &Page::onSortingSelectionChanged); connect(ui->versionSelectionBox, &QComboBox::currentTextChanged, this, &Page::onVersionSelectionItemChanged); + connect(ui->searchEdit, &QLineEdit::textChanged, this, &Page::triggerSearch); + connect(ui->publicPackList->selectionModel(), &QItemSelectionModel::currentChanged, this, &Page::onPublicPackSelectionChanged); connect(ui->thirdPartyPackList->selectionModel(), &QItemSelectionModel::currentChanged, this, &Page::onThirdPartyPackSelectionChanged); connect(ui->privatePackList->selectionModel(), &QItemSelectionModel::currentChanged, this, &Page::onPrivatePackSelectionChanged); @@ -125,6 +128,9 @@ Page::Page(NewInstanceDialog* dialog, QWidget* parent) : QWidget(parent), dialog ui->thirdPartyPackList->selectionModel()->reset(); ui->privatePackList->selectionModel()->reset(); + ui->publicPackList->setItemDelegate(new ProjectItemDelegate(this)); + ui->thirdPartyPackList->setItemDelegate(new ProjectItemDelegate(this)); + ui->privatePackList->setItemDelegate(new ProjectItemDelegate(this)); onTabChanged(ui->tabWidget->currentIndex()); } @@ -215,7 +221,7 @@ void Page::ftbPrivatePackDataDownloadSuccessfully(Modpack pack) privateListModel->addPack(pack); } -void Page::ftbPrivatePackDataDownloadFailed(QString reason, QString packCode) +void Page::ftbPrivatePackDataDownloadFailed([[maybe_unused]] QString reason, QString packCode) { auto reply = QMessageBox::question(this, tr("FTB private packs"), tr("Failed to download pack information for code %1.\nShould it be removed now?").arg(packCode)); @@ -224,7 +230,7 @@ void Page::ftbPrivatePackDataDownloadFailed(QString reason, QString packCode) } } -void Page::onPublicPackSelectionChanged(QModelIndex now, QModelIndex prev) +void Page::onPublicPackSelectionChanged(QModelIndex now, [[maybe_unused]] QModelIndex prev) { if (!now.isValid()) { onPackSelectionChanged(); @@ -234,7 +240,7 @@ void Page::onPublicPackSelectionChanged(QModelIndex now, QModelIndex prev) onPackSelectionChanged(&selectedPack); } -void Page::onThirdPartyPackSelectionChanged(QModelIndex now, QModelIndex prev) +void Page::onThirdPartyPackSelectionChanged(QModelIndex now, [[maybe_unused]] QModelIndex prev) { if (!now.isValid()) { onPackSelectionChanged(); @@ -244,7 +250,7 @@ void Page::onThirdPartyPackSelectionChanged(QModelIndex now, QModelIndex prev) onPackSelectionChanged(&selectedPack); } -void Page::onPrivatePackSelectionChanged(QModelIndex now, QModelIndex prev) +void Page::onPrivatePackSelectionChanged(QModelIndex now, [[maybe_unused]] QModelIndex prev) { if (!now.isValid()) { onPackSelectionChanged(); @@ -284,20 +290,20 @@ void Page::onPackSelectionChanged(Modpack* pack) suggestCurrent(); } -void Page::onVersionSelectionItemChanged(QString data) +void Page::onVersionSelectionItemChanged(QString version) { - if (data.isNull() || data.isEmpty()) { + if (version.isNull() || version.isEmpty()) { selectedVersion = ""; return; } - selectedVersion = data; + selectedVersion = version; suggestCurrent(); } -void Page::onSortingSelectionChanged(QString data) +void Page::onSortingSelectionChanged(QString sort) { - FilterModel::Sorting toSet = publicFilterModel->getAvailableSortings().value(data); + FilterModel::Sorting toSet = publicFilterModel->getAvailableSortings().value(sort); publicFilterModel->setSorting(toSet); thirdPartyFilterModel->setSorting(toSet); privateFilterModel->setSorting(toSet); @@ -319,6 +325,8 @@ void Page::onTabChanged(int tab) currentModpackInfo = ui->publicPackDescription; } + triggerSearch(); + currentList->selectionModel()->reset(); QModelIndex idx = currentList->currentIndex(); if (idx.isValid()) { @@ -358,4 +366,9 @@ void Page::onRemovePackClicked() onPackSelectionChanged(); } +void Page::triggerSearch() +{ + currentModel->setSearchTerm(ui->searchEdit->text()); +} + } // namespace LegacyFTB diff --git a/launcher/ui/pages/modplatform/legacy_ftb/Page.h b/launcher/ui/pages/modplatform/legacy_ftb/Page.h index a12b0745d..4d317b7c0 100644 --- a/launcher/ui/pages/modplatform/legacy_ftb/Page.h +++ b/launcher/ui/pages/modplatform/legacy_ftb/Page.h @@ -43,7 +43,6 @@ #include "QObjectPtr.h" #include "modplatform/legacy_ftb/PackFetchTask.h" #include "modplatform/legacy_ftb/PackHelpers.h" -#include "tasks/Task.h" #include "ui/pages/BasePage.h" class NewInstanceDialog; @@ -56,8 +55,6 @@ class Page; class ListModel; class FilterModel; -class PrivatePackListModel; -class PrivatePackFilterModel; class PrivatePackManager; class Page : public QWidget, public BasePage { @@ -98,6 +95,8 @@ class Page : public QWidget, public BasePage { void onAddPackClicked(); void onRemovePackClicked(); + void triggerSearch(); + private: FilterModel* currentModel = nullptr; QTreeView* currentList = nullptr; diff --git a/launcher/ui/pages/modplatform/legacy_ftb/Page.ui b/launcher/ui/pages/modplatform/legacy_ftb/Page.ui index ad08dc255..56cba7485 100644 --- a/launcher/ui/pages/modplatform/legacy_ftb/Page.ui +++ b/launcher/ui/pages/modplatform/legacy_ftb/Page.ui @@ -10,8 +10,29 @@ 602 - - + + + + + + + Search and filter... + + + true + + + + + + + Search + + + + + + 0 @@ -36,9 +57,9 @@ - - true - + + true + @@ -50,10 +71,10 @@ - - true - - + + true + + @@ -104,16 +125,16 @@ - - true - - + + true + + - + diff --git a/launcher/ui/pages/modplatform/modrinth/ModrinthModel.cpp b/launcher/ui/pages/modplatform/modrinth/ModrinthModel.cpp index 761e265d2..f691a185d 100644 --- a/launcher/ui/pages/modplatform/modrinth/ModrinthModel.cpp +++ b/launcher/ui/pages/modplatform/modrinth/ModrinthModel.cpp @@ -38,10 +38,12 @@ #include "BuildConfig.h" #include "Json.h" -#include "minecraft/MinecraftInstance.h" -#include "minecraft/PackProfile.h" +#include "modplatform/modrinth/ModrinthAPI.h" +#include "net/NetJob.h" #include "ui/widgets/ProjectItem.h" +#include "net/ApiDownload.h" + #include namespace Modrinth { @@ -115,7 +117,7 @@ auto ModpackListModel::data(const QModelIndex& index, int role) const -> QVarian return {}; } -bool ModpackListModel::setData(const QModelIndex& index, const QVariant& value, int role) +bool ModpackListModel::setData(const QModelIndex& index, const QVariant& value, [[maybe_unused]] int role) { int pos = index.row(); if (pos >= modpacks.size() || pos < 0 || !index.isValid()) @@ -128,7 +130,24 @@ bool ModpackListModel::setData(const QModelIndex& index, const QVariant& value, void ModpackListModel::performPaginatedSearch() { - // TODO: Move to standalone API + if (hasActiveSearchJob()) + return; + + if (currentSearchTerm.startsWith("#")) { + auto projectId = currentSearchTerm.mid(1); + if (!projectId.isEmpty()) { + ResourceAPI::ProjectInfoCallbacks callbacks; + + callbacks.on_fail = [this](QString reason) { searchRequestFailed(reason); }; + callbacks.on_succeed = [this](auto& doc, auto& pack) { searchRequestForOneSucceeded(doc); }; + static const ModrinthAPI api; + if (auto job = api.getProjectInfo({ projectId }, std::move(callbacks)); job) { + jobPtr = job; + jobPtr->start(); + } + return; + } + } // TODO: Move to standalone API auto netJob = makeShared("Modrinth::SearchModpack", APPLICATION->network()); auto searchAllUrl = QString(BuildConfig.MODRINTH_PROD_URL + "/search?" @@ -142,7 +161,7 @@ void ModpackListModel::performPaginatedSearch() .arg(currentSearchTerm) .arg(currentSort); - netJob->addNetAction(Net::Download::makeByteArray(QUrl(searchAllUrl), m_all_response)); + netJob->addNetAction(Net::ApiDownload::makeByteArray(QUrl(searchAllUrl), m_all_response)); QObject::connect(netJob.get(), &NetJob::succeeded, this, [this] { QJsonParseError parse_error_all{}; @@ -165,16 +184,17 @@ void ModpackListModel::performPaginatedSearch() void ModpackListModel::refresh() { - if (jobPtr) { + if (hasActiveSearchJob()) { jobPtr->abort(); searchState = ResetRequested; return; - } else { - beginResetModel(); - modpacks.clear(); - endResetModel(); - searchState = None; } + + beginResetModel(); + modpacks.clear(); + endResetModel(); + searchState = None; + nextSearchOffset = 0; performPaginatedSearch(); } @@ -194,8 +214,6 @@ static auto sortFromIndex(int index) -> QString case 4: return "updated"; } - - return {}; } void ModpackListModel::searchWithTerm(const QString& term, const int sort) @@ -218,9 +236,7 @@ void ModpackListModel::searchWithTerm(const QString& term, const int sort) void ModpackListModel::getLogo(const QString& logo, const QString& logoUrl, LogoCallback callback) { if (m_logoMap.contains(logo)) { - callback(APPLICATION->metacache() - ->resolveEntry(m_parent->metaEntryBase(), QString("logos/%1").arg(logo.section(".", 0, 0))) - ->getFullPath()); + callback(APPLICATION->metacache()->resolveEntry(m_parent->metaEntryBase(), QString("logos/%1").arg(logo))->getFullPath()); } else { requestLogo(logo, logoUrl); } @@ -232,10 +248,9 @@ void ModpackListModel::requestLogo(QString logo, QString url) return; } - MetaEntryPtr entry = - APPLICATION->metacache()->resolveEntry(m_parent->metaEntryBase(), QString("logos/%1").arg(logo.section(".", 0, 0))); + MetaEntryPtr entry = APPLICATION->metacache()->resolveEntry(m_parent->metaEntryBase(), QString("logos/%1").arg(logo)); auto job = new NetJob(QString("%1 Icon Download %2").arg(m_parent->debugName()).arg(logo), APPLICATION->network()); - job->addNetAction(Net::Download::makeCached(QUrl(url), entry)); + job->addNetAction(Net::ApiDownload::makeCached(QUrl(url), entry)); auto fullPath = entry->getFullPath(); QObject::connect(job, &NetJob::succeeded, this, [this, logo, fullPath, job] { @@ -310,9 +325,29 @@ void ModpackListModel::searchRequestFinished(QJsonDocument& doc_all) endInsertRows(); } +void ModpackListModel::searchRequestForOneSucceeded(QJsonDocument& doc) +{ + jobPtr.reset(); + + auto packObj = doc.object(); + + Modrinth::Modpack pack; + try { + Modrinth::loadIndexedPack(pack, packObj); + pack.id = Json::ensureString(packObj, "id", pack.id); + } catch (const JSONValidationError& e) { + qWarning() << "Error while loading mod from " << m_parent->debugName() << ": " << e.cause(); + return; + } + + beginInsertRows(QModelIndex(), modpacks.size(), modpacks.size() + 1); + modpacks.append({ pack }); + endInsertRows(); +} + void ModpackListModel::searchRequestFailed(QString reason) { - auto failed_action = jobPtr->getFailedActions().at(0); + auto failed_action = dynamic_cast(jobPtr.get())->getFailedActions().at(0); if (!failed_action->m_reply) { // Network error QMessageBox::critical(nullptr, tr("Error"), tr("A network error occurred. Could not load modpacks.")); diff --git a/launcher/ui/pages/modplatform/modrinth/ModrinthModel.h b/launcher/ui/pages/modplatform/modrinth/ModrinthModel.h index 721c69f55..2a9d62261 100644 --- a/launcher/ui/pages/modplatform/modrinth/ModrinthModel.h +++ b/launcher/ui/pages/modplatform/modrinth/ModrinthModel.h @@ -73,6 +73,9 @@ class ModpackListModel : public QAbstractListModel { void refresh(); void searchWithTerm(const QString& term, const int sort); + [[nodiscard]] bool hasActiveSearchJob() const { return jobPtr && jobPtr->isRunning(); } + [[nodiscard]] Task::Ptr activeSearchJob() { return hasActiveSearchJob() ? jobPtr : nullptr; } + void getLogo(const QString& logo, const QString& logoUrl, LogoCallback callback); inline auto canFetchMore(const QModelIndex& parent) const -> bool override @@ -83,6 +86,7 @@ class ModpackListModel : public QAbstractListModel { public slots: void searchRequestFinished(QJsonDocument& doc_all); void searchRequestFailed(QString reason); + void searchRequestForOneSucceeded(QJsonDocument&); protected slots: @@ -111,7 +115,7 @@ class ModpackListModel : public QAbstractListModel { int nextSearchOffset = 0; enum SearchState { None, CanPossiblyFetchMore, ResetRequested, Finished } searchState = None; - NetJob::Ptr jobPtr; + Task::Ptr jobPtr; std::shared_ptr m_all_response = std::make_shared(); QByteArray m_specific_response; diff --git a/launcher/ui/pages/modplatform/modrinth/ModrinthPage.cpp b/launcher/ui/pages/modplatform/modrinth/ModrinthPage.cpp index b4d73b2fa..8e2b9a902 100644 --- a/launcher/ui/pages/modplatform/modrinth/ModrinthPage.cpp +++ b/launcher/ui/pages/modplatform/modrinth/ModrinthPage.cpp @@ -46,11 +46,14 @@ #include "ui/widgets/ProjectItem.h" +#include "net/ApiDownload.h" + #include #include #include -ModrinthPage::ModrinthPage(NewInstanceDialog* dialog, QWidget* parent) : QWidget(parent), ui(new Ui::ModrinthPage), dialog(dialog) +ModrinthPage::ModrinthPage(NewInstanceDialog* dialog, QWidget* parent) + : QWidget(parent), ui(new Ui::ModrinthPage), dialog(dialog), m_fetch_progress(this, false) { ui->setupUi(this); @@ -62,6 +65,17 @@ ModrinthPage::ModrinthPage(NewInstanceDialog* dialog, QWidget* parent) : QWidget ui->versionSelectionBox->view()->setVerticalScrollBarPolicy(Qt::ScrollBarAsNeeded); ui->versionSelectionBox->view()->parentWidget()->setMaximumHeight(300); + m_search_timer.setTimerType(Qt::TimerType::CoarseTimer); + m_search_timer.setSingleShot(true); + + connect(&m_search_timer, &QTimer::timeout, this, &ModrinthPage::triggerSearch); + + m_fetch_progress.hideIfInactive(true); + m_fetch_progress.setFixedHeight(24); + m_fetch_progress.progressFormat(""); + + ui->gridLayout->addWidget(&m_fetch_progress, 2, 0, 1, ui->gridLayout->columnCount()); + ui->sortByBox->addItem(tr("Sort by Relevance")); ui->sortByBox->addItem(tr("Sort by Total Downloads")); ui->sortByBox->addItem(tr("Sort by Follows")); @@ -100,12 +114,17 @@ bool ModrinthPage::eventFilter(QObject* watched, QEvent* event) this->triggerSearch(); keyEvent->accept(); return true; + } else { + if (m_search_timer.isActive()) + m_search_timer.stop(); + + m_search_timer.start(350); } } return QObject::eventFilter(watched, event); } -void ModrinthPage::onSelectionChanged(QModelIndex curr, QModelIndex prev) +void ModrinthPage::onSelectionChanged(QModelIndex curr, [[maybe_unused]] QModelIndex prev) { ui->versionSelectionBox->clear(); @@ -127,7 +146,7 @@ void ModrinthPage::onSelectionChanged(QModelIndex curr, QModelIndex prev) QString id = current.id; - netJob->addNetAction(Net::Download::makeByteArray(QString("%1/project/%2").arg(BuildConfig.MODRINTH_PROD_URL, id), response)); + netJob->addNetAction(Net::ApiDownload::makeByteArray(QString("%1/project/%2").arg(BuildConfig.MODRINTH_PROD_URL, id), response)); QObject::connect(netJob, &NetJob::succeeded, this, [this, response, id, curr] { if (id != current.id) { @@ -176,7 +195,7 @@ void ModrinthPage::onSelectionChanged(QModelIndex curr, QModelIndex prev) QString id = current.id; netJob->addNetAction( - Net::Download::makeByteArray(QString("%1/project/%2/version").arg(BuildConfig.MODRINTH_PROD_URL, id), response)); + Net::ApiDownload::makeByteArray(QString("%1/project/%2/version").arg(BuildConfig.MODRINTH_PROD_URL, id), response)); QObject::connect(netJob, &NetJob::succeeded, this, [this, response, id, curr] { if (id != current.id) { @@ -308,11 +327,12 @@ void ModrinthPage::suggestCurrent() void ModrinthPage::triggerSearch() { m_model->searchWithTerm(ui->searchEdit->text(), ui->sortByBox->currentIndex()); + m_fetch_progress.watch(m_model->activeSearchJob().get()); } -void ModrinthPage::onVersionSelectionChanged(QString data) +void ModrinthPage::onVersionSelectionChanged(QString version) { - if (data.isNull() || data.isEmpty()) { + if (version.isNull() || version.isEmpty()) { selectedVersion = ""; return; } diff --git a/launcher/ui/pages/modplatform/modrinth/ModrinthPage.h b/launcher/ui/pages/modplatform/modrinth/ModrinthPage.h index b7054c886..4240dcafb 100644 --- a/launcher/ui/pages/modplatform/modrinth/ModrinthPage.h +++ b/launcher/ui/pages/modplatform/modrinth/ModrinthPage.h @@ -41,7 +41,9 @@ #include "ui/pages/BasePage.h" #include "modplatform/modrinth/ModrinthPackManifest.h" +#include "ui/widgets/ProgressWidget.h" +#include #include namespace Ui { @@ -88,4 +90,9 @@ class ModrinthPage : public QWidget, public BasePage { Modrinth::Modpack current; QString selectedVersion; + + ProgressWidget m_fetch_progress; + + // Used to do instant searching with a delay to cache quick changes + QTimer m_search_timer; }; diff --git a/launcher/ui/pages/modplatform/modrinth/ModrinthPage.ui b/launcher/ui/pages/modplatform/modrinth/ModrinthPage.ui index 6d8b2b675..78a25feae 100644 --- a/launcher/ui/pages/modplatform/modrinth/ModrinthPage.ui +++ b/launcher/ui/pages/modplatform/modrinth/ModrinthPage.ui @@ -10,8 +10,8 @@ 600 - - + + @@ -29,7 +29,7 @@ - + @@ -47,7 +47,7 @@ - + @@ -77,7 +77,7 @@ - + diff --git a/launcher/ui/pages/modplatform/modrinth/ModrinthResourceModels.cpp b/launcher/ui/pages/modplatform/modrinth/ModrinthResourceModels.cpp index 00a0108aa..856018294 100644 --- a/launcher/ui/pages/modplatform/modrinth/ModrinthResourceModels.cpp +++ b/launcher/ui/pages/modplatform/modrinth/ModrinthResourceModels.cpp @@ -39,13 +39,13 @@ void ModrinthModModel::loadExtraPackInfo(ModPlatform::IndexedPack& m, QJsonObjec void ModrinthModModel::loadIndexedPackVersions(ModPlatform::IndexedPack& m, QJsonArray& arr) { - ::Modrinth::loadIndexedPackVersions(m, arr, APPLICATION->network(), &m_base_instance); + ::Modrinth::loadIndexedPackVersions(m, arr, &m_base_instance); } auto ModrinthModModel::loadDependencyVersions(const ModPlatform::Dependency& m, QJsonArray& arr) -> ModPlatform::IndexedVersion { - return ::Modrinth::loadDependencyVersions(m, arr); -}; + return ::Modrinth::loadDependencyVersions(m, arr, &m_base_instance); +} auto ModrinthModModel::documentToArray(QJsonDocument& obj) const -> QJsonArray { @@ -66,7 +66,7 @@ void ModrinthResourcePackModel::loadExtraPackInfo(ModPlatform::IndexedPack& m, Q void ModrinthResourcePackModel::loadIndexedPackVersions(ModPlatform::IndexedPack& m, QJsonArray& arr) { - ::Modrinth::loadIndexedPackVersions(m, arr, APPLICATION->network(), &m_base_instance); + ::Modrinth::loadIndexedPackVersions(m, arr, &m_base_instance); } auto ModrinthResourcePackModel::documentToArray(QJsonDocument& obj) const -> QJsonArray @@ -88,7 +88,7 @@ void ModrinthTexturePackModel::loadExtraPackInfo(ModPlatform::IndexedPack& m, QJ void ModrinthTexturePackModel::loadIndexedPackVersions(ModPlatform::IndexedPack& m, QJsonArray& arr) { - ::Modrinth::loadIndexedPackVersions(m, arr, APPLICATION->network(), &m_base_instance); + ::Modrinth::loadIndexedPackVersions(m, arr, &m_base_instance); } auto ModrinthTexturePackModel::documentToArray(QJsonDocument& obj) const -> QJsonArray @@ -110,7 +110,7 @@ void ModrinthShaderPackModel::loadExtraPackInfo(ModPlatform::IndexedPack& m, QJs void ModrinthShaderPackModel::loadIndexedPackVersions(ModPlatform::IndexedPack& m, QJsonArray& arr) { - ::Modrinth::loadIndexedPackVersions(m, arr, APPLICATION->network(), &m_base_instance); + ::Modrinth::loadIndexedPackVersions(m, arr, &m_base_instance); } auto ModrinthShaderPackModel::documentToArray(QJsonDocument& obj) const -> QJsonArray diff --git a/launcher/ui/pages/modplatform/modrinth/ModrinthResourcePages.cpp b/launcher/ui/pages/modplatform/modrinth/ModrinthResourcePages.cpp index 616c1a815..a4197b225 100644 --- a/launcher/ui/pages/modplatform/modrinth/ModrinthResourcePages.cpp +++ b/launcher/ui/pages/modplatform/modrinth/ModrinthResourcePages.cpp @@ -65,21 +65,9 @@ ModrinthModPage::ModrinthModPage(ModDownloadDialog* dialog, BaseInstance& instan auto ModrinthModPage::validateVersion(ModPlatform::IndexedVersion& ver, QString mineVer, - std::optional loaders) const -> bool + std::optional loaders) const -> bool { - auto loaderCompatible = !loaders.has_value(); - - if (!loaderCompatible) { - auto loaderStrings = ModrinthAPI::getModLoaderStrings(loaders.value()); - for (auto remoteLoader : ver.loaders) { - if (loaderStrings.contains(remoteLoader)) { - loaderCompatible = true; - break; - } - } - } - - return ver.mcVersion.contains(mineVer) && loaderCompatible; + return ver.mcVersion.contains(mineVer) && (!loaders.has_value() || !ver.loaders || loaders.value() & ver.loaders); } ModrinthResourcePackPage::ModrinthResourcePackPage(ResourcePackDownloadDialog* dialog, BaseInstance& instance) diff --git a/launcher/ui/pages/modplatform/modrinth/ModrinthResourcePages.h b/launcher/ui/pages/modplatform/modrinth/ModrinthResourcePages.h index 86ba1ccb2..311bcfe32 100644 --- a/launcher/ui/pages/modplatform/modrinth/ModrinthResourcePages.h +++ b/launcher/ui/pages/modplatform/modrinth/ModrinthResourcePages.h @@ -93,7 +93,7 @@ class ModrinthModPage : public ModPage { [[nodiscard]] inline auto helpPage() const -> QString override { return "Mod-platform"; } - auto validateVersion(ModPlatform::IndexedVersion& ver, QString mineVer, std::optional loaders = {}) const + auto validateVersion(ModPlatform::IndexedVersion& ver, QString mineVer, std::optional loaders = {}) const -> bool override; }; diff --git a/launcher/ui/pages/modplatform/technic/TechnicModel.cpp b/launcher/ui/pages/modplatform/technic/TechnicModel.cpp index 02d7fd5b9..3cd1d9a2d 100644 --- a/launcher/ui/pages/modplatform/technic/TechnicModel.cpp +++ b/launcher/ui/pages/modplatform/technic/TechnicModel.cpp @@ -38,6 +38,9 @@ #include "BuildConfig.h" #include "Json.h" +#include "net/ApiDownload.h" +#include "ui/widgets/ProjectItem.h" + #include Technic::ListModel::ListModel(QObject* parent) : QAbstractListModel(parent) {} @@ -52,21 +55,47 @@ QVariant Technic::ListModel::data(const QModelIndex& index, int role) const } Modpack pack = modpacks.at(pos); - if (role == Qt::DisplayRole) { - return pack.name; - } else if (role == Qt::DecorationRole) { - if (m_logoMap.contains(pack.logoName)) { - return (m_logoMap.value(pack.logoName)); + switch (role) { + case Qt::ToolTipRole: { + if (pack.description.length() > 100) { + // some magic to prevent to long tooltips and replace html linebreaks + QString edit = pack.description.left(97); + edit = edit.left(edit.lastIndexOf("
")).left(edit.lastIndexOf(" ")).append("..."); + return edit; + } + return pack.description; } - QIcon icon = APPLICATION->getThemedIcon("screenshot-placeholder"); - ((ListModel*)this)->requestLogo(pack.logoName, pack.logoUrl); - return icon; - } else if (role == Qt::UserRole) { - QVariant v; - v.setValue(pack); - return v; + case Qt::DecorationRole: { + if (m_logoMap.contains(pack.logoName)) { + return (m_logoMap.value(pack.logoName)); + } + QIcon icon = APPLICATION->getThemedIcon("screenshot-placeholder"); + ((ListModel*)this)->requestLogo(pack.logoName, pack.logoUrl); + return icon; + } + case Qt::UserRole: { + QVariant v; + v.setValue(pack); + return v; + } + case Qt::DisplayRole: + return pack.name; + case Qt::SizeHintRole: + return QSize(0, 58); + // Custom data + case UserDataTypes::TITLE: + return pack.name; + case UserDataTypes::DESCRIPTION: + return pack.description; + case UserDataTypes::SELECTED: + return false; + case UserDataTypes::INSTALLED: + return false; + default: + break; } - return QVariant(); + + return {}; } int Technic::ListModel::columnCount(const QModelIndex& parent) const @@ -85,21 +114,25 @@ void Technic::ListModel::searchWithTerm(const QString& term) return; } currentSearchTerm = term; - if (jobPtr) { + if (hasActiveSearchJob()) { jobPtr->abort(); searchState = ResetRequested; return; - } else { - beginResetModel(); - modpacks.clear(); - endResetModel(); - searchState = None; } + + beginResetModel(); + modpacks.clear(); + endResetModel(); + searchState = None; + performSearch(); } void Technic::ListModel::performSearch() { + if (hasActiveSearchJob()) + return; + auto netJob = makeShared("Technic::Search", APPLICATION->network()); QString searchUrl = ""; if (currentSearchTerm.isEmpty()) { @@ -111,12 +144,15 @@ void Technic::ListModel::performSearch() } else if (currentSearchTerm.startsWith("https://api.technicpack.net/modpack/")) { searchUrl = QString("%1?build=%2").arg(currentSearchTerm, BuildConfig.TECHNIC_API_BUILD); searchMode = Single; + } else if (currentSearchTerm.startsWith("#")) { + searchUrl = QString("https://api.technicpack.net/modpack/%1?build=%2").arg(currentSearchTerm.mid(1), BuildConfig.TECHNIC_API_BUILD); + searchMode = Single; } else { searchUrl = QString("%1search?build=%2&q=%3").arg(BuildConfig.TECHNIC_API_BASE_URL, BuildConfig.TECHNIC_API_BUILD, currentSearchTerm); searchMode = List; } - netJob->addNetAction(Net::Download::makeByteArray(QUrl(searchUrl), response)); + netJob->addNetAction(Net::ApiDownload::makeByteArray(QUrl(searchUrl), response)); jobPtr = netJob; jobPtr->start(); QObject::connect(netJob.get(), &NetJob::succeeded, this, &ListModel::searchRequestFinished); @@ -157,7 +193,7 @@ void Technic::ListModel::searchRequestFinished() pack.logoName = "null"; } else { pack.logoUrl = rawURL; - pack.logoName = rawURL.section(QLatin1Char('/'), -1).section(QLatin1Char('.'), 0, 0); + pack.logoName = rawURL.section(QLatin1Char('/'), -1); } pack.broken = false; newList.append(pack); @@ -179,7 +215,7 @@ void Technic::ListModel::searchRequestFinished() auto iconUrl = Json::requireString(iconObj, "url"); pack.logoUrl = iconUrl; - pack.logoName = iconUrl.section(QLatin1Char('/'), -1).section(QLatin1Char('.'), 0, 0); + pack.logoName = iconUrl.section(QLatin1Char('/'), -1); } else { pack.logoUrl = "null"; pack.logoName = "null"; @@ -254,7 +290,7 @@ void Technic::ListModel::requestLogo(QString logo, QString url) MetaEntryPtr entry = APPLICATION->metacache()->resolveEntry("TechnicPacks", QString("logos/%1").arg(logo)); auto job = new NetJob(QString("Technic Icon Download %1").arg(logo), APPLICATION->network()); - job->addNetAction(Net::Download::makeCached(QUrl(url), entry)); + job->addNetAction(Net::ApiDownload::makeCached(QUrl(url), entry)); auto fullPath = entry->getFullPath(); diff --git a/launcher/ui/pages/modplatform/technic/TechnicModel.h b/launcher/ui/pages/modplatform/technic/TechnicModel.h index d7a635d41..aeb4f3084 100644 --- a/launcher/ui/pages/modplatform/technic/TechnicModel.h +++ b/launcher/ui/pages/modplatform/technic/TechnicModel.h @@ -58,6 +58,9 @@ class ListModel : public QAbstractListModel { void getLogo(const QString& logo, const QString& logoUrl, LogoCallback callback); void searchWithTerm(const QString& term); + [[nodiscard]] bool hasActiveSearchJob() const { return jobPtr && jobPtr->isRunning(); } + [[nodiscard]] Task::Ptr activeSearchJob() { return hasActiveSearchJob() ? jobPtr : nullptr; } + private slots: void searchRequestFinished(); void searchRequestFailed(); diff --git a/launcher/ui/pages/modplatform/technic/TechnicPage.cpp b/launcher/ui/pages/modplatform/technic/TechnicPage.cpp index 881327547..190b7c68f 100644 --- a/launcher/ui/pages/modplatform/technic/TechnicPage.cpp +++ b/launcher/ui/pages/modplatform/technic/TechnicPage.cpp @@ -34,6 +34,7 @@ */ #include "TechnicPage.h" +#include "ui/widgets/ProjectItem.h" #include "ui_TechnicPage.h" #include @@ -49,7 +50,10 @@ #include "Application.h" #include "modplatform/technic/SolderPackManifest.h" -TechnicPage::TechnicPage(NewInstanceDialog* dialog, QWidget* parent) : QWidget(parent), ui(new Ui::TechnicPage), dialog(dialog) +#include "net/ApiDownload.h" + +TechnicPage::TechnicPage(NewInstanceDialog* dialog, QWidget* parent) + : QWidget(parent), ui(new Ui::TechnicPage), dialog(dialog), m_fetch_progress(this, false) { ui->setupUi(this); connect(ui->searchButton, &QPushButton::clicked, this, &TechnicPage::triggerSearch); @@ -57,8 +61,21 @@ TechnicPage::TechnicPage(NewInstanceDialog* dialog, QWidget* parent) : QWidget(p model = new Technic::ListModel(this); ui->packView->setModel(model); + m_search_timer.setTimerType(Qt::TimerType::CoarseTimer); + m_search_timer.setSingleShot(true); + + connect(&m_search_timer, &QTimer::timeout, this, &TechnicPage::triggerSearch); + + m_fetch_progress.hideIfInactive(true); + m_fetch_progress.setFixedHeight(24); + m_fetch_progress.progressFormat(""); + + ui->gridLayout->addWidget(&m_fetch_progress, 2, 0, 1, ui->gridLayout->columnCount()); + connect(ui->packView->selectionModel(), &QItemSelectionModel::currentChanged, this, &TechnicPage::onSelectionChanged); connect(ui->versionSelectionBox, &QComboBox::currentTextChanged, this, &TechnicPage::onVersionSelectionChanged); + + ui->packView->setItemDelegate(new ProjectItemDelegate(this)); } bool TechnicPage::eventFilter(QObject* watched, QEvent* event) @@ -69,6 +86,11 @@ bool TechnicPage::eventFilter(QObject* watched, QEvent* event) triggerSearch(); keyEvent->accept(); return true; + } else { + if (m_search_timer.isActive()) + m_search_timer.stop(); + + m_search_timer.start(350); } } return QWidget::eventFilter(watched, event); @@ -98,9 +120,10 @@ void TechnicPage::openedImpl() void TechnicPage::triggerSearch() { model->searchWithTerm(ui->searchEdit->text()); + m_fetch_progress.watch(model->activeSearchJob().get()); } -void TechnicPage::onSelectionChanged(QModelIndex first, QModelIndex second) +void TechnicPage::onSelectionChanged(QModelIndex first, [[maybe_unused]] QModelIndex second) { ui->versionSelectionBox->clear(); @@ -125,7 +148,7 @@ void TechnicPage::suggestCurrent() return; } - QString editedLogoName = "technic_" + current.logoName.section(".", 0, 0); + QString editedLogoName = "technic_" + current.logoName; model->getLogo(current.logoName, current.logoUrl, [this, editedLogoName](QString logo) { dialog->setSuggestedIconFromFile(logo, editedLogoName); }); @@ -136,7 +159,7 @@ void TechnicPage::suggestCurrent() auto netJob = makeShared(QString("Technic::PackMeta(%1)").arg(current.name), APPLICATION->network()); QString slug = current.slug; - netJob->addNetAction(Net::Download::makeByteArray( + netJob->addNetAction(Net::ApiDownload::makeByteArray( QString("%1modpack/%2?build=%3").arg(BuildConfig.TECHNIC_API_BASE_URL, slug, BuildConfig.TECHNIC_API_BUILD), response)); QObject::connect(netJob.get(), &NetJob::succeeded, this, [this, slug] { jobPtr.reset(); @@ -232,7 +255,7 @@ void TechnicPage::metadataLoaded() auto netJob = makeShared(QString("Technic::SolderMeta(%1)").arg(current.name), APPLICATION->network()); auto url = QString("%1/modpack/%2").arg(current.url, current.slug); - netJob->addNetAction(Net::Download::makeByteArray(QUrl(url), response)); + netJob->addNetAction(Net::ApiDownload::makeByteArray(QUrl(url), response)); QObject::connect(netJob.get(), &NetJob::succeeded, this, &TechnicPage::onSolderLoaded); @@ -304,13 +327,13 @@ void TechnicPage::onSolderLoaded() metadataLoaded(); } -void TechnicPage::onVersionSelectionChanged(QString data) +void TechnicPage::onVersionSelectionChanged(QString version) { - if (data.isNull() || data.isEmpty()) { + if (version.isNull() || version.isEmpty()) { selectedVersion = ""; return; } - selectedVersion = data; + selectedVersion = version; selectVersion(); } diff --git a/launcher/ui/pages/modplatform/technic/TechnicPage.h b/launcher/ui/pages/modplatform/technic/TechnicPage.h index 91b61eaf2..01439337d 100644 --- a/launcher/ui/pages/modplatform/technic/TechnicPage.h +++ b/launcher/ui/pages/modplatform/technic/TechnicPage.h @@ -35,13 +35,14 @@ #pragma once +#include #include #include #include "TechnicData.h" #include "net/NetJob.h" -#include "tasks/Task.h" #include "ui/pages/BasePage.h" +#include "ui/widgets/ProgressWidget.h" namespace Ui { class TechnicPage; @@ -91,4 +92,9 @@ class TechnicPage : public QWidget, public BasePage { NetJob::Ptr jobPtr; std::shared_ptr response = std::make_shared(); + + ProgressWidget m_fetch_progress; + + // Used to do instant searching with a delay to cache quick changes + QTimer m_search_timer; }; diff --git a/launcher/ui/pages/modplatform/technic/TechnicPage.ui b/launcher/ui/pages/modplatform/technic/TechnicPage.ui index 15bf645fb..b988eda2b 100644 --- a/launcher/ui/pages/modplatform/technic/TechnicPage.ui +++ b/launcher/ui/pages/modplatform/technic/TechnicPage.ui @@ -11,7 +11,7 @@ - + @@ -44,7 +44,7 @@ - + diff --git a/launcher/ui/setupwizard/ThemeWizardPage.cpp b/launcher/ui/setupwizard/ThemeWizardPage.cpp index 1c3369219..fe11ed9ae 100644 --- a/launcher/ui/setupwizard/ThemeWizardPage.cpp +++ b/launcher/ui/setupwizard/ThemeWizardPage.cpp @@ -61,7 +61,7 @@ void ThemeWizardPage::updateIcons() void ThemeWizardPage::updateCat() { qDebug() << "Setting Cat"; - ui->catImagePreviewButton->setIcon(QIcon(QString(R"(%1)").arg(APPLICATION->getCatPack()))); + ui->catImagePreviewButton->setIcon(QIcon(QString(R"(%1)").arg(APPLICATION->themeManager()->getCatPack()))); } void ThemeWizardPage::retranslate() diff --git a/launcher/ui/themes/CatPack.cpp b/launcher/ui/themes/CatPack.cpp index f0d8ddd55..bbcb58bc8 100644 --- a/launcher/ui/themes/CatPack.cpp +++ b/launcher/ui/themes/CatPack.cpp @@ -99,18 +99,22 @@ QDate ensureDay(int year, int month, int day) QString JsonCatPack::path() { - const QDate now = QDate::currentDate(); + return path(QDate::currentDate()); +} + +QString JsonCatPack::path(QDate now) +{ for (auto var : m_variants) { QDate startDate = ensureDay(now.year(), var.startTime.month, var.startTime.day); QDate endDate = ensureDay(now.year(), var.endTime.month, var.endTime.day); if (startDate > endDate) { // it's spans over multiple years - if (endDate <= now) // end date is in the past so jump one year into the future for endDate + if (endDate < now) // end date is in the past so jump one year into the future for endDate endDate = endDate.addYears(1); else // end date is in the future so jump one year into the past for startDate startDate = startDate.addYears(-1); } - if (startDate >= now && now >= endDate) + if (startDate <= now && now <= endDate) return var.path; } return m_defaultPath; diff --git a/launcher/ui/themes/CatPack.h b/launcher/ui/themes/CatPack.h index b03a19f03..1d310e796 100644 --- a/launcher/ui/themes/CatPack.h +++ b/launcher/ui/themes/CatPack.h @@ -52,9 +52,9 @@ class BasicCatPack : public CatPack { public: BasicCatPack(QString id, QString name) : m_id(id), m_name(name) {} BasicCatPack(QString id) : BasicCatPack(id, id) {} - virtual QString id() { return m_id; }; - virtual QString name() { return m_name; }; - virtual QString path(); + virtual QString id() override { return m_id; } + virtual QString name() override { return m_name; } + virtual QString path() override; protected: QString m_id; @@ -83,7 +83,8 @@ class JsonCatPack : public BasicCatPack { PartialDate endTime; }; JsonCatPack(QFileInfo& manifestInfo); - virtual QString path(); + virtual QString path() override; + QString path(QDate now); private: QString m_defaultPath; diff --git a/launcher/ui/themes/CustomTheme.cpp b/launcher/ui/themes/CustomTheme.cpp index d1eaf1571..29ecf6254 100644 --- a/launcher/ui/themes/CustomTheme.cpp +++ b/launcher/ui/themes/CustomTheme.cpp @@ -148,7 +148,7 @@ static bool writeThemeJson(const QString& path, try { Json::write(rootObj, path); return true; - } catch (const Exception& e) { + } catch ([[maybe_unused]] const Exception& e) { themeWarningLog() << "Failed to write theme json to" << path; return false; } diff --git a/launcher/ui/themes/IconTheme.cpp b/launcher/ui/themes/IconTheme.cpp new file mode 100644 index 000000000..4bd889854 --- /dev/null +++ b/launcher/ui/themes/IconTheme.cpp @@ -0,0 +1,53 @@ +// SPDX-License-Identifier: GPL-3.0-only +/* + * Prism Launcher - Minecraft Launcher + * Copyright (C) 2023 TheKodeToad + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, version 3. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +#include "IconTheme.h" + +#include +#include + +IconTheme::IconTheme(const QString& id, const QString& path) : m_id(id), m_path(path) {} + +bool IconTheme::load() +{ + const QString path = m_path + "/index.theme"; + + if (!QFile::exists(path)) + return false; + + QSettings settings(path, QSettings::IniFormat); + settings.beginGroup("Icon Theme"); + m_name = settings.value("Name").toString(); + settings.endGroup(); + return !m_name.isNull(); +} + +QString IconTheme::id() +{ + return m_id; +} + +QString IconTheme::path() +{ + return m_path; +} + +QString IconTheme::name() +{ + return m_name; +} diff --git a/launcher/ui/themes/IconTheme.h b/launcher/ui/themes/IconTheme.h new file mode 100644 index 000000000..4e466c6ae --- /dev/null +++ b/launcher/ui/themes/IconTheme.h @@ -0,0 +1,37 @@ +// SPDX-License-Identifier: GPL-3.0-only +/* + * Prism Launcher - Minecraft Launcher + * Copyright (C) 2023 TheKodeToad + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * 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 + +class IconTheme { + public: + IconTheme(const QString& id, const QString& path); + IconTheme() = default; + + bool load(); + QString id(); + QString path(); + QString name(); + + private: + QString m_id; + QString m_path; + QString m_name; +}; diff --git a/launcher/ui/themes/ThemeManager.cpp b/launcher/ui/themes/ThemeManager.cpp index 321f7db4a..0bcac100c 100644 --- a/launcher/ui/themes/ThemeManager.cpp +++ b/launcher/ui/themes/ThemeManager.cpp @@ -2,6 +2,7 @@ /* * Prism Launcher - Minecraft Launcher * Copyright (C) 2022 Tayou + * Copyright (C) 2023 TheKodeToad * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by @@ -31,9 +32,8 @@ #include "Application.h" -ThemeManager::ThemeManager(MainWindow* mainWindow) +ThemeManager::ThemeManager() { - m_mainWindow = mainWindow; initializeThemes(); initializeCatPacks(); } @@ -59,53 +59,110 @@ ITheme* ThemeManager::getTheme(QString themeId) return m_themes[themeId].get(); } +QString ThemeManager::addIconTheme(IconTheme theme) +{ + QString id = theme.id(); + if (m_icons.find(id) == m_icons.end()) + m_icons.emplace(id, std::move(theme)); + else + themeWarningLog() << "IconTheme(" << id << ") not added to prevent id duplication"; + return id; +} + void ThemeManager::initializeThemes() { // Icon themes - { - // TODO: icon themes and instance icons do not mesh well together. Rearrange and fix discrepancies! - // set icon theme search path! - auto searchPaths = QIcon::themeSearchPaths(); - searchPaths.append("iconthemes"); - QIcon::setThemeSearchPaths(searchPaths); - themeDebugLog() << "<> Icon themes initialized."; - } + initializeIcons(); // Initialize widget themes - { - themeDebugLog() << "<> Initializing Widget Themes"; - themeDebugLog() << "Loading Built-in Theme:" << addTheme(std::make_unique()); - auto darkThemeId = addTheme(std::make_unique()); - themeDebugLog() << "Loading Built-in Theme:" << darkThemeId; - themeDebugLog() << "Loading Built-in Theme:" << addTheme(std::make_unique()); + initializeWidgets(); +} - // TODO: need some way to differentiate same name themes in different subdirectories (maybe smaller grey text next to theme name in - // dropdown?) - QString themeFolder = QDir("./themes/").absoluteFilePath(""); - themeDebugLog() << "Theme Folder Path: " << themeFolder; +void ThemeManager::initializeIcons() +{ + // TODO: icon themes and instance icons do not mesh well together. Rearrange and fix discrepancies! + // set icon theme search path! + themeDebugLog() << "<> Initializing Icon Themes"; - QDirIterator directoryIterator(themeFolder, QDir::Dirs | QDir::NoDotAndDotDot); - while (directoryIterator.hasNext()) { - QDir dir(directoryIterator.next()); - QFileInfo themeJson(dir.absoluteFilePath("theme.json")); - if (themeJson.exists()) { - // Load "theme.json" based themes - themeDebugLog() << "Loading JSON Theme from:" << themeJson.absoluteFilePath(); - addTheme(std::make_unique(getTheme(darkThemeId), themeJson, true)); - } else { - // Load pure QSS Themes - QDirIterator stylesheetFileIterator(dir.absoluteFilePath(""), { "*.qss", "*.css" }, QDir::Files); - while (stylesheetFileIterator.hasNext()) { - QFile customThemeFile(stylesheetFileIterator.next()); - QFileInfo customThemeFileInfo(customThemeFile); - themeDebugLog() << "Loading QSS Theme from:" << customThemeFileInfo.absoluteFilePath(); - addTheme(std::make_unique(getTheme(darkThemeId), customThemeFileInfo, false)); - } - } + auto searchPaths = QIcon::themeSearchPaths(); + searchPaths.append(m_iconThemeFolder.path()); + QIcon::setThemeSearchPaths(searchPaths); + + for (const QString& id : builtinIcons) { + IconTheme theme(id, QString(":/icons/%1").arg(id)); + if (!theme.load()) { + themeWarningLog() << "Couldn't load built-in icon theme" << id; + continue; } - themeDebugLog() << "<> Widget themes initialized."; + addIconTheme(std::move(theme)); + themeDebugLog() << "Loaded Built-In Icon Theme" << id; } + + if (!m_iconThemeFolder.mkpath(".")) + themeWarningLog() << "Couldn't create icon theme folder"; + themeDebugLog() << "Icon Theme Folder Path: " << m_iconThemeFolder.absolutePath(); + + QDirIterator directoryIterator(m_iconThemeFolder.path(), QDir::Dirs | QDir::NoDotAndDotDot); + while (directoryIterator.hasNext()) { + QDir dir(directoryIterator.next()); + IconTheme theme(dir.dirName(), dir.path()); + if (!theme.load()) + continue; + + addIconTheme(std::move(theme)); + themeDebugLog() << "Loaded Custom Icon Theme from" << dir.path(); + } + + themeDebugLog() << "<> Icon themes initialized."; +} + +void ThemeManager::initializeWidgets() +{ + themeDebugLog() << "<> Initializing Widget Themes"; + themeDebugLog() << "Loading Built-in Theme:" << addTheme(std::make_unique()); + auto darkThemeId = addTheme(std::make_unique()); + themeDebugLog() << "Loading Built-in Theme:" << darkThemeId; + themeDebugLog() << "Loading Built-in Theme:" << addTheme(std::make_unique()); + + // TODO: need some way to differentiate same name themes in different subdirectories (maybe smaller grey text next to theme name in + // dropdown?) + + if (!m_applicationThemeFolder.mkpath(".")) + themeWarningLog() << "Couldn't create theme folder"; + themeDebugLog() << "Theme Folder Path: " << m_applicationThemeFolder.absolutePath(); + + QDirIterator directoryIterator(m_applicationThemeFolder.path(), QDir::Dirs | QDir::NoDotAndDotDot); + while (directoryIterator.hasNext()) { + QDir dir(directoryIterator.next()); + QFileInfo themeJson(dir.absoluteFilePath("theme.json")); + if (themeJson.exists()) { + // Load "theme.json" based themes + themeDebugLog() << "Loading JSON Theme from:" << themeJson.absoluteFilePath(); + addTheme(std::make_unique(getTheme(darkThemeId), themeJson, true)); + } else { + // Load pure QSS Themes + QDirIterator stylesheetFileIterator(dir.absoluteFilePath(""), { "*.qss", "*.css" }, QDir::Files); + while (stylesheetFileIterator.hasNext()) { + QFile customThemeFile(stylesheetFileIterator.next()); + QFileInfo customThemeFileInfo(customThemeFile); + themeDebugLog() << "Loading QSS Theme from:" << customThemeFileInfo.absoluteFilePath(); + addTheme(std::make_unique(getTheme(darkThemeId), customThemeFileInfo, false)); + } + } + } + + themeDebugLog() << "<> Widget themes initialized."; +} + +QList ThemeManager::getValidIconThemes() +{ + QList ret; + ret.reserve(m_icons.size()); + for (auto&& [id, theme] : m_icons) { + ret.append(&theme); + } + return ret; } QList ThemeManager::getValidApplicationThemes() @@ -128,17 +185,39 @@ QList ThemeManager::getValidCatPacks() return ret; } -void ThemeManager::setIconTheme(const QString& name) +bool ThemeManager::isValidIconTheme(const QString& id) { - QIcon::setThemeName(name); + return !id.isEmpty() && m_icons.find(id) != m_icons.end(); } -void ThemeManager::applyCurrentlySelectedTheme(bool initial) +bool ThemeManager::isValidApplicationTheme(const QString& id) { - setIconTheme(APPLICATION->settings()->get("IconTheme").toString()); - themeDebugLog() << "<> Icon theme set."; - setApplicationTheme(APPLICATION->settings()->get("ApplicationTheme").toString(), initial); - themeDebugLog() << "<> Application theme set."; + return !id.isEmpty() && m_themes.find(id) != m_themes.end(); +} + +QDir ThemeManager::getIconThemesFolder() +{ + return m_iconThemeFolder; +} + +QDir ThemeManager::getApplicationThemesFolder() +{ + return m_applicationThemeFolder; +} + +QDir ThemeManager::getCatPacksFolder() +{ + return m_catPacksFolder; +} + +void ThemeManager::setIconTheme(const QString& name) +{ + if (m_icons.find(name) == m_icons.end()) { + themeWarningLog() << "Tried to set invalid icon theme:" << name; + return; + } + + QIcon::setThemeName(name); } void ThemeManager::setApplicationTheme(const QString& name, bool initial) @@ -154,6 +233,15 @@ void ThemeManager::setApplicationTheme(const QString& name, bool initial) } } +void ThemeManager::applyCurrentlySelectedTheme(bool initial) +{ + auto settings = APPLICATION->settings(); + setIconTheme(settings->get("IconTheme").toString()); + themeDebugLog() << "<> Icon theme set."; + setApplicationTheme(settings->get("ApplicationTheme").toString(), initial); + themeDebugLog() << "<> Application theme set."; +} + QString ThemeManager::getCatPack(QString catName) { auto catIter = m_catPacks.find(!catName.isEmpty() ? catName : APPLICATION->settings()->get("BackgroundCat").toString()); @@ -187,9 +275,9 @@ void ThemeManager::initializeCatPacks() for (auto [id, name] : defaultCats) { addCatPack(std::unique_ptr(new BasicCatPack(id, name))); } - QDir catpacksDir("catpacks"); - QString catpacksFolder = catpacksDir.absoluteFilePath(""); - themeDebugLog() << "CatPacks Folder Path:" << catpacksFolder; + if (!m_catPacksFolder.mkpath(".")) + themeWarningLog() << "Couldn't create catpacks folder"; + themeDebugLog() << "CatPacks Folder Path:" << m_catPacksFolder.absolutePath(); QStringList supportedImageFormats; for (auto format : QImageReader::supportedImageFormats()) { @@ -206,9 +294,9 @@ void ThemeManager::initializeCatPacks() } }; - loadFiles(catpacksDir); + loadFiles(m_catPacksFolder); - QDirIterator directoryIterator(catpacksFolder, QDir::Dirs | QDir::NoDotAndDotDot); + QDirIterator directoryIterator(m_catPacksFolder.path(), QDir::Dirs | QDir::NoDotAndDotDot); while (directoryIterator.hasNext()) { QDir dir(directoryIterator.next()); QFileInfo manifest(dir.absoluteFilePath("catpack.json")); diff --git a/launcher/ui/themes/ThemeManager.h b/launcher/ui/themes/ThemeManager.h index 1ce8c6f49..b5c66677b 100644 --- a/launcher/ui/themes/ThemeManager.h +++ b/launcher/ui/themes/ThemeManager.h @@ -2,6 +2,7 @@ /* * Prism Launcher - Minecraft Launcher * Copyright (C) 2022 Tayou + * Copyright (C) 2023 TheKodeToad * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by @@ -19,6 +20,7 @@ #include +#include "IconTheme.h" #include "ui/MainWindow.h" #include "ui/themes/CatPack.h" #include "ui/themes/ITheme.h" @@ -34,11 +36,17 @@ inline auto themeWarningLog() class ThemeManager { public: - ThemeManager(MainWindow* mainWindow); + ThemeManager(); + QList getValidIconThemes(); QList getValidApplicationThemes(); - void setIconTheme(const QString& name); + bool isValidIconTheme(const QString& id); + bool isValidApplicationTheme(const QString& id); + QDir getIconThemesFolder(); + QDir getApplicationThemesFolder(); + QDir getCatPacksFolder(); void applyCurrentlySelectedTheme(bool initial = false); + void setIconTheme(const QString& name); void setApplicationTheme(const QString& name, bool initial = false); /// @brief Returns the background based on selected and with events (Birthday, XMas, etc.) @@ -49,12 +57,21 @@ class ThemeManager { private: std::map> m_themes; + std::map m_icons; + QDir m_iconThemeFolder{ "iconthemes" }; + QDir m_applicationThemeFolder{ "themes" }; + QDir m_catPacksFolder{ "catpacks" }; std::map> m_catPacks; - MainWindow* m_mainWindow; void initializeThemes(); void initializeCatPacks(); QString addTheme(std::unique_ptr theme); ITheme* getTheme(QString themeId); + QString addIconTheme(IconTheme theme); QString addCatPack(std::unique_ptr catPack); + void initializeIcons(); + void initializeWidgets(); + + const QStringList builtinIcons{ "pe_colored", "pe_light", "pe_dark", "pe_blue", "breeze_light", "breeze_dark", + "OSX", "iOS", "flat", "flat_white", "multimc" }; }; diff --git a/launcher/ui/widgets/InfoFrame.cpp b/launcher/ui/widgets/InfoFrame.cpp index a0fda952f..69f72fea2 100644 --- a/launcher/ui/widgets/InfoFrame.cpp +++ b/launcher/ui/widgets/InfoFrame.cpp @@ -158,12 +158,12 @@ QString InfoFrame::renderColorCodes(QString input) // // TODO: Wrap links inside tags - // https://minecraft.fandom.com/wiki/Formatting_codes#Color_codes + // https://minecraft.wiki/w/Formatting_codes#Color_codes const QMap color_codes_map = { { '0', "#000000" }, { '1', "#0000AA" }, { '2', "#00AA00" }, { '3', "#00AAAA" }, { '4', "#AA0000" }, { '5', "#AA00AA" }, { '6', "#FFAA00" }, { '7', "#AAAAAA" }, { '8', "#555555" }, { '9', "#5555FF" }, { 'a', "#55FF55" }, { 'b', "#55FFFF" }, { 'c', "#FF5555" }, { 'd', "#FF55FF" }, { 'e', "#FFFF55" }, { 'f', "#FFFFFF" } }; - // https://minecraft.fandom.com/wiki/Formatting_codes#Formatting_codes + // https://minecraft.wiki/w/Formatting_codes#Formatting_codes const QMap formatting_codes_map = { { 'l', "b" }, { 'm', "s" }, { 'n', "u" }, { 'o', "i" } }; QString html(""); @@ -348,7 +348,7 @@ void InfoFrame::setImage(QPixmap img) } } -void InfoFrame::descriptionEllipsisHandler(QString link) +void InfoFrame::descriptionEllipsisHandler([[maybe_unused]] QString link) { if (!m_current_box) { m_current_box = CustomMessageBox::selectable(this, "", m_description); @@ -359,7 +359,7 @@ void InfoFrame::descriptionEllipsisHandler(QString link) } } -void InfoFrame::licenseEllipsisHandler(QString link) +void InfoFrame::licenseEllipsisHandler([[maybe_unused]] QString link) { if (!m_current_box) { m_current_box = CustomMessageBox::selectable(this, "", m_license); @@ -370,7 +370,7 @@ void InfoFrame::licenseEllipsisHandler(QString link) } } -void InfoFrame::boxClosed(int result) +void InfoFrame::boxClosed([[maybe_unused]] int result) { m_current_box = nullptr; } diff --git a/launcher/ui/widgets/JavaSettingsWidget.cpp b/launcher/ui/widgets/JavaSettingsWidget.cpp index 8c4048ff2..d2d921918 100644 --- a/launcher/ui/widgets/JavaSettingsWidget.cpp +++ b/launcher/ui/widgets/JavaSettingsWidget.cpp @@ -46,7 +46,7 @@ void JavaSettingsWidget::setupUi() m_verticalLayout = new QVBoxLayout(this); m_verticalLayout->setObjectName(QStringLiteral("verticalLayout")); - m_versionWidget = new VersionSelectWidget(true, this); + m_versionWidget = new VersionSelectWidget(this); m_verticalLayout->addWidget(m_versionWidget); m_horizontalLayout = new QHBoxLayout(); @@ -126,6 +126,7 @@ void JavaSettingsWidget::setupUi() void JavaSettingsWidget::initialize() { m_versionWidget->initialize(APPLICATION->javalist().get()); + m_versionWidget->selectSearch(); m_versionWidget->setResizeOn(2); auto s = APPLICATION->settings(); // Memory @@ -185,12 +186,20 @@ QString JavaSettingsWidget::javaPath() const int JavaSettingsWidget::maxHeapSize() const { - return m_maxMemSpinBox->value(); + auto min = m_minMemSpinBox->value(); + auto max = m_maxMemSpinBox->value(); + if (max < min) + max = min; + return max; } int JavaSettingsWidget::minHeapSize() const { - return m_minMemSpinBox->value(); + auto min = m_minMemSpinBox->value(); + auto max = m_maxMemSpinBox->value(); + if (min > max) + min = max; + return min; } bool JavaSettingsWidget::permGenEnabled() const @@ -213,17 +222,9 @@ void JavaSettingsWidget::memoryValueChanged(int) if (obj == m_minMemSpinBox && min != observedMinMemory) { observedMinMemory = min; actuallyChanged = true; - if (min > max) { - observedMaxMemory = min; - m_maxMemSpinBox->setValue(min); - } } else if (obj == m_maxMemSpinBox && max != observedMaxMemory) { observedMaxMemory = max; actuallyChanged = true; - if (min > max) { - observedMinMemory = max; - m_minMemSpinBox->setValue(max); - } } else if (obj == m_permGenSpinBox && permgen != observedPermGenMemory) { observedPermGenMemory = permgen; actuallyChanged = true; @@ -360,8 +361,8 @@ void JavaSettingsWidget::checkJavaPath(const QString& path) setJavaStatus(JavaStatus::Pending); m_checker.reset(new JavaChecker()); m_checker->m_path = path; - m_checker->m_minMem = m_minMemSpinBox->value(); - m_checker->m_maxMem = m_maxMemSpinBox->value(); + m_checker->m_minMem = minHeapSize(); + m_checker->m_maxMem = maxHeapSize(); if (m_permGenSpinBox->isVisible()) { m_checker->m_permGen = m_permGenSpinBox->value(); } @@ -414,6 +415,9 @@ void JavaSettingsWidget::updateThresholds() } else if (observedMaxMemory > (m_availableMemory * 0.9)) { iconName = "status-yellow"; m_labelMaxMemIcon->setToolTip(tr("Your maximum memory allocation approaches your system memory capacity.")); + } else if (observedMaxMemory < observedMinMemory) { + iconName = "status-yellow"; + m_labelMaxMemIcon->setToolTip(tr("Your maximum memory allocation is smaller than the minimum value")); } else { iconName = "status-good"; m_labelMaxMemIcon->setToolTip(""); diff --git a/launcher/ui/widgets/PageContainer.cpp b/launcher/ui/widgets/PageContainer.cpp index f132643a0..514e1d25c 100644 --- a/launcher/ui/widgets/PageContainer.cpp +++ b/launcher/ui/widgets/PageContainer.cpp @@ -3,6 +3,7 @@ * Prism Launcher - Minecraft Launcher * Copyright (C) 2022 Sefa Eyeoglu * Copyright (c) 2022 Jamie Mansfield + * Copyright (C) 2023 TheKodeToad * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by @@ -131,7 +132,12 @@ BasePage* PageContainer::getPage(QString pageId) return m_model->findPageEntryById(pageId); } -const QList PageContainer::getPages() const +BasePage* PageContainer::selectedPage() const +{ + return m_currentPage; +} + +const QList& PageContainer::getPages() const { return m_model->pages(); } diff --git a/launcher/ui/widgets/PageContainer.h b/launcher/ui/widgets/PageContainer.h index 43aa0e7c0..05be1c3a5 100644 --- a/launcher/ui/widgets/PageContainer.h +++ b/launcher/ui/widgets/PageContainer.h @@ -2,6 +2,7 @@ /* * Prism Launcher - Minecraft Launcher * Copyright (c) 2022 Jamie Mansfield + * Copyright (C) 2023 TheKodeToad * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by @@ -75,9 +76,10 @@ class PageContainer : public QWidget, public BasePageContainer { return false; } - virtual bool selectPage(QString pageId) override; + bool selectPage(QString pageId) override; + BasePage* selectedPage() const override; BasePage* getPage(QString pageId) override; - const QList getPages() const; + const QList& getPages() const; void refreshContainer() override; virtual void setParentContainer(BasePageContainer* container) { m_container = container; }; diff --git a/launcher/ui/widgets/PageContainer_p.h b/launcher/ui/widgets/PageContainer_p.h index 9a7651c75..e61f6e154 100644 --- a/launcher/ui/widgets/PageContainer_p.h +++ b/launcher/ui/widgets/PageContainer_p.h @@ -89,6 +89,8 @@ class PageView : public QListView { setSizePolicy(QSizePolicy::MinimumExpanding, QSizePolicy::Expanding); setItemDelegate(new PageViewDelegate(this)); setHorizontalScrollBarPolicy(Qt::ScrollBarAlwaysOff); + // Adjust margins when using Breeze theme + setProperty("_kde_side_panel_view", true); } virtual QSize sizeHint() const diff --git a/launcher/ui/widgets/ProjectItem.cpp b/launcher/ui/widgets/ProjectItem.cpp index 1481c1b6b..60b92b28b 100644 --- a/launcher/ui/widgets/ProjectItem.cpp +++ b/launcher/ui/widgets/ProjectItem.cpp @@ -34,8 +34,8 @@ void ProjectItemDelegate::paint(QPainter* painter, const QStyleOptionViewItem& o icon_width = icon_size.width(); icon_height = icon_size.height(); - icon_x_margin = (rect.height() - icon_width) / 2; icon_y_margin = (rect.height() - icon_height) / 2; + icon_x_margin = icon_y_margin; // use same margins for consistency } // Centralize icon with a margin to separate from the other elements diff --git a/launcher/ui/widgets/SubTaskProgressBar.cpp b/launcher/ui/widgets/SubTaskProgressBar.cpp index 391021977..b0e62e0f5 100644 --- a/launcher/ui/widgets/SubTaskProgressBar.cpp +++ b/launcher/ui/widgets/SubTaskProgressBar.cpp @@ -26,7 +26,7 @@ unique_qobject_ptr SubTaskProgressBar::create(QWidget* paren return unique_qobject_ptr(progress_bar); } -SubTaskProgressBar::SubTaskProgressBar(QWidget* parent) : ui(new Ui::SubTaskProgressBar) +SubTaskProgressBar::SubTaskProgressBar(QWidget* parent) : QWidget(parent), ui(new Ui::SubTaskProgressBar) { ui->setupUi(this); } diff --git a/launcher/ui/widgets/ThemeCustomizationWidget.cpp b/launcher/ui/widgets/ThemeCustomizationWidget.cpp index d26906bd3..0de97441f 100644 --- a/launcher/ui/widgets/ThemeCustomizationWidget.cpp +++ b/launcher/ui/widgets/ThemeCustomizationWidget.cpp @@ -19,6 +19,7 @@ #include "ui_ThemeCustomizationWidget.h" #include "Application.h" +#include "DesktopServices.h" #include "ui/themes/ITheme.h" #include "ui/themes/ThemeManager.h" @@ -31,6 +32,13 @@ ThemeCustomizationWidget::ThemeCustomizationWidget(QWidget* parent) : QWidget(pa connect(ui->widgetStyleComboBox, QOverload::of(&QComboBox::currentIndexChanged), this, &ThemeCustomizationWidget::applyWidgetTheme); connect(ui->backgroundCatComboBox, QOverload::of(&QComboBox::currentIndexChanged), this, &ThemeCustomizationWidget::applyCatTheme); + + connect(ui->iconsFolder, &QPushButton::clicked, this, + [] { DesktopServices::openDirectory(APPLICATION->themeManager()->getIconThemesFolder().path()); }); + connect(ui->widgetStyleFolder, &QPushButton::clicked, this, + [] { DesktopServices::openDirectory(APPLICATION->themeManager()->getApplicationThemesFolder().path()); }); + connect(ui->catPackFolder, &QPushButton::clicked, this, + [] { DesktopServices::openDirectory(APPLICATION->themeManager()->getCatPacksFolder().path()); }); } ThemeCustomizationWidget::~ThemeCustomizationWidget() @@ -67,7 +75,7 @@ void ThemeCustomizationWidget::showFeatures(ThemeFields features) ui->iconsComboBox->setEnabled(features & ThemeFields::ICONS); ui->iconsLabel->setEnabled(features & ThemeFields::ICONS); ui->widgetStyleComboBox->setEnabled(features & ThemeFields::WIDGETS); - ui->widgetThemeLabel->setEnabled(features & ThemeFields::WIDGETS); + ui->widgetStyleLabel->setEnabled(features & ThemeFields::WIDGETS); ui->backgroundCatComboBox->setEnabled(features & ThemeFields::CAT); ui->backgroundCatLabel->setEnabled(features & ThemeFields::CAT); } @@ -76,11 +84,10 @@ void ThemeCustomizationWidget::applyIconTheme(int index) { auto settings = APPLICATION->settings(); auto originalIconTheme = settings->get("IconTheme").toString(); - auto& newIconTheme = m_iconThemeOptions[index].first; - settings->set("IconTheme", newIconTheme); - + auto newIconTheme = ui->iconsComboBox->currentData().toString(); if (originalIconTheme != newIconTheme) { - APPLICATION->applyCurrentlySelectedTheme(); + settings->set("IconTheme", newIconTheme); + APPLICATION->themeManager()->applyCurrentlySelectedTheme(); } emit currentIconThemeChanged(index); @@ -93,7 +100,7 @@ void ThemeCustomizationWidget::applyWidgetTheme(int index) auto newAppTheme = ui->widgetStyleComboBox->currentData().toString(); if (originalAppTheme != newAppTheme) { settings->set("ApplicationTheme", newAppTheme); - APPLICATION->applyCurrentlySelectedTheme(); + APPLICATION->themeManager()->applyCurrentlySelectedTheme(); } emit currentWidgetThemeChanged(index); @@ -121,18 +128,23 @@ void ThemeCustomizationWidget::loadSettings() { auto settings = APPLICATION->settings(); - auto iconTheme = settings->get("IconTheme").toString(); - for (auto& iconThemeFromList : m_iconThemeOptions) { - QIcon iconForComboBox = QIcon(QString(":/icons/%1/scalable/settings").arg(iconThemeFromList.first)); - ui->iconsComboBox->addItem(iconForComboBox, iconThemeFromList.second); - if (iconTheme == iconThemeFromList.first) { - ui->iconsComboBox->setCurrentIndex(ui->iconsComboBox->count() - 1); + { + auto currentIconTheme = settings->get("IconTheme").toString(); + auto iconThemes = APPLICATION->themeManager()->getValidIconThemes(); + int idx = 0; + for (auto iconTheme : iconThemes) { + QIcon iconForComboBox = QIcon(iconTheme->path() + "/scalable/settings"); + ui->iconsComboBox->addItem(iconForComboBox, iconTheme->name(), iconTheme->id()); + if (currentIconTheme == iconTheme->id()) { + ui->iconsComboBox->setCurrentIndex(idx); + } + idx++; } } { auto currentTheme = settings->get("ApplicationTheme").toString(); - auto themes = APPLICATION->getValidApplicationThemes(); + auto themes = APPLICATION->themeManager()->getValidApplicationThemes(); int idx = 0; for (auto& theme : themes) { ui->widgetStyleComboBox->addItem(theme->name(), theme->id()); @@ -144,7 +156,7 @@ void ThemeCustomizationWidget::loadSettings() } auto cat = settings->get("BackgroundCat").toString(); - for (auto& catFromList : APPLICATION->getValidCatPacks()) { + for (auto& catFromList : APPLICATION->themeManager()->getValidCatPacks()) { QIcon catIcon = QIcon(QString("%1").arg(catFromList->path())); ui->backgroundCatComboBox->addItem(catIcon, catFromList->name(), catFromList->id()); if (cat == catFromList->id()) { diff --git a/launcher/ui/widgets/ThemeCustomizationWidget.h b/launcher/ui/widgets/ThemeCustomizationWidget.h index af47c788a..cef5fb6c6 100644 --- a/launcher/ui/widgets/ThemeCustomizationWidget.h +++ b/launcher/ui/widgets/ThemeCustomizationWidget.h @@ -31,7 +31,7 @@ class ThemeCustomizationWidget : public QWidget { public: explicit ThemeCustomizationWidget(QWidget* parent = nullptr); - ~ThemeCustomizationWidget(); + ~ThemeCustomizationWidget() override; void showFeatures(ThemeFields features); @@ -52,18 +52,4 @@ class ThemeCustomizationWidget : public QWidget { private: Ui::ThemeCustomizationWidget* ui; - - // TODO finish implementing - QList> m_iconThemeOptions{ { "pe_colored", QObject::tr("Simple (Colored Icons)") }, - { "pe_light", QObject::tr("Simple (Light Icons)") }, - { "pe_dark", QObject::tr("Simple (Dark Icons)") }, - { "pe_blue", QObject::tr("Simple (Blue Icons)") }, - { "breeze_light", QObject::tr("Breeze Light") }, - { "breeze_dark", QObject::tr("Breeze Dark") }, - { "OSX", QObject::tr("OSX") }, - { "iOS", QObject::tr("iOS") }, - { "flat", QObject::tr("Flat") }, - { "flat_white", QObject::tr("Flat (White)") }, - { "multimc", QObject::tr("Legacy") }, - { "custom", QObject::tr("Custom") } }; }; diff --git a/launcher/ui/widgets/ThemeCustomizationWidget.ui b/launcher/ui/widgets/ThemeCustomizationWidget.ui index f216a610e..4503181c2 100644 --- a/launcher/ui/widgets/ThemeCustomizationWidget.ui +++ b/launcher/ui/widgets/ThemeCustomizationWidget.ui @@ -40,22 +40,43 @@ - - - - 0 - 0 - - - - Qt::StrongFocus - - + + + + + + 0 + 0 + + + + Qt::StrongFocus + + + + + + + View icon themes folder. + + + + + + + .. + + + true + + + + - + - &Colors + &Widgets widgetStyleComboBox @@ -63,17 +84,38 @@ - - - - 0 - 0 - - - - Qt::StrongFocus - - + + + + + + 0 + 0 + + + + Qt::StrongFocus + + + + + + + View widget themes folder. + + + + + + + .. + + + true + + + + @@ -89,7 +131,7 @@ - + @@ -107,15 +149,15 @@ - + - The cat appears in the background and is not shown by default. It is only made visible when pressing the Cat button in the Toolbar. + View cat packs folder. - + .. diff --git a/launcher/ui/widgets/VariableSizedImageObject.cpp b/launcher/ui/widgets/VariableSizedImageObject.cpp index 4daf513a3..f655fc38d 100644 --- a/launcher/ui/widgets/VariableSizedImageObject.cpp +++ b/launcher/ui/widgets/VariableSizedImageObject.cpp @@ -25,6 +25,7 @@ #include "Application.h" +#include "net/ApiDownload.h" #include "net/NetJob.h" enum FormatProperties { ImageData = QTextFormat::UserProperty + 1 }; @@ -97,7 +98,7 @@ void VariableSizedImageObject::loadImage(QTextDocument* doc, const QUrl& source, QString("images/%1").arg(QString(QCryptographicHash::hash(source.toEncoded(), QCryptographicHash::Algorithm::Sha1).toHex()))); auto job = new NetJob(QString("Load Image: %1").arg(source.fileName()), APPLICATION->network()); - job->addNetAction(Net::Download::makeCached(source, entry)); + job->addNetAction(Net::ApiDownload::makeCached(source, entry)); auto full_entry_path = entry->getFullPath(); auto source_url = source; diff --git a/launcher/ui/widgets/VersionSelectWidget.cpp b/launcher/ui/widgets/VersionSelectWidget.cpp index 9647dc799..a24630b31 100644 --- a/launcher/ui/widgets/VersionSelectWidget.cpp +++ b/launcher/ui/widgets/VersionSelectWidget.cpp @@ -11,9 +11,7 @@ #include "ui/dialogs/CustomMessageBox.h" -VersionSelectWidget::VersionSelectWidget(QWidget* parent) : VersionSelectWidget(false, parent) {} - -VersionSelectWidget::VersionSelectWidget(bool focusSearch, QWidget* parent) : QWidget(parent), focusSearch(focusSearch) +VersionSelectWidget::VersionSelectWidget(QWidget* parent) : QWidget(parent) { setObjectName(QStringLiteral("VersionSelectWidget")); verticalLayout = new QVBoxLayout(this); @@ -114,9 +112,6 @@ void VersionSelectWidget::initialize(BaseVersionList* vlist) listView->header()->setSectionResizeMode(QHeaderView::ResizeToContents); listView->header()->setSectionResizeMode(resizeOnColumn, QHeaderView::Stretch); - if (focusSearch) - search->setFocus(); - if (!m_vlist->isLoaded()) { loadList(); } else { @@ -199,6 +194,16 @@ void VersionSelectWidget::selectCurrent() } } +void VersionSelectWidget::selectSearch() +{ + search->setFocus(); +} + +VersionListView* VersionSelectWidget::view() +{ + return listView; +} + void VersionSelectWidget::selectRecommended() { auto idx = m_proxyModel->getRecommended(); @@ -221,14 +226,19 @@ BaseVersion::Ptr VersionSelectWidget::selectedVersion() const return variant.value(); } +void VersionSelectWidget::setFuzzyFilter(BaseVersionList::ModelRoles role, QString filter) +{ + m_proxyModel->setFilter(role, new ContainsFilter(filter)); +} + void VersionSelectWidget::setExactFilter(BaseVersionList::ModelRoles role, QString filter) { m_proxyModel->setFilter(role, new ExactFilter(filter)); } -void VersionSelectWidget::setFuzzyFilter(BaseVersionList::ModelRoles role, QString filter) +void VersionSelectWidget::setExactIfPresentFilter(BaseVersionList::ModelRoles role, QString filter) { - m_proxyModel->setFilter(role, new ContainsFilter(filter)); + m_proxyModel->setFilter(role, new ExactIfPresentFilter(filter)); } void VersionSelectWidget::setFilter(BaseVersionList::ModelRoles role, Filter* filter) diff --git a/launcher/ui/widgets/VersionSelectWidget.h b/launcher/ui/widgets/VersionSelectWidget.h index b7f401019..99729fbdf 100644 --- a/launcher/ui/widgets/VersionSelectWidget.h +++ b/launcher/ui/widgets/VersionSelectWidget.h @@ -51,7 +51,6 @@ class VersionSelectWidget : public QWidget { Q_OBJECT public: explicit VersionSelectWidget(QWidget* parent); - explicit VersionSelectWidget(bool focusSearch = false, QWidget* parent = 0); ~VersionSelectWidget(); //! loads the list if needed. @@ -64,15 +63,19 @@ class VersionSelectWidget : public QWidget { BaseVersion::Ptr selectedVersion() const; void selectRecommended(); void selectCurrent(); + void selectSearch(); + VersionListView* view(); void setCurrentVersion(const QString& version); void setFuzzyFilter(BaseVersionList::ModelRoles role, QString filter); void setExactFilter(BaseVersionList::ModelRoles role, QString filter); + void setExactIfPresentFilter(BaseVersionList::ModelRoles role, QString filter); void setFilter(BaseVersionList::ModelRoles role, Filter* filter); void setEmptyString(QString emptyString); void setEmptyErrorString(QString emptyErrorString); void setEmptyMode(VersionListView::EmptyMode mode); void setResizeOn(int column); + bool eventFilter(QObject* watched, QEvent* event) override; signals: @@ -97,7 +100,6 @@ class VersionSelectWidget : public QWidget { int resizeOnColumn = 0; Task* loadTask; bool preselectedAlready = false; - bool focusSearch; QVBoxLayout* verticalLayout = nullptr; VersionListView* listView = nullptr; diff --git a/launcher/ui/widgets/WideBar.cpp b/launcher/ui/widgets/WideBar.cpp index b793d84f5..46caaaef2 100644 --- a/launcher/ui/widgets/WideBar.cpp +++ b/launcher/ui/widgets/WideBar.cpp @@ -204,8 +204,10 @@ static void copyAction(QAction* from, QAction* to) void WideBar::showVisibilityMenu(QPoint const& position) { - if (!m_bar_menu) + if (!m_bar_menu) { m_bar_menu = std::make_unique(this); + m_bar_menu->setTearOffEnabled(true); + } if (m_menu_state == MenuState::Dirty) { for (auto* old_action : m_bar_menu->actions()) diff --git a/libraries/LocalPeer/src/LocalPeer.cpp b/libraries/LocalPeer/src/LocalPeer.cpp index ab528c2a1..bd407042f 100644 --- a/libraries/LocalPeer/src/LocalPeer.cpp +++ b/libraries/LocalPeer/src/LocalPeer.cpp @@ -220,5 +220,5 @@ void LocalPeer::receiveConnection() socket->waitForBytesWritten(1000); socket->waitForDisconnected(1000); // make sure client reads ack delete socket; - emit messageReceived(uMsg); //### (might take a long time to return) + emit messageReceived(uMsg); // ### (might take a long time to return) } diff --git a/nix/NIX.md b/nix/NIX.md deleted file mode 100644 index aa945acc6..000000000 --- a/nix/NIX.md +++ /dev/null @@ -1,89 +0,0 @@ -# Running on Nix - -## Putting it in your system configuration - -### On flakes-enabled nix - -#### Directly installing - -The `prismlauncher` flake provides a package which you can install along with -the rest of your packages - -```nix -# In your flake.nix: -{ - inputs = { - prismlauncher.url = "github:PrismLauncher/PrismLauncher"; - }; -} -``` - -```nix -# And in your system configuration: -environment.systemPackages = [ prismlauncher.packages.${pkgs.system}.prismlauncher ]; - -# Or in your home-manager configuration: -home.packages = [ prismlauncher.packages.${pkgs.system}.prismlauncher ]; -``` - -#### Using the overlay - -Alternatively, you can overlay the prismlauncher version in nixpkgs which will -allow you to install using `pkgs` as you normally would while also using the -latest version - -```nix -# In your flake.nix: -{ - inputs = { - prismlauncher.url = "github:PrismLauncher/PrismLauncher"; - }; -} -``` - -```nix -# And in your system configuration: -nixpkgs.overlays = [ inputs.prismlauncher.overlay ]; -environment.systemPackages = [ pkgs.prismlauncher ]; - -# Or in your home-manager configuration: -config.nixpkgs.overlays = [ inputs.prismlauncher.overlay ]; -home.packages = [ pkgs.prismlauncher ]; -``` - -### Without flakes-enabled nix - -
-Using channels - -```sh -nix-channel --add https://github.com/PrismLauncher/PrismLauncher/archive/master.tar.gz prismlauncher -nix-channel --update prismlauncher -nix-env -iA prismlauncher -``` - -
- -
-Using the overlay - -```nix -# In your configuration.nix: -{ - nixpkgs.overlays = [ - (import (builtins.fetchTarball "https://github.com/PrismLauncher/PrismLauncher/archive/develop.tar.gz")).overlay - ]; - - environment.systemPackages = with pkgs; [ prismlauncher ]; -} -``` - -
- -## Running ad-hoc - -If you're on a flakes-enabled nix you can run the launcher in one-line - -```sh -nix run github:PrismLauncher/PrismLauncher -``` diff --git a/nix/README.md b/nix/README.md new file mode 100644 index 000000000..7a422fc5b --- /dev/null +++ b/nix/README.md @@ -0,0 +1,193 @@ +# Prism Launcher Nix Packaging + +## Installing a stable release (nixpkgs) + +Prism Launcher is packaged in [nixpkgs](https://github.com/NixOS/nixpkgs/) since 22.11. + +See [Package variants](#package-variants) for a list of available packages. + +## Installing a development release (flake) + +We use [garnix](https://garnix.io/) to build and cache our development builds. +If you want to avoid rebuilds you may add the garnix cache to your substitutors. + +Example (NixOS): + +```nix +{...}: +{ + nix.settings = { + trusted-substituters = [ + "https://cache.garnix.io" + ]; + + trusted-public-keys = [ + "cache.garnix.io:CTFPyKSLcx5RMJKfLo5EEPUObbA78b0YQ2DTCJXqr9g=" + ]; + }; +} +``` + +### Using the overlay + +After adding `github:PrismLauncher/PrismLauncher` to your flake inputs, you can add the `default` overlay to your nixpkgs instance. + +Example: + +```nix +{ + inputs = { + nixpkgs.url = "github:NixOS/nixpkgs/nixos-unstable"; + prismlauncher = { + url = "github:PrismLauncher/PrismLauncher"; + # Optional: Override the nixpkgs input of prismlauncher to use the same revision as the rest of your flake + # Note that overriding any input of prismlauncher may break reproducibility + # inputs.nixpkgs.follows = "nixpkgs"; + }; + }; + + outputs = {nixpkgs, prismlauncher}: { + nixosConfigurations.foo = nixpkgs.lib.nixosSystem { + system = "x86_64-linux"; + + modules = [ + ({pkgs, ...}: { + nixpkgs.overlays = [prismlauncher.overlays.default]; + + environment.systemPackages = [pkgs.prismlauncher]; + }) + ]; + }; + } +} +``` + +### Installing the package directly + +Alternatively, if you don't want to use an overlay, you can install Prism Launcher directly by installing the `prismlauncher` package. +This way the installed package is fully reproducible. + +Example: + +```nix +{ + inputs = { + nixpkgs.url = "github:NixOS/nixpkgs/nixos-unstable"; + prismlauncher = { + url = "github:PrismLauncher/PrismLauncher"; + # Optional: Override the nixpkgs input of prismlauncher to use the same revision as the rest of your flake + # Note that overriding any input of prismlauncher may break reproducibility + # inputs.nixpkgs.follows = "nixpkgs"; + }; + }; + + outputs = {nixpkgs, prismlauncher}: { + nixosConfigurations.foo = nixpkgs.lib.nixosSystem { + system = "x86_64-linux"; + + modules = [ + ({pkgs, ...}: { + environment.systemPackages = [prismlauncher.packages.${pkgs.system}.prismlauncher]; + }) + ]; + }; + } +} +``` + +### Installing the package ad-hoc (`nix shell`, `nix run`, etc.) + +You can simply call the default package of this flake. + +Example: + +```shell +nix run github:PrismLauncher/PrismLauncher + +nix shell github:PrismLauncher/PrismLauncher + +nix profile install github:PrismLauncher/PrismLauncher +``` + +## Installing a development release (without flakes) + +We use [garnix](https://garnix.io/) to build and cache our development builds. +If you want to avoid rebuilds you may add the garnix cache to your substitutors. + +Example (NixOS): + +```nix +{...}: +{ + nix.settings = { + trusted-substituters = [ + "https://cache.garnix.io" + ]; + + trusted-public-keys = [ + "cache.garnix.io:CTFPyKSLcx5RMJKfLo5EEPUObbA78b0YQ2DTCJXqr9g=" + ]; + }; +} +``` + +### Using the overlay (`fetchTarball`) + +We use flake-compat to allow using this Flake on a system that doesn't use flakes. + +Example: + +```nix +{pkgs, ...}: { + nixpkgs.overlays = [(import (builtins.fetchTarball "https://github.com/PrismLauncher/PrismLauncher/archive/develop.tar.gz")).overlays.default]; + + environment.systemPackages = [pkgs.prismlauncher]; +} +``` + +### Installing the package directly (`fetchTarball`) + +Alternatively, if you don't want to use an overlay, you can install Prism Launcher directly by installing the `prismlauncher` package. +This way the installed package is fully reproducible. + +Example: + +```nix +{pkgs, ...}: { + environment.systemPackages = [(import (builtins.fetchTarball "https://github.com/PrismLauncher/PrismLauncher/archive/develop.tar.gz")).packages.${pkgs.system}.prismlauncher]; +} +``` + +### Installing the package ad-hoc (`nix-env`) + +You can add this repository as a channel and install its packages that way. + +Example: + +```shell +nix-channel --add https://github.com/PrismLauncher/PrismLauncher/archive/develop.tar.gz prismlauncher + +nix-channel --update prismlauncher + +nix-env -iA prismlauncher.prismlauncher +``` + +## Package variants + +Both Nixpkgs and this repository offer the following packages: + +- `prismlauncher` - Preferred build using Qt 6 +- `prismlauncher-qt5` - Legacy build using Qt 5 (i.e. for Qt 5 theming support) + +Both of these packages also have `-unwrapped` counterparts, that are not wrapped and can therefore be customized even further than what the wrapper packages offer. + +### Customizing wrapped packages + +The wrapped packages (`prismlauncher` and `prismlauncher-qt5`) offer some build parameters to further customize the launcher's environment. + +The following parameters can be overridden: + +- `msaClientID` (default: `null`, requires full rebuild!) Client ID used for Microsoft Authentication +- `gamemodeSupport` (default: `true`) Turn on/off support for [Feral GameMode](https://github.com/FeralInteractive/gamemode) +- `jdks` (default: `[ jdk17 jdk8 ]`) Java runtimes added to `PRISMLAUNCHER_JAVA_PATHS` variable +- `additionalLibs` (default: `[ ]`) Additional libraries that will be added to `LD_LIBRARY_PATH` diff --git a/nix/default.nix b/nix/default.nix deleted file mode 100644 index 47172927a..000000000 --- a/nix/default.nix +++ /dev/null @@ -1,32 +0,0 @@ -{ - inputs, - self, - ... -}: { - imports = [ - ./dev.nix - ./distribution.nix - ]; - - _module.args = { - # User-friendly version number. - version = builtins.substring 0 8 self.lastModifiedDate; - }; - - perSystem = {system, ...}: { - # Nixpkgs instantiated for supported systems with our overlay. - _module.args.pkgs = import inputs.nixpkgs { - inherit system; - overlays = [self.overlays.default]; - }; - }; - - # Supported systems. - systems = [ - "x86_64-linux" - "aarch64-linux" - # Disabled due to our packages not supporting darwin yet. - # "x86_64-darwin" - # "aarch64-darwin" - ]; -} diff --git a/nix/dev.nix b/nix/dev.nix index c39e15653..c476ed10f 100644 --- a/nix/dev.nix +++ b/nix/dev.nix @@ -1,42 +1,33 @@ { - inputs, - self, - ... -}: { perSystem = { - system, + config, + lib, pkgs, ... }: { - checks = { - pre-commit-check = inputs.pre-commit-hooks.lib.${system}.run { - src = self; - hooks = { - markdownlint.enable = true; + pre-commit.settings = { + hooks = { + markdownlint.enable = true; - alejandra.enable = true; - deadnix.enable = true; - nil.enable = true; + alejandra.enable = true; + deadnix.enable = true; + nil.enable = true; - clang-format = { - enable = true; - types_or = ["c" "c++" "java" "json" "objective-c"]; - }; + clang-format = { + enable = true; + types_or = ["c" "c++" "java" "json" "objective-c"]; }; }; + + tools.clang-tools = lib.mkForce pkgs.clang-tools_16; }; devShells.default = pkgs.mkShell { - inherit (self.checks.${system}.pre-commit-check) shellHook; - packages = with pkgs; [ - nodePackages.markdownlint-cli - alejandra - deadnix - clang-tools - nil - ]; + shellHook = '' + ${config.pre-commit.installationScript} + ''; - inputsFrom = [self.packages.${system}.prismlauncher-unwrapped]; + inputsFrom = [config.packages.prismlauncher-unwrapped]; buildInputs = with pkgs; [ccache ninja]; }; diff --git a/nix/distribution.nix b/nix/distribution.nix index 0f2e26f3e..ca9999dcb 100644 --- a/nix/distribution.nix +++ b/nix/distribution.nix @@ -1,29 +1,65 @@ { inputs, self, - version, ... }: { - perSystem = {pkgs, ...}: { - packages = { - inherit (pkgs) prismlauncher-qt5-unwrapped prismlauncher-qt5 prismlauncher-unwrapped prismlauncher; - default = pkgs.prismlauncher; + perSystem = { + lib, + pkgs, + ... + }: { + packages = let + ourPackages = lib.fix (final: self.overlays.default final pkgs); + in { + inherit + (ourPackages) + prismlauncher-qt5-unwrapped + prismlauncher-qt5 + prismlauncher-unwrapped + prismlauncher + ; + default = ourPackages.prismlauncher; }; }; flake = { overlays.default = final: prev: let - # Helper function to build prism against different versions of Qt. - mkPrism = qt: - qt.callPackage ./package.nix { - inherit (inputs) libnbtplusplus; - inherit self version; - }; + version = builtins.substring 0 8 self.lastModifiedDate or "dirty"; + + filteredSelf = inputs.nix-filter.lib.filter { + root = ../.; + include = [ + "buildconfig" + "cmake" + "launcher" + "libraries" + "program_info" + "tests" + ../COPYING.md + ../CMakeLists.txt + ]; + }; + + # common args for prismlauncher evaluations + unwrappedArgs = { + self = filteredSelf; + + inherit (inputs) libnbtplusplus; + inherit ((final.darwin or prev.darwin).apple_sdk.frameworks) Cocoa; + inherit version; + }; in { - prismlauncher-qt5-unwrapped = mkPrism final.libsForQt5; - prismlauncher-qt5 = prev.prismlauncher-qt5.override {prismlauncher-unwrapped = final.prismlauncher-qt5-unwrapped;}; - prismlauncher-unwrapped = mkPrism final.qt6Packages; - prismlauncher = prev.prismlauncher.override {inherit (final) prismlauncher-unwrapped;}; + prismlauncher-qt5-unwrapped = prev.libsForQt5.callPackage ./pkg unwrappedArgs; + + prismlauncher-qt5 = prev.libsForQt5.callPackage ./pkg/wrapper.nix { + prismlauncher-unwrapped = final.prismlauncher-qt5-unwrapped; + }; + + prismlauncher-unwrapped = prev.qt6Packages.callPackage ./pkg unwrappedArgs; + + prismlauncher = prev.qt6Packages.callPackage ./pkg/wrapper.nix { + inherit (final) prismlauncher-unwrapped; + }; }; }; } diff --git a/nix/package.nix b/nix/package.nix deleted file mode 100644 index edc266dc4..000000000 --- a/nix/package.nix +++ /dev/null @@ -1,65 +0,0 @@ -{ - lib, - stdenv, - cmake, - ninja, - jdk17, - zlib, - qtbase, - quazip, - extra-cmake-modules, - tomlplusplus, - cmark, - ghc_filesystem, - gamemode, - msaClientID ? null, - gamemodeSupport ? true, - self, - version, - libnbtplusplus, -}: -stdenv.mkDerivation rec { - pname = "prismlauncher-unwrapped"; - inherit version; - - src = lib.cleanSource self; - - nativeBuildInputs = [extra-cmake-modules cmake jdk17 ninja]; - buildInputs = - [ - qtbase - zlib - quazip - ghc_filesystem - tomlplusplus - cmark - ] - ++ lib.optional gamemodeSupport gamemode; - - hardeningEnable = ["pie"]; - - cmakeFlags = - lib.optionals (msaClientID != null) ["-DLauncher_MSA_CLIENT_ID=${msaClientID}"] - ++ lib.optionals (lib.versionOlder qtbase.version "6") ["-DLauncher_QT_VERSION_MAJOR=5"]; - - postUnpack = '' - rm -rf source/libraries/libnbtplusplus - ln -s ${libnbtplusplus} source/libraries/libnbtplusplus - ''; - - dontWrapQtApps = true; - - meta = with lib; { - homepage = "https://prismlauncher.org/"; - description = "A free, open source launcher for Minecraft"; - longDescription = '' - Allows you to have multiple, separate instances of Minecraft (each with - their own mods, texture packs, saves, etc) and helps you manage them and - their associated options with a simple interface. - ''; - platforms = platforms.linux; - changelog = "https://github.com/PrismLauncher/PrismLauncher/releases/tag/${version}"; - license = licenses.gpl3Only; - maintainers = with maintainers; [minion3665 Scrumplex]; - }; -} diff --git a/nix/pkg/default.nix b/nix/pkg/default.nix new file mode 100644 index 000000000..fd19a0b3d --- /dev/null +++ b/nix/pkg/default.nix @@ -0,0 +1,74 @@ +{ + lib, + stdenv, + canonicalize-jars-hook, + cmake, + cmark, + Cocoa, + ninja, + jdk17, + zlib, + qtbase, + quazip, + extra-cmake-modules, + tomlplusplus, + ghc_filesystem, + gamemode, + msaClientID ? null, + gamemodeSupport ? stdenv.isLinux, + self, + version, + libnbtplusplus, +}: +assert lib.assertMsg (stdenv.isLinux || !gamemodeSupport) "gamemodeSupport is only available on Linux"; + stdenv.mkDerivation rec { + pname = "prismlauncher-unwrapped"; + inherit version; + + src = lib.cleanSource self; + + nativeBuildInputs = [extra-cmake-modules cmake jdk17 ninja canonicalize-jars-hook]; + buildInputs = + [ + qtbase + zlib + quazip + ghc_filesystem + tomlplusplus + cmark + ] + ++ lib.optional gamemodeSupport gamemode + ++ lib.optionals stdenv.isDarwin [Cocoa]; + + hardeningEnable = lib.optionals stdenv.isLinux ["pie"]; + + cmakeFlags = + [ + "-DLauncher_BUILD_PLATFORM=nixpkgs" + ] + ++ lib.optionals (msaClientID != null) ["-DLauncher_MSA_CLIENT_ID=${msaClientID}"] + ++ lib.optionals (lib.versionOlder qtbase.version "6") ["-DLauncher_QT_VERSION_MAJOR=5"] + ++ lib.optionals stdenv.isDarwin ["-DINSTALL_BUNDLE=nodeps" "-DMACOSX_SPARKLE_UPDATE_FEED_URL=''"]; + + postUnpack = '' + rm -rf source/libraries/libnbtplusplus + ln -s ${libnbtplusplus} source/libraries/libnbtplusplus + ''; + + dontWrapQtApps = true; + + meta = with lib; { + mainProgram = "prismlauncher"; + homepage = "https://prismlauncher.org/"; + description = "A free, open source launcher for Minecraft"; + longDescription = '' + Allows you to have multiple, separate instances of Minecraft (each with + their own mods, texture packs, saves, etc) and helps you manage them and + their associated options with a simple interface. + ''; + platforms = with platforms; linux ++ darwin; + changelog = "https://github.com/PrismLauncher/PrismLauncher/releases/tag/${version}"; + license = licenses.gpl3Only; + maintainers = with maintainers; [minion3665 Scrumplex getchoo]; + }; + } diff --git a/nix/pkg/wrapper.nix b/nix/pkg/wrapper.nix new file mode 100644 index 000000000..8bc255e71 --- /dev/null +++ b/nix/pkg/wrapper.nix @@ -0,0 +1,94 @@ +{ + lib, + stdenv, + symlinkJoin, + prismlauncher-unwrapped, + wrapQtAppsHook, + qtbase, # needed for wrapQtAppsHook + qtsvg, + qtwayland, + xorg, + libpulseaudio, + libGL, + glfw, + openal, + jdk8, + jdk17, + gamemode, + flite, + mesa-demos, + udev, + libusb1, + msaClientID ? null, + gamemodeSupport ? stdenv.isLinux, + textToSpeechSupport ? stdenv.isLinux, + controllerSupport ? stdenv.isLinux, + jdks ? [jdk17 jdk8], + additionalLibs ? [], + additionalPrograms ? [], +}: let + prismlauncherFinal = prismlauncher-unwrapped.override { + inherit msaClientID gamemodeSupport; + }; +in + symlinkJoin { + name = "prismlauncher-${prismlauncherFinal.version}"; + + paths = [prismlauncherFinal]; + + nativeBuildInputs = [ + wrapQtAppsHook + ]; + + buildInputs = + [ + qtbase + qtsvg + ] + ++ lib.optional (lib.versionAtLeast qtbase.version "6" && stdenv.isLinux) qtwayland; + + postBuild = '' + wrapQtAppsHook + ''; + + qtWrapperArgs = let + runtimeLibs = + (with xorg; [ + libX11 + libXext + libXcursor + libXrandr + libXxf86vm + ]) + ++ [ + # lwjgl + libpulseaudio + libGL + glfw + openal + stdenv.cc.cc.lib + + # oshi + udev + ] + ++ lib.optional gamemodeSupport gamemode.lib + ++ lib.optional textToSpeechSupport flite + ++ lib.optional controllerSupport libusb1 + ++ additionalLibs; + + runtimePrograms = + [ + xorg.xrandr + mesa-demos # need glxinfo + ] + ++ additionalPrograms; + in + ["--prefix PRISMLAUNCHER_JAVA_PATHS : ${lib.makeSearchPath "bin/java" jdks}"] + ++ lib.optionals stdenv.isLinux [ + "--set LD_LIBRARY_PATH /run/opengl-driver/lib:${lib.makeLibraryPath runtimeLibs}" + # xorg.xrandr needed for LWJGL [2.9.2, 3) https://github.com/LWJGL/lwjgl/issues/128 + "--prefix PATH : ${lib.makeBinPath runtimePrograms}" + ]; + + inherit (prismlauncherFinal) meta; + } diff --git a/program_info/org.prismlauncher.PrismLauncher.desktop.in b/program_info/org.prismlauncher.PrismLauncher.desktop.in index 20fabe9d4..76f4b19c0 100644 --- a/program_info/org.prismlauncher.PrismLauncher.desktop.in +++ b/program_info/org.prismlauncher.PrismLauncher.desktop.in @@ -4,10 +4,10 @@ Name=Prism Launcher Comment=A custom launcher for Minecraft that allows you to easily manage multiple installations of Minecraft at once. Type=Application Terminal=false -Exec=@Launcher_APP_BINARY_NAME@ +Exec=@Launcher_APP_BINARY_NAME@ %U StartupNotify=true Icon=org.prismlauncher.PrismLauncher Categories=Game;ActionGame;AdventureGame;Simulation; Keywords=game;minecraft;mc; StartupWMClass=PrismLauncher -MimeType=application/zip;application/x-modrinth-modpack+zip +MimeType=application/zip;application/x-modrinth-modpack+zip;x-scheme-handler/curseforge; diff --git a/program_info/win_install.nsi.in b/program_info/win_install.nsi.in index d3b5c256f..cb7b0935b 100644 --- a/program_info/win_install.nsi.in +++ b/program_info/win_install.nsi.in @@ -363,6 +363,10 @@ Section "@Launcher_DisplayName@" ; Write the installation path into the registry WriteRegStr HKCU Software\@Launcher_CommonName@ "InstallDir" "$INSTDIR" + ; Write the URL Handler into registry for curseforge + WriteRegStr HKCU Software\Classes\curseforge "URL Protocol" "" + WriteRegStr HKCU Software\Classes\curseforge\shell\open\command "" '"$INSTDIR\@Launcher_APP_BINARY_NAME@.exe" "%1"' + ; Write the uninstall keys for Windows ${GetParameters} $R0 ${GetOptions} $R0 "/NoUninstaller" $R1 diff --git a/renovate.json b/renovate.json index 71581e3b0..f9c2c3270 100644 --- a/renovate.json +++ b/renovate.json @@ -2,11 +2,5 @@ "$schema": "https://docs.renovatebot.com/renovate-schema.json", "extends": [ "config:base" - ], - "nix": { - "enabled": true - }, - "lockFileMaintenance": { - "enabled": true - } + ] } diff --git a/tests/CMakeLists.txt b/tests/CMakeLists.txt index a26a49fec..59e0e3144 100644 --- a/tests/CMakeLists.txt +++ b/tests/CMakeLists.txt @@ -56,3 +56,6 @@ ecm_add_test(Index_test.cpp LINK_LIBRARIES Launcher_logic Qt${QT_VERSION_MAJOR}: ecm_add_test(Version_test.cpp LINK_LIBRARIES Launcher_logic Qt${QT_VERSION_MAJOR}::Test TEST_NAME Version) + +ecm_add_test(CatPack_test.cpp LINK_LIBRARIES Launcher_logic Qt${QT_VERSION_MAJOR}::Test + TEST_NAME CatPack) diff --git a/tests/CatPack_test.cpp b/tests/CatPack_test.cpp new file mode 100644 index 000000000..330d1a814 --- /dev/null +++ b/tests/CatPack_test.cpp @@ -0,0 +1,40 @@ +#include + +#include +#include +#include +#include +#include "FileSystem.h" +#include "ui/themes/CatPack.h" + +class CatPackTest : public QObject { + Q_OBJECT + private slots: + void test_catPack() + { + auto dataDir = QDir(QFINDTESTDATA("testdata/CatPacks")).absolutePath(); + auto fileName = FS::PathCombine(dataDir, "index.json"); + auto fileinfo = QFileInfo(fileName); + try { + auto cat = JsonCatPack(fileinfo); + QCOMPARE(cat.path(QDate(2023, 4, 12)), FS::PathCombine(fileinfo.path(), "oneDay.png")); + QCOMPARE(cat.path(QDate(2023, 4, 11)), FS::PathCombine(fileinfo.path(), "maxwell.png")); + QCOMPARE(cat.path(QDate(2023, 4, 13)), FS::PathCombine(fileinfo.path(), "maxwell.png")); + QCOMPARE(cat.path(QDate(2023, 12, 21)), FS::PathCombine(fileinfo.path(), "christmas.png")); + QCOMPARE(cat.path(QDate(2023, 12, 28)), FS::PathCombine(fileinfo.path(), "christmas.png")); + QCOMPARE(cat.path(QDate(2023, 12, 29)), FS::PathCombine(fileinfo.path(), "newyear.png")); + QCOMPARE(cat.path(QDate(2023, 12, 30)), FS::PathCombine(fileinfo.path(), "newyear2.png")); + QCOMPARE(cat.path(QDate(2023, 12, 31)), FS::PathCombine(fileinfo.path(), "newyear2.png")); + QCOMPARE(cat.path(QDate(2024, 1, 1)), FS::PathCombine(fileinfo.path(), "newyear2.png")); + QCOMPARE(cat.path(QDate(2024, 1, 2)), FS::PathCombine(fileinfo.path(), "newyear.png")); + QCOMPARE(cat.path(QDate(2024, 1, 3)), FS::PathCombine(fileinfo.path(), "newyear.png")); + QCOMPARE(cat.path(QDate(2024, 1, 4)), FS::PathCombine(fileinfo.path(), "maxwell.png")); + } catch (const Exception& e) { + QFAIL(e.cause().toLatin1()); + } + } +}; + +QTEST_GUILESS_MAIN(CatPackTest) + +#include "CatPack_test.moc" diff --git a/tests/DummyResourceAPI.h b/tests/DummyResourceAPI.h index 0cc909584..35de95151 100644 --- a/tests/DummyResourceAPI.h +++ b/tests/DummyResourceAPI.h @@ -32,7 +32,7 @@ class DummyResourceAPI : public ResourceAPI { } DummyResourceAPI() : ResourceAPI() {} - [[nodiscard]] auto getSortingMethods() const -> QList override { return {}; }; + [[nodiscard]] auto getSortingMethods() const -> QList override { return {}; } [[nodiscard]] Task::Ptr searchProjects(SearchArgs&&, SearchCallbacks&& callbacks) const override { diff --git a/tests/FileSystem_test.cpp b/tests/FileSystem_test.cpp index 3adcd3432..1d3cee85f 100644 --- a/tests/FileSystem_test.cpp +++ b/tests/FileSystem_test.cpp @@ -81,10 +81,10 @@ class LinkTask : public Task { } else { emitSucceeded(); } - }; + } FS::create_link* m_lnk; - bool m_useHard = false; + [[maybe_unused]] bool m_useHard = false; bool m_linkRecursive = true; }; @@ -353,15 +353,12 @@ class FileSystemTest : public QObject { } } - void test_getDesktop() - { - QCOMPARE(FS::getDesktopDir(), QStandardPaths::writableLocation(QStandardPaths::DesktopLocation)); - } + void test_getDesktop() { QCOMPARE(FS::getDesktopDir(), QStandardPaths::writableLocation(QStandardPaths::DesktopLocation)); } void test_link() { QString folder = QFINDTESTDATA("testdata/FileSystem/test_folder"); - auto f = [&folder, this]() { + auto f = [&folder]() { QTemporaryDir tempDir; tempDir.setAutoRemove(true); qDebug() << "From:" << folder << "To:" << tempDir.path(); @@ -630,7 +627,7 @@ class FileSystemTest : public QObject { void test_link_with_max_depth() { QString folder = QFINDTESTDATA("testdata/FileSystem/test_folder"); - auto f = [&folder, this]() { + auto f = [&folder]() { QTemporaryDir tempDir; tempDir.setAutoRemove(true); qDebug() << "From:" << folder << "To:" << tempDir.path(); diff --git a/tests/GZip_test.cpp b/tests/GZip_test.cpp index 4c1259c22..c11ba2711 100644 --- a/tests/GZip_test.cpp +++ b/tests/GZip_test.cpp @@ -26,7 +26,7 @@ class GZipTest : public QObject { // initialize random buffer for (int i = 0; i < size; i++) { - random.append((char)idis(eng)); + random.append(static_cast(idis(eng))); } // initialize fibonacci diff --git a/tests/ResourceModel_test.cpp b/tests/ResourceModel_test.cpp index 9626c64ef..b589758aa 100644 --- a/tests/ResourceModel_test.cpp +++ b/tests/ResourceModel_test.cpp @@ -40,17 +40,17 @@ class DummyResourceModel : public ResourceModel { DummyResourceModel() : ResourceModel(new DummyResourceAPI) {} ~DummyResourceModel() {} - [[nodiscard]] auto metaEntryBase() const -> QString override { return ""; }; + [[nodiscard]] auto metaEntryBase() const -> QString override { return ""; } - ResourceAPI::SearchArgs createSearchArguments() override { return {}; }; - ResourceAPI::VersionSearchArgs createVersionsArguments(QModelIndex&) override { return {}; }; - ResourceAPI::ProjectInfoArgs createInfoArguments(QModelIndex&) override { return {}; }; + ResourceAPI::SearchArgs createSearchArguments() override { return {}; } + ResourceAPI::VersionSearchArgs createVersionsArguments(QModelIndex&) override { return {}; } + ResourceAPI::ProjectInfoArgs createInfoArguments(QModelIndex&) override { return {}; } QJsonArray documentToArray(QJsonDocument& doc) const override { return doc.object().value("hits").toArray(); } void loadIndexedPack(ModPlatform::IndexedPack& pack, QJsonObject& obj) override { - pack.authors.append({ Json::requireString(obj, "author") }); + pack.authors.append({ Json::requireString(obj, "author"), "" }); pack.description = Json::requireString(obj, "description"); pack.addonId = Json::requireString(obj, "project_id"); } diff --git a/tests/Task_test.cpp b/tests/Task_test.cpp index 95d927c68..abc9be905 100644 --- a/tests/Task_test.cpp +++ b/tests/Task_test.cpp @@ -19,7 +19,7 @@ class BasicTask : public Task { BasicTask(bool show_debug_log = true) : Task(nullptr, show_debug_log) {} private: - void executeTask() override { emitSucceeded(); }; + void executeTask() override { emitSucceeded(); } }; /* Does nothing. Only used for testing. */ @@ -31,7 +31,7 @@ class BasicTask_MultiStep : public Task { private: auto isMultiStep() const -> bool override { return true; } - void executeTask() override{}; + void executeTask() override {} }; class BigConcurrentTask : public ConcurrentTask { diff --git a/tests/testdata/CatPacks/index.json b/tests/testdata/CatPacks/index.json new file mode 100644 index 000000000..b5401d230 --- /dev/null +++ b/tests/testdata/CatPacks/index.json @@ -0,0 +1,50 @@ +{ + "name": "My Cute Cat", + "default": "maxwell.png", + "variants": [ + { + "startTime": { + "day": 12, + "month": 4 + }, + "endTime": { + "day": 12, + "month": 4 + }, + "path": "oneDay.png" + }, + { + "startTime": { + "day": 20, + "month": 12 + }, + "endTime": { + "day": 28, + "month": 12 + }, + "path": "christmas.png" + }, + { + "startTime": { + "day": 30, + "month": 12 + }, + "endTime": { + "day": 1, + "month": 1 + }, + "path": "newyear2.png" + }, + { + "startTime": { + "day": 28, + "month": 12 + }, + "endTime": { + "day": 3, + "month": 1 + }, + "path": "newyear.png" + } + ] +}