diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 059795a11..7e542d4e5 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -37,56 +37,43 @@ jobs: fail-fast: false matrix: include: - - os: ubuntu-20.04 qt_ver: 5 - os: ubuntu-20.04 qt_ver: 6 qt_host: linux - qt_arch: '' - qt_version: '6.2.4' - qt_modules: 'qt5compat qtimageformats' - qt_tools: '' + qt_arch: "" + qt_version: "6.2.4" + qt_modules: "qt5compat qtimageformats" + qt_tools: "" - os: windows-2022 name: "Windows-MinGW-w64" msystem: clang64 - vcvars_arch: 'amd64_x86' - - - os: windows-2022 - name: "Windows-MSVC-Legacy" - msystem: '' - architecture: 'win32' - vcvars_arch: 'amd64_x86' - qt_ver: 5 - qt_host: windows - qt_arch: 'win32_msvc2019' - qt_version: '5.15.2' - qt_modules: '' - qt_tools: 'tools_openssl_x86' + vcvars_arch: "amd64_x86" - os: windows-2022 name: "Windows-MSVC" - msystem: '' - architecture: 'x64' - vcvars_arch: 'amd64' + msystem: "" + architecture: "x64" + vcvars_arch: "amd64" qt_ver: 6 qt_host: windows qt_arch: '' - qt_version: '6.5.2' + qt_version: '6.6.0' qt_modules: 'qt5compat qtimageformats' qt_tools: '' - os: windows-2022 name: "Windows-MSVC-arm64" - msystem: '' - architecture: 'arm64' - vcvars_arch: 'amd64_arm64' + msystem: "" + architecture: "arm64" + vcvars_arch: "amd64_arm64" qt_ver: 6 qt_host: windows qt_arch: 'win64_msvc2019_arm64' - qt_version: '6.5.2' + qt_version: '6.6.0' qt_modules: 'qt5compat qtimageformats' qt_tools: '' @@ -96,7 +83,7 @@ jobs: qt_ver: 6 qt_host: mac qt_arch: '' - qt_version: '6.5.2' + qt_version: '6.6.0' qt_modules: 'qt5compat qtimageformats' qt_tools: '' @@ -105,9 +92,9 @@ jobs: macosx_deployment_target: 10.13 qt_ver: 5 qt_host: mac - qt_version: '5.15.2' - qt_modules: '' - qt_tools: '' + qt_version: "5.15.2" + qt_modules: "" + qt_tools: "" runs-on: ${{ matrix.os }} @@ -127,9 +114,9 @@ jobs: - name: Checkout uses: actions/checkout@v4 with: - submodules: 'true' + submodules: "true" - - name: 'Setup MSYS2' + - name: "Setup MSYS2" if: runner.os == 'Windows' && matrix.msystem != '' uses: msys2/setup-msys2@v2 with: @@ -169,7 +156,7 @@ jobs: path: '${{ github.workspace }}\.ccache' key: ${{ matrix.os }}-mingw-w64-ccache-${{ github.run_id }} restore-keys: | - ${{ matrix.os }}-mingw-w64-ccache + ${{ matrix.os }}-mingw-w64-ccache - name: Setup ccache (Windows MinGW-w64) if: runner.os == 'Windows' && matrix.msystem != '' && inputs.build_type == 'Debug' @@ -214,35 +201,35 @@ jobs: if: runner.os == 'Windows' && matrix.architecture == 'arm64' uses: jurplel/install-qt-action@v3 with: - aqtversion: '==3.1.*' - py7zrversion: '>=0.20.2' - version: ${{ matrix.qt_version }} - host: 'windows' - target: 'desktop' - arch: '' - modules: ${{ matrix.qt_modules }} - tools: ${{ matrix.qt_tools }} - cache: ${{ inputs.is_qt_cached }} - cache-key-prefix: host-qt-arm64-windows - dir: ${{ github.workspace }}\HostQt - set-env: false + aqtversion: "==3.1.*" + py7zrversion: ">=0.20.2" + version: ${{ matrix.qt_version }} + host: "windows" + target: "desktop" + arch: "" + modules: ${{ matrix.qt_modules }} + tools: ${{ matrix.qt_tools }} + cache: ${{ inputs.is_qt_cached }} + cache-key-prefix: host-qt-arm64-windows + dir: ${{ github.workspace }}\HostQt + set-env: false - name: Install Qt (macOS, Linux, Qt 6 & Windows MSVC) if: runner.os == 'Linux' && matrix.qt_ver == 6 || runner.os == 'macOS' || (runner.os == 'Windows' && matrix.msystem == '') uses: jurplel/install-qt-action@v3 with: - aqtversion: '==3.1.*' - py7zrversion: '>=0.20.2' - version: ${{ matrix.qt_version }} - host: ${{ matrix.qt_host }} - target: 'desktop' - arch: ${{ matrix.qt_arch }} - modules: ${{ matrix.qt_modules }} - tools: ${{ matrix.qt_tools }} - cache: ${{ inputs.is_qt_cached }} + aqtversion: "==3.1.*" + py7zrversion: ">=0.20.2" + version: ${{ matrix.qt_version }} + host: ${{ matrix.qt_host }} + target: "desktop" + arch: ${{ matrix.qt_arch }} + modules: ${{ matrix.qt_modules }} + tools: ${{ matrix.qt_tools }} + cache: ${{ inputs.is_qt_cached }} - name: Install MSVC (Windows MSVC) - if: runner.os == 'Windows' # We want this for MinGW builds as well, as we need SignTool + if: runner.os == 'Windows' # We want this for MinGW builds as well, as we need SignTool uses: ilammy/msvc-dev-cmd@v1 with: vsversion: 2022 @@ -283,12 +270,12 @@ jobs: if: runner.os == 'Windows' && matrix.msystem != '' shell: msys2 {0} run: | - cmake -S . -B ${{ env.BUILD_DIR }} -DCMAKE_INSTALL_PREFIX=${{ env.INSTALL_DIR }} -DCMAKE_BUILD_TYPE=${{ inputs.build_type }} -DENABLE_LTO=ON -DLauncher_BUILD_PLATFORM=official -DCMAKE_C_COMPILER_LAUNCHER=${{ env.CCACHE_VAR }} -DCMAKE_CXX_COMPILER_LAUNCHER=${{ env.CCACHE_VAR }} -DLauncher_QT_VERSION_MAJOR=6 -DCMAKE_OBJDUMP=/mingw64/bin/objdump.exe -G Ninja + cmake -S . -B ${{ env.BUILD_DIR }} -DCMAKE_INSTALL_PREFIX=${{ env.INSTALL_DIR }} -DCMAKE_BUILD_TYPE=${{ inputs.build_type }} -DENABLE_LTO=ON -DLauncher_BUILD_PLATFORM=official -DCMAKE_C_COMPILER_LAUNCHER=${{ env.CCACHE_VAR }} -DCMAKE_CXX_COMPILER_LAUNCHER=${{ env.CCACHE_VAR }} -DLauncher_QT_VERSION_MAJOR=6 -DCMAKE_OBJDUMP=/mingw64/bin/objdump.exe -DLauncher_BUILD_ARTIFACT=${{ matrix.name }}-Qt${{ matrix.qt_ver }} -G Ninja - name: Configure CMake (Windows MSVC) if: runner.os == 'Windows' && matrix.msystem == '' run: | - cmake -S . -B ${{ env.BUILD_DIR }} -DCMAKE_INSTALL_PREFIX=${{ env.INSTALL_DIR }} -DCMAKE_BUILD_TYPE=${{ inputs.build_type }} -DENABLE_LTO=ON -DLauncher_BUILD_PLATFORM=official -DLauncher_QT_VERSION_MAJOR=${{ matrix.qt_ver }} -DCMAKE_MSVC_RUNTIME_LIBRARY="MultiThreadedDLL" -A${{ matrix.architecture}} -DLauncher_FORCE_BUNDLED_LIBS=ON + cmake -S . -B ${{ env.BUILD_DIR }} -DCMAKE_INSTALL_PREFIX=${{ env.INSTALL_DIR }} -DCMAKE_BUILD_TYPE=${{ inputs.build_type }} -DENABLE_LTO=ON -DLauncher_BUILD_PLATFORM=official -DLauncher_QT_VERSION_MAJOR=${{ matrix.qt_ver }} -DCMAKE_MSVC_RUNTIME_LIBRARY="MultiThreadedDLL" -A${{ matrix.architecture}} -DLauncher_FORCE_BUNDLED_LIBS=ON -DLauncher_BUILD_ARTIFACT=${{ matrix.name }}-Qt${{ matrix.qt_ver }} # https://github.com/ccache/ccache/wiki/MS-Visual-Studio (I coudn't figure out the compiler prefix) if ("${{ env.CCACHE_VAR }}") { @@ -303,7 +290,7 @@ jobs: - name: Configure CMake (Linux) if: runner.os == 'Linux' run: | - cmake -S . -B ${{ env.BUILD_DIR }} -DCMAKE_INSTALL_PREFIX=/usr -DCMAKE_BUILD_TYPE=${{ inputs.build_type }} -DENABLE_LTO=ON -DLauncher_BUILD_PLATFORM=official -DCMAKE_C_COMPILER_LAUNCHER=${{ env.CCACHE_VAR }} -DCMAKE_CXX_COMPILER_LAUNCHER=${{ env.CCACHE_VAR }} -DLauncher_QT_VERSION_MAJOR=${{ matrix.qt_ver }} -G Ninja + cmake -S . -B ${{ env.BUILD_DIR }} -DCMAKE_INSTALL_PREFIX=/usr -DCMAKE_BUILD_TYPE=${{ inputs.build_type }} -DENABLE_LTO=ON -DLauncher_BUILD_PLATFORM=official -DCMAKE_C_COMPILER_LAUNCHER=${{ env.CCACHE_VAR }} -DCMAKE_CXX_COMPILER_LAUNCHER=${{ env.CCACHE_VAR }} -DLauncher_QT_VERSION_MAJOR=${{ matrix.qt_ver }} -DLauncher_BUILD_ARTIFACT=Linux-Qt${{ matrix.qt_ver }} -G Ninja ## # BUILD @@ -343,7 +330,7 @@ jobs: - name: Test (Windows MSVC) if: runner.os == 'Windows' && matrix.msystem == '' && matrix.architecture != 'arm64' run: | - ctest -E "^example64|example$" --test-dir build --output-on-failure -C ${{ inputs.build_type }} + ctest -E "^example64|example$" --test-dir build --output-on-failure -C ${{ inputs.build_type }} ## # PACKAGE BUILDS @@ -385,7 +372,7 @@ jobs: run: | cmake --install ${{ env.BUILD_DIR }} touch ${{ env.INSTALL_DIR }}/manifest.txt - for l in $(find ${{ env.INSTALL_DIR }} -type f); do l=$(cygpath -u $l); l=${l#$(pwd)/}; l=${l#${{ env.INSTALL_DIR }}/}; l=${l#./}; echo $l; done >> ${{ env.INSTALL_DIR }}/manifest.txt + for l in $(find ${{ env.INSTALL_DIR }} -type f); do l=$(cygpath -u $l); l=${l#$(pwd)/}; l=${l#${{ env.INSTALL_DIR }}/}; l=${l#./}; echo $l; done >> ${{ env.INSTALL_DIR }}/manifest.txt - name: Package (Windows MSVC) if: runner.os == 'Windows' && matrix.msystem == '' @@ -402,10 +389,9 @@ jobs: Get-ChildItem ${{ env.INSTALL_DIR }} -Recurse | ForEach FullName | Resolve-Path -Relative | %{ $_.TrimStart('.\') } | %{ $_.TrimStart('${{ env.INSTALL_DIR }}') } | %{ $_.TrimStart('\') } | Out-File -FilePath ${{ env.INSTALL_DIR }}/manifest.txt - - name: Fetch codesign certificate (Windows) if: runner.os == 'Windows' - shell: bash # yes, we are not using MSYS2 or PowerShell here + shell: bash # yes, we are not using MSYS2 or PowerShell here run: | echo '${{ secrets.WINDOWS_CODESIGN_CERT }}' | base64 --decode > codesign.pfx @@ -415,7 +401,7 @@ jobs: if (Get-Content ./codesign.pfx){ cd ${{ env.INSTALL_DIR }} # We ship the exact same executable for portable and non-portable editions, so signing just once is fine - SignTool sign /fd sha256 /td sha256 /f ../codesign.pfx /p '${{ secrets.WINDOWS_CODESIGN_PASSWORD }}' /tr http://timestamp.digicert.com prismlauncher.exe prismlauncher_filelink.exe + SignTool sign /fd sha256 /td sha256 /f ../codesign.pfx /p '${{ secrets.WINDOWS_CODESIGN_PASSWORD }}' /tr http://timestamp.digicert.com prismlauncher.exe prismlauncher_updater.exe prismlauncher_filelink.exe } else { ":warning: Skipped code signing for Windows, as certificate was not present." >> $env:GITHUB_STEP_SUMMARY } @@ -507,15 +493,7 @@ jobs: 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 + cp AppImageUpdate-x86_64.AppImage ${{ env.INSTALL_APPIMAGE_DIR }}/usr/bin export UPDATE_INFORMATION="gh-releases-zsync|${{ github.repository_owner }}|${{ github.event.repository.name }}|latest|PrismLauncher-Linux-x86_64.AppImage.zsync" @@ -569,14 +547,14 @@ jobs: if: runner.os == 'Linux' && matrix.qt_ver != 6 uses: actions/upload-artifact@v3 with: - name: PrismLauncher-${{ runner.os }}-${{ env.VERSION }}-${{ inputs.build_type }} + name: PrismLauncher-${{ runner.os }}-Qt5-${{ env.VERSION }}-${{ inputs.build_type }} path: PrismLauncher.tar.gz - name: Upload binary tarball (Linux, portable, Qt 5) if: runner.os == 'Linux' && matrix.qt_ver != 6 uses: actions/upload-artifact@v3 with: - name: PrismLauncher-${{ runner.os }}-Portable-${{ env.VERSION }}-${{ inputs.build_type }} + name: PrismLauncher-${{ runner.os }}-Qt5-Portable-${{ env.VERSION }}-${{ inputs.build_type }} path: PrismLauncher-portable.tar.gz - name: Upload binary tarball (Linux, Qt 6) @@ -599,7 +577,7 @@ 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 @@ -623,10 +601,10 @@ jobs: uses: actions/checkout@v4 if: inputs.build_type == 'Debug' with: - submodules: 'true' + submodules: "true" - name: Build Flatpak (Linux) if: inputs.build_type == 'Debug' uses: flatpak/flatpak-github-actions/flatpak-builder@v6 with: bundle: "Prism Launcher.flatpak" - manifest-path: flatpak/org.prismlauncher.PrismLauncher.yml + manifest-path: flatpak/org.prismlauncher.PrismLauncher.yml diff --git a/.github/workflows/trigger_builds.yml b/.github/workflows/trigger_builds.yml index 26ee4380b..70fda60ed 100644 --- a/.github/workflows/trigger_builds.yml +++ b/.github/workflows/trigger_builds.yml @@ -3,26 +3,25 @@ name: Build Application on: push: branches-ignore: - - 'renovate/**' + - "renovate/**" paths-ignore: - - '**.md' - - '**/LICENSE' - - 'flake.lock' - - 'packages/**' - - '.github/ISSUE_TEMPLATE/**' - - '.markdownlint**' + - "**.md" + - "**/LICENSE" + - "flake.lock" + - "packages/**" + - ".github/ISSUE_TEMPLATE/**" + - ".markdownlint**" pull_request: paths-ignore: - - '**.md' - - '**/LICENSE' - - 'flake.lock' - - 'packages/**' - - '.github/ISSUE_TEMPLATE/**' - - '.markdownlint**' + - "**.md" + - "**/LICENSE" + - "flake.lock" + - "packages/**" + - ".github/ISSUE_TEMPLATE/**" + - ".markdownlint**" workflow_dispatch: jobs: - build_debug: name: Build Debug uses: ./.github/workflows/build.yml @@ -34,3 +33,5 @@ jobs: WINDOWS_CODESIGN_CERT: ${{ secrets.WINDOWS_CODESIGN_CERT }} WINDOWS_CODESIGN_PASSWORD: ${{ secrets.WINDOWS_CODESIGN_PASSWORD }} CACHIX_AUTH_TOKEN: ${{ secrets.CACHIX_AUTH_TOKEN }} + GPG_PRIVATE_KEY: ${{ secrets.GPG_PRIVATE_KEY }} + GPG_PRIVATE_KEY_ID: ${{ secrets.GPG_PRIVATE_KEY_ID }} diff --git a/.github/workflows/trigger_release.yml b/.github/workflows/trigger_release.yml index bda75e354..28578165f 100644 --- a/.github/workflows/trigger_release.yml +++ b/.github/workflows/trigger_release.yml @@ -3,10 +3,9 @@ name: Build Application and Make Release on: push: tags: - - '*' + - "*" jobs: - build_release: name: Build Release uses: ./.github/workflows/build.yml @@ -18,6 +17,8 @@ jobs: WINDOWS_CODESIGN_CERT: ${{ secrets.WINDOWS_CODESIGN_CERT }} WINDOWS_CODESIGN_PASSWORD: ${{ secrets.WINDOWS_CODESIGN_PASSWORD }} CACHIX_AUTH_TOKEN: ${{ secrets.CACHIX_AUTH_TOKEN }} + GPG_PRIVATE_KEY: ${{ secrets.GPG_PRIVATE_KEY }} + GPG_PRIVATE_KEY_ID: ${{ secrets.GPG_PRIVATE_KEY_ID }} create_release: needs: build_release @@ -28,8 +29,8 @@ jobs: - name: Checkout uses: actions/checkout@v4 with: - submodules: 'true' - path: 'PrismLauncher-source' + submodules: "true" + path: "PrismLauncher-source" - name: Download artifacts uses: actions/download-artifact@v3 - name: Grab and store version @@ -40,9 +41,9 @@ jobs: run: | mv ${{ github.workspace }}/PrismLauncher-source PrismLauncher-${{ env.VERSION }} mv PrismLauncher-Linux-Qt6-Portable*/PrismLauncher-portable.tar.gz PrismLauncher-Linux-Qt6-Portable-${{ env.VERSION }}.tar.gz - 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-Linux-Qt6*/PrismLauncher.tar.gz PrismLauncher-Linux-Qt6-${{ env.VERSION }}.tar.gz + mv PrismLauncher-Linux-Qt5-Portable*/PrismLauncher-portable.tar.gz PrismLauncher-Linux-Qt5-Portable-${{ env.VERSION }}.tar.gz + mv PrismLauncher-Linux-Qt5*/PrismLauncher.tar.gz PrismLauncher-Linux-Qt5-${{ env.VERSION }}.tar.gz 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 @@ -86,8 +87,8 @@ jobs: draft: true prerelease: false files: | - PrismLauncher-Linux-${{ env.VERSION }}.tar.gz - PrismLauncher-Linux-Portable-${{ env.VERSION }}.tar.gz + PrismLauncher-Linux-Qt5-${{ env.VERSION }}.tar.gz + PrismLauncher-Linux-Qt5-Portable-${{ env.VERSION }}.tar.gz PrismLauncher-Linux-x86_64.AppImage PrismLauncher-Linux-x86_64.AppImage.zsync PrismLauncher-Linux-Qt6-${{ env.VERSION }}.tar.gz @@ -95,9 +96,6 @@ jobs: PrismLauncher-Windows-MinGW-w64-${{ env.VERSION }}.zip PrismLauncher-Windows-MinGW-w64-Portable-${{ env.VERSION }}.zip PrismLauncher-Windows-MinGW-w64-Setup-${{ env.VERSION }}.exe - PrismLauncher-Windows-MSVC-Legacy-${{ env.VERSION }}.zip - PrismLauncher-Windows-MSVC-Legacy-Portable-${{ env.VERSION }}.zip - PrismLauncher-Windows-MSVC-Legacy-Setup-${{ env.VERSION }}.exe PrismLauncher-Windows-MSVC-arm64-${{ env.VERSION }}.zip PrismLauncher-Windows-MSVC-arm64-Portable-${{ env.VERSION }}.zip PrismLauncher-Windows-MSVC-arm64-Setup-${{ env.VERSION }}.exe diff --git a/CMakeLists.txt b/CMakeLists.txt index 638fba051..be443fe31 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -188,8 +188,11 @@ set(Launcher_VERSION_NAME4_COMMA "${Launcher_VERSION_MAJOR},${Launcher_VERSION_M # Build platform. set(Launcher_BUILD_PLATFORM "unknown" CACHE STRING "A short string identifying the platform that this build was built for. Only used to display in the about dialog.") -# Channel list URL -set(Launcher_UPDATER_BASE "" CACHE STRING "Base URL for the updater.") +# Github repo URL with releases for updater +set(Launcher_UPDATER_GITHUB_REPO "https://github.com/PrismLauncher/PrismLauncher" CACHE STRING "Base github URL for the updater.") + +# Name to help updater identify valid artifacts +set(Launcher_BUILD_ARTIFACT "" CACHE STRING "Artifact name to help the updater identify valid artifacts.") # The metadata server set(Launcher_META_URL "https://meta.prismlauncher.org/v1/" CACHE STRING "URL to fetch Launcher's meta files from.") @@ -245,6 +248,11 @@ set(Launcher_MSA_CLIENT_ID "c36a9fb6-4f2a-41ff-90bd-ae7cc92031eb" CACHE STRING " # This key was issued specifically for Prism Launcher set(Launcher_CURSEFORGE_API_KEY "$2a$10$wuAJuNZuted3NORVmpgUC.m8sI.pv1tOPKZyBgLFGjxFp/br0lZCC" CACHE STRING "API key for the CurseForge platform") +set(Launcher_COMPILER_NAME ${CMAKE_CXX_COMPILER_ID}) +set(Launcher_COMPILER_VERSION ${CMAKE_CXX_COMPILER_VERSION}) +set(Launcher_COMPILER_TARGET_SYSTEM ${CMAKE_SYSTEM_NAME}) +set(Launcher_COMPILER_TARGET_SYSTEM_VERSION ${CMAKE_SYSTEM_VERSION}) +set(Launcher_COMPILER_TARGET_PROCESSOR ${CMAKE_SYSTEM_PROCESSOR}) #### Check the current Git commit and branch include(GetGitRevisionDescription) diff --git a/README.md b/README.md index 641622b5c..093bc94da 100644 --- a/README.md +++ b/README.md @@ -50,7 +50,7 @@ Feel free to create a GitHub issue if you find a bug or want to suggest a new fe ## Translations -The translation effort for Prism Launcher is hosted on [Weblate](https://hosted.weblate.org/projects/prismlauncher/launcher/) and information about translating Prism Launcher is available at +The translation effort for Prism Launcher is hosted on [Weblate](https://hosted.weblate.org/projects/prismlauncher/launcher/) and information about translating Prism Launcher is available at . ## Building diff --git a/buildconfig/BuildConfig.cpp.in b/buildconfig/BuildConfig.cpp.in index 1eb0022b8..b40cacb0f 100644 --- a/buildconfig/BuildConfig.cpp.in +++ b/buildconfig/BuildConfig.cpp.in @@ -33,6 +33,7 @@ * limitations under the License. */ +#include #include "BuildConfig.h" #include @@ -59,8 +60,16 @@ Config::Config() VERSION_MINOR = @Launcher_VERSION_MINOR@; BUILD_PLATFORM = "@Launcher_BUILD_PLATFORM@"; + BUILD_ARTIFACT = "@Launcher_BUILD_ARTIFACT@"; BUILD_DATE = "@Launcher_BUILD_TIMESTAMP@"; - UPDATER_BASE = "@Launcher_UPDATER_BASE@"; + UPDATER_GITHUB_REPO = "@Launcher_UPDATER_GITHUB_REPO@"; + + COMPILER_NAME = "@Launcher_COMPILER_NAME@"; + COMPILER_VERSION = "@Launcher_COMPILER_VERSION@"; + + COMPILER_TARGET_SYSTEM = "@Launcher_COMPILER_TARGET_SYSTEM@"; + COMPILER_TARGET_SYSTEM_VERSION = "@Launcher_COMPILER_TARGET_SYSTEM_VERSION@"; + COMPILER_TARGET_SYSTEM_PROCESSOR = "@Launcher_COMPILER_TARGET_PROCESSOR@"; MAC_SPARKLE_PUB_KEY = "@MACOSX_SPARKLE_UPDATE_PUBLIC_KEY@"; MAC_SPARKLE_APPCAST_URL = "@MACOSX_SPARKLE_UPDATE_FEED_URL@"; @@ -68,6 +77,8 @@ Config::Config() if (!MAC_SPARKLE_PUB_KEY.isEmpty() && !MAC_SPARKLE_APPCAST_URL.isEmpty()) { UPDATER_ENABLED = true; + } else if(!UPDATER_GITHUB_REPO.isEmpty() && !BUILD_ARTIFACT.isEmpty()) { + UPDATER_ENABLED = true; } GIT_COMMIT = "@Launcher_GIT_COMMIT@"; @@ -88,10 +99,7 @@ Config::Config() if (GIT_REFSPEC.startsWith("refs/heads/")) { VERSION_CHANNEL = GIT_REFSPEC; - VERSION_CHANNEL.remove("refs/heads/"); - if(!UPDATER_BASE.isEmpty() && !BUILD_PLATFORM.isEmpty()) { - UPDATER_ENABLED = true; - } + VERSION_CHANNEL.remove("refs/heads/"); } else if (!GIT_COMMIT.isEmpty()) { @@ -136,3 +144,16 @@ QString Config::printableVersionString() const } return vstr; } + +QString Config::compilerID() const +{ + if (COMPILER_VERSION.isEmpty()) + return COMPILER_NAME; + return QStringLiteral("%1 - %2").arg(COMPILER_NAME).arg(COMPILER_VERSION); +} + +QString Config::systemID() const +{ + return QStringLiteral("%1 %2 %3").arg(COMPILER_TARGET_SYSTEM, COMPILER_TARGET_SYSTEM_VERSION, COMPILER_TARGET_SYSTEM_PROCESSOR); +} + diff --git a/buildconfig/BuildConfig.h b/buildconfig/BuildConfig.h index a5649b98f..77b6eef54 100644 --- a/buildconfig/BuildConfig.h +++ b/buildconfig/BuildConfig.h @@ -71,11 +71,29 @@ class Config { /// A short string identifying this build's platform or distribution. QString BUILD_PLATFORM; + /// A short string identifying this build's valid artifacts int he updater. For example, "lin64" or "win32". + QString BUILD_ARTIFACT; + /// A string containing the build timestamp QString BUILD_DATE; + /// A string identifying the compiler use to build + QString COMPILER_NAME; + + /// A string identifying the compiler version used to build + QString COMPILER_VERSION; + + /// A string identifying the compiler target system os + QString COMPILER_TARGET_SYSTEM; + + /// A String identifying the compiler target system version + QString COMPILER_TARGET_SYSTEM_VERSION; + + /// A String identifying the compiler target processor + QString COMPILER_TARGET_SYSTEM_PROCESSOR; + /// URL for the updater's channel - QString UPDATER_BASE; + QString UPDATER_GITHUB_REPO; /// The public key used to sign releases for the Sparkle updater appcast QString MAC_SPARKLE_PUB_KEY; @@ -175,6 +193,18 @@ class Config { * \return The version number in string format (major.minor.revision.build). */ QString printableVersionString() const; + + /** + * \brief Compiler ID String + * \return a string of the form "Name - Version" of just "Name" if the version is empty + */ + QString compilerID() const; + + /** + * \brief System ID String + * \return a string of the form "OS Verison Processor" + */ + QString systemID() const; }; extern const Config BuildConfig; diff --git a/cmake/CompilerWarnings.cmake b/cmake/CompilerWarnings.cmake index 69e57b4d5..c9962c443 100644 --- a/cmake/CompilerWarnings.cmake +++ b/cmake/CompilerWarnings.cmake @@ -89,6 +89,10 @@ function( -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 + # -Wgnu-zero-variadic-macro-arguments (part of -pedantic) is triggered by every qCDebug() call and therefore results + # in a lot of noise. This warning is only notifying us that clang is emulating the GCC behaviour + # instead of the exact standard wording so we can safely ignore it + -Wno-gnu-zero-variadic-macro-arguments ) endif() diff --git a/flake.lock b/flake.lock index f96336551..ad18ff615 100644 --- a/flake.lock +++ b/flake.lock @@ -3,11 +3,11 @@ "flake-compat": { "flake": false, "locked": { - "lastModified": 1673956053, - "narHash": "sha256-4gtG9iQuiKITOjNQQeQIpoIB6b16fm+504Ch3sNKLd8=", + "lastModified": 1696426674, + "narHash": "sha256-kvjfFW7WAETZlt09AgDn1MrtKzP7t90Vf7vypd3OL1U=", "owner": "edolstra", "repo": "flake-compat", - "rev": "35bb57c0c8d8b62bbfd284272c928ceb64ddbde9", + "rev": "0f9255e01c2351cc7d116c072cb317785dd33b33", "type": "github" }, "original": { @@ -21,11 +21,11 @@ "nixpkgs-lib": "nixpkgs-lib" }, "locked": { - "lastModified": 1693611461, - "narHash": "sha256-aPODl8vAgGQ0ZYFIRisxYG5MOGSkIczvu2Cd8Gb9+1Y=", + "lastModified": 1696343447, + "narHash": "sha256-B2xAZKLkkeRFG5XcHHSXXcP7To9Xzr59KXeZiRf4vdQ=", "owner": "hercules-ci", "repo": "flake-parts", - "rev": "7f53fdb7bdc5bb237da7fefef12d099e4fd611ca", + "rev": "c9afaba3dfa4085dbd2ccb38dfade5141e33d9d4", "type": "github" }, "original": { @@ -106,11 +106,11 @@ }, "nixpkgs": { "locked": { - "lastModified": 1695318763, - "narHash": "sha256-FHVPDRP2AfvsxAdc+AsgFJevMz5VBmnZglFUMlxBkcY=", + "lastModified": 1697009197, + "narHash": "sha256-viVRhBTFT8fPJTb1N3brQIpFZnttmwo3JVKNuWRVc3s=", "owner": "nixos", "repo": "nixpkgs", - "rev": "e12483116b3b51a185a33a272bf351e357ba9a99", + "rev": "01441e14af5e29c9d27ace398e6dd0b293e25a54", "type": "github" }, "original": { @@ -123,11 +123,11 @@ "nixpkgs-lib": { "locked": { "dir": "lib", - "lastModified": 1693471703, - "narHash": "sha256-0l03ZBL8P1P6z8MaSDS/MvuU8E75rVxe5eE1N6gxeTo=", + "lastModified": 1696019113, + "narHash": "sha256-X3+DKYWJm93DRSdC5M6K5hLqzSya9BjibtBsuARoPco=", "owner": "NixOS", "repo": "nixpkgs", - "rev": "3e52e76b70d5508f3cec70b882a29199f4d1ee85", + "rev": "f5892ddac112a1e9b3612c39af1b72987ee5783a", "type": "github" }, "original": { @@ -153,11 +153,11 @@ ] }, "locked": { - "lastModified": 1694364351, - "narHash": "sha256-oadhSCqopYXxURwIA6/Anpe5IAG11q2LhvTJNP5zE6o=", + "lastModified": 1696846637, + "narHash": "sha256-0hv4kbXxci2+pxhuXlVgftj/Jq79VSmtAyvfabCCtYk=", "owner": "cachix", "repo": "pre-commit-hooks.nix", - "rev": "4f883a76282bc28eb952570afc3d8a1bf6f481d7", + "rev": "42e1b6095ef80a51f79595d9951eb38e91c4e6ca", "type": "github" }, "original": { diff --git a/launcher/Application.cpp b/launcher/Application.cpp index 11e42f163..3b553f4b1 100644 --- a/launcher/Application.cpp +++ b/launcher/Application.cpp @@ -122,6 +122,7 @@ #include #include +#include #include #ifdef Q_OS_LINUX @@ -130,9 +131,13 @@ #include "gamemode_client.h" #endif -#if defined(Q_OS_MAC) && defined(SPARKLE_ENABLED) +#if defined(Q_OS_MAC) +#if defined(SPARKLE_ENABLED) #include "updater/MacSparkleUpdater.h" #endif +#else +#include "updater/PrismExternalUpdater.h" +#endif #if defined Q_OS_WIN32 #include "WindowsConsole.h" @@ -164,6 +169,34 @@ void appDebugOutput(QtMsgType type, const QMessageLogContext& context, const QSt } // namespace +std::tuple read_lock_File(const QString& path) +{ + auto contents = QString(FS::read(path)); + auto lines = contents.split('\n'); + + QDateTime timestamp; + QString from, to, target, data_path; + for (auto line : lines) { + auto index = line.indexOf("="); + if (index < 0) + continue; + auto left = line.left(index); + auto right = line.mid(index + 1); + if (left.toLower() == "timestamp") { + timestamp = QDateTime::fromString(right, Qt::ISODate); + } else if (left.toLower() == "from") { + from = right; + } else if (left.toLower() == "to") { + to = right; + } else if (left.toLower() == "target") { + target = right; + } else if (left.toLower() == "data_path") { + data_path = right; + } + } + return std::make_tuple(timestamp, from, to, target, data_path); +} + Application::Application(int& argc, char** argv) : QApplication(argc, argv) { #if defined Q_OS_WIN32 @@ -296,6 +329,7 @@ Application::Application(int& argc, char** argv) : QApplication(argc, argv) .arg(dataPath)); return; } + m_dataPath = dataPath; /* * Establish the mechanism for communication with an already running PrismLauncher that uses the same data path. @@ -450,11 +484,16 @@ Application::Application(int& argc, char** argv) : QApplication(argc, argv) } { - qDebug() << BuildConfig.LAUNCHER_DISPLAYNAME << ", (c) 2013-2021 " << BuildConfig.LAUNCHER_COPYRIGHT; + qDebug() << qPrintable(BuildConfig.LAUNCHER_DISPLAYNAME) << ", (c) 2022-2023 " + << qPrintable(QString(BuildConfig.LAUNCHER_COPYRIGHT).replace("\n", ", ")); qDebug() << "Version : " << BuildConfig.printableVersionString(); qDebug() << "Platform : " << BuildConfig.BUILD_PLATFORM; qDebug() << "Git commit : " << BuildConfig.GIT_COMMIT; qDebug() << "Git refspec : " << BuildConfig.GIT_REFSPEC; + qDebug() << "Compiled for : " << BuildConfig.systemID(); + qDebug() << "Compiled by : " << BuildConfig.compilerID(); + qDebug() << "Build Artifact : " << BuildConfig.BUILD_ARTIFACT; + qDebug() << "Updates Enabled : " << (updaterEnabled() ? "Yes" : "No"); if (adjustedBy.size()) { qDebug() << "Work dir before adjustment : " << origcwdPath; qDebug() << "Work dir after adjustment : " << QDir::currentPath(); @@ -582,6 +621,9 @@ Application::Application(int& argc, char** argv) : QApplication(argc, argv) m_settings->registerSetting("IgnoreJavaCompatibility", false); m_settings->registerSetting("IgnoreJavaWizard", false); + // Legacy settings + m_settings->registerSetting("OnlineFixes", false); + // Native library workarounds m_settings->registerSetting("UseNativeOpenAL", false); m_settings->registerSetting("CustomOpenALPath", ""); @@ -738,15 +780,6 @@ Application::Application(int& argc, char** argv) : QApplication(argc, argv) qDebug() << "<> Translations loaded."; } - // initialize the updater - if (BuildConfig.UPDATER_ENABLED) { - qDebug() << "Initializing updater"; -#if defined(Q_OS_MAC) && defined(SPARKLE_ENABLED) - m_updater.reset(new MacSparkleUpdater()); -#endif - qDebug() << "<> Updater started."; - } - // Instance icons { auto setting = APPLICATION->settings()->getSetting("IconsDir"); @@ -849,6 +882,107 @@ Application::Application(int& argc, char** argv) : QApplication(argc, argv) detectLibraries(); + // check update locks + { + auto update_log_path = FS::PathCombine(m_dataPath, "logs", "prism_launcher_update.log"); + + auto update_lock = QFileInfo(FS::PathCombine(m_dataPath, ".prism_launcher_update.lock")); + if (update_lock.exists()) { + auto [timestamp, from, to, target, data_path] = read_lock_File(update_lock.absoluteFilePath()); + auto infoMsg = tr("This installation has a update lock file present at: %1\n" + "\n" + "Timestamp: %2\n" + "Updating from version %3 to %4\n" + "Target install path: %5\n" + "Data Path: %6" + "\n" + "This likely means that a update attempt failed. Please ensure your installation is in working order before " + "proceeding.\n" + "Check the Prism Launcher updater log at: \n" + "%7\n" + "for details on the last update attempt.\n" + "\n" + "To delete this lock and proceed select \"Ignore\" below.") + .arg(update_lock.absoluteFilePath()) + .arg(timestamp.toString(Qt::ISODate), from, to, target, data_path) + .arg(update_log_path); + auto msgBox = QMessageBox(QMessageBox::Warning, tr("Update In Progress"), infoMsg, QMessageBox::Ignore | QMessageBox::Abort); + msgBox.setDefaultButton(QMessageBox::Abort); + msgBox.setModal(true); + msgBox.setDetailedText(FS::read(update_log_path)); + msgBox.setMinimumWidth(460); + msgBox.adjustSize(); + auto res = msgBox.exec(); + switch (res) { + case QMessageBox::Ignore: { + FS::deletePath(update_lock.absoluteFilePath()); + break; + } + case QMessageBox::Abort: + [[fallthrough]]; + default: { + qDebug() << "Exiting because update lockfile is present"; + QMetaObject::invokeMethod( + this, []() { exit(1); }, Qt::QueuedConnection); + return; + } + } + } + + auto update_fail_marker = QFileInfo(FS::PathCombine(m_dataPath, ".prism_launcher_update.fail")); + if (update_fail_marker.exists()) { + auto infoMsg = tr("An update attempt failed\n" + "\n" + "Please ensure your installation is in working order before " + "proceeding.\n" + "Check the Prism Launcher updater log at: \n" + "%1\n" + "for details on the last update attempt.") + .arg(update_log_path); + auto msgBox = QMessageBox(QMessageBox::Warning, tr("Update Failed"), infoMsg, QMessageBox::Ignore | QMessageBox::Abort); + msgBox.setDefaultButton(QMessageBox::Abort); + msgBox.setModal(true); + msgBox.setDetailedText(FS::read(update_log_path)); + msgBox.setMinimumWidth(460); + msgBox.adjustSize(); + auto res = msgBox.exec(); + switch (res) { + case QMessageBox::Ignore: { + FS::deletePath(update_fail_marker.absoluteFilePath()); + break; + } + case QMessageBox::Abort: + [[fallthrough]]; + default: { + qDebug() << "Exiting because update lockfile is present"; + QMetaObject::invokeMethod( + this, []() { exit(1); }, Qt::QueuedConnection); + return; + } + } + } + + auto update_success_marker = QFileInfo(FS::PathCombine(m_dataPath, ".prism_launcher_update.success")); + if (update_success_marker.exists()) { + auto infoMsg = tr("Update succeeded\n" + "\n" + "You are now running %1 .\n" + "Check the Prism Launcher updater log at: \n" + "%1\n" + "for details.") + .arg(BuildConfig.printableVersionString()) + .arg(update_log_path); + auto msgBox = new QMessageBox(QMessageBox::Information, tr("Update Succeeded"), infoMsg, QMessageBox::Ok); + msgBox->setDefaultButton(QMessageBox::Ok); + msgBox->setDetailedText(FS::read(update_log_path)); + msgBox->setAttribute(Qt::WA_DeleteOnClose); + msgBox->setMinimumWidth(460); + msgBox->adjustSize(); + msgBox->open(); + FS::deletePath(update_success_marker.absoluteFilePath()); + } + } + if (createSetupWizard()) { return; } @@ -917,6 +1051,26 @@ bool Application::createSetupWizard() return false; } +bool Application::updaterEnabled() +{ +#if defined(Q_OS_MAC) + return BuildConfig.UPDATER_ENABLED; +#else + return BuildConfig.UPDATER_ENABLED && QFileInfo(FS::PathCombine(m_rootPath, updaterBinaryName())).isFile(); +#endif +} + +QString Application::updaterBinaryName() +{ + auto exe_name = QStringLiteral("%1_updater").arg(BuildConfig.LAUNCHER_APP_BINARY_NAME); +#if defined Q_OS_WIN32 + exe_name.append(".exe"); +#else + exe_name.prepend("bin/"); +#endif + return exe_name; +} + bool Application::event(QEvent* event) { #ifdef Q_OS_MACOS @@ -985,6 +1139,20 @@ void Application::performMainStartupAction() showMainWindow(false); qDebug() << "<> Main window shown."; } + + // initialize the updater + if (updaterEnabled()) { + qDebug() << "Initializing updater"; +#ifdef Q_OS_MAC +#if defined(SPARKLE_ENABLED) + m_updater.reset(new MacSparkleUpdater()); +#endif +#else + m_updater.reset(new PrismExternalUpdater(m_mainWindow, m_rootPath, m_dataPath)); +#endif + qDebug() << "<> Updater started."; + } + if (!m_urlsToImport.isEmpty()) { qDebug() << "<> Importing from url:" << m_urlsToImport; m_mainWindow->processURLs(m_urlsToImport); diff --git a/launcher/Application.h b/launcher/Application.h index b227bb813..7669e08ec 100644 --- a/launcher/Application.h +++ b/launcher/Application.h @@ -159,6 +159,9 @@ class Application : public QApplication { /// this is the root of the 'installation'. Used for automatic updates const QString& root() { return m_rootPath; } + /// the data path the application is using + const QString& dataRoot() { return m_dataPath; } + bool isPortable() { return m_portable; } const Capabilities capabilities() { return m_capabilities; } @@ -179,6 +182,9 @@ class Application : public QApplication { int suitableMaxMem(); + bool updaterEnabled(); + QString updaterBinaryName(); + QUrl normalizeImportUrl(QString const& url); signals: @@ -244,6 +250,7 @@ class Application : public QApplication { QMap> m_profilers; QString m_rootPath; + QString m_dataPath; Status m_status = Application::StartingUp; Capabilities m_capabilities; bool m_portable = false; diff --git a/launcher/CMakeLists.txt b/launcher/CMakeLists.txt index a7de01051..d15dc85de 100644 --- a/launcher/CMakeLists.txt +++ b/launcher/CMakeLists.txt @@ -181,6 +181,11 @@ set(MAC_UPDATE_SOURCES updater/MacSparkleUpdater.mm ) +set(PRISM_UPDATE_SOURCES + updater/PrismExternalUpdater.h + updater/PrismExternalUpdater.cpp +) + # Backend for the news bar... there's usually no news. set(NEWS_SOURCES # News System @@ -216,13 +221,9 @@ set(MINECRAFT_SOURCES minecraft/auth/MinecraftAccount.h minecraft/auth/Parsers.cpp minecraft/auth/Parsers.h - minecraft/auth/Yggdrasil.cpp - minecraft/auth/Yggdrasil.h minecraft/auth/flows/AuthFlow.cpp minecraft/auth/flows/AuthFlow.h - minecraft/auth/flows/Mojang.cpp - minecraft/auth/flows/Mojang.h minecraft/auth/flows/MSA.cpp minecraft/auth/flows/MSA.h minecraft/auth/flows/Offline.cpp @@ -236,12 +237,8 @@ set(MINECRAFT_SOURCES minecraft/auth/steps/GetSkinStep.h minecraft/auth/steps/LauncherLoginStep.cpp minecraft/auth/steps/LauncherLoginStep.h - minecraft/auth/steps/MigrationEligibilityStep.cpp - minecraft/auth/steps/MigrationEligibilityStep.h minecraft/auth/steps/MinecraftProfileStep.cpp minecraft/auth/steps/MinecraftProfileStep.h - minecraft/auth/steps/MinecraftProfileStepMojang.cpp - minecraft/auth/steps/MinecraftProfileStepMojang.h minecraft/auth/steps/MSAStep.cpp minecraft/auth/steps/MSAStep.h minecraft/auth/steps/XboxAuthorizationStep.cpp @@ -250,8 +247,6 @@ set(MINECRAFT_SOURCES minecraft/auth/steps/XboxProfileStep.h minecraft/auth/steps/XboxUserStep.cpp minecraft/auth/steps/XboxUserStep.h - minecraft/auth/steps/YggdrasilStep.cpp - minecraft/auth/steps/YggdrasilStep.h minecraft/gameoptions/GameOptions.h minecraft/gameoptions/GameOptions.cpp @@ -589,6 +584,63 @@ set(LINKEXE_SOURCES DesktopServices.cpp ) +set(PRISMUPDATER_SOURCES + updater/prismupdater/PrismUpdater.h + updater/prismupdater/PrismUpdater.cpp + updater/prismupdater/UpdaterDialogs.h + updater/prismupdater/UpdaterDialogs.cpp + updater/prismupdater/GitHubRelease.h + updater/prismupdater/GitHubRelease.cpp + + Json.h + Json.cpp + FileSystem.h + FileSystem.cpp + StringUtils.h + StringUtils.cpp + DesktopServices.h + DesktopServices.cpp + Version.h + Version.cpp + Markdown.h + Markdown.cpp + + # Zip + MMCZip.h + MMCZip.cpp + + # Time + MMCTime.h + MMCTime.cpp + + net/ByteArraySink.h + net/ChecksumValidator.h + net/Download.cpp + net/Download.h + net/FileSink.cpp + net/FileSink.h + net/HttpMetaCache.cpp + net/HttpMetaCache.h + net/Logging.h + net/Logging.cpp + net/NetAction.h + net/NetRequest.cpp + net/NetRequest.h + net/NetJob.cpp + net/NetJob.h + net/NetUtils.h + net/Sink.h + net/Validator.h + net/HeaderProxy.h + net/RawHeaderProxy.h + + ui/dialogs/ProgressDialog.cpp + ui/dialogs/ProgressDialog.h + ui/widgets/SubTaskProgressBar.h + ui/widgets/SubTaskProgressBar.cpp + +) + ######## Logging categories ######## ecm_qt_declare_logging_category(CORE_SOURCES @@ -685,6 +737,8 @@ set(LOGIC_SOURCES if(APPLE AND Launcher_ENABLE_UPDATER) set (LOGIC_SOURCES ${LOGIC_SOURCES} ${MAC_UPDATE_SOURCES}) +else() + set (LOGIC_SOURCES ${LOGIC_SOURCES} ${PRISM_UPDATE_SOURCES}) endif() SET(LAUNCHER_SOURCES @@ -916,6 +970,9 @@ SET(LAUNCHER_SOURCES ui/pages/modplatform/ImportPage.cpp ui/pages/modplatform/ImportPage.h + ui/pages/modplatform/OptionalModDialog.cpp + ui/pages/modplatform/OptionalModDialog.h + ui/pages/modplatform/modrinth/ModrinthResourceModels.cpp ui/pages/modplatform/modrinth/ModrinthResourceModels.h ui/pages/modplatform/modrinth/ModrinthResourcePages.cpp @@ -944,8 +1001,6 @@ SET(LAUNCHER_SOURCES ui/dialogs/IconPickerDialog.h ui/dialogs/ImportResourceDialog.cpp ui/dialogs/ImportResourceDialog.h - ui/dialogs/LoginDialog.cpp - ui/dialogs/LoginDialog.h ui/dialogs/MSALoginDialog.cpp ui/dialogs/MSALoginDialog.h ui/dialogs/OfflineLoginDialog.cpp @@ -1042,6 +1097,15 @@ SET(LAUNCHER_SOURCES ui/instanceview/VisualGroup.h ) +if (NOT Apple) +set(LAUNCHER_SOURCES + ${LAUNCHER_SOURCES} + + ui/dialogs/UpdateAvailableDialog.h + ui/dialogs/UpdateAvailableDialog.cpp +) +endif() + if(WIN32) set(LAUNCHER_SOURCES WindowsConsole.cpp @@ -1080,6 +1144,7 @@ qt_wrap_ui(LAUNCHER_UI ui/pages/modplatform/legacy_ftb/Page.ui ui/pages/modplatform/import_ftb/ImportFTBPage.ui ui/pages/modplatform/ImportPage.ui + ui/pages/modplatform/OptionalModDialog.ui ui/pages/modplatform/modrinth/ModrinthPage.ui ui/pages/modplatform/technic/TechnicPage.ui ui/widgets/InstanceCardWidget.ui @@ -1104,7 +1169,6 @@ qt_wrap_ui(LAUNCHER_UI ui/dialogs/MSALoginDialog.ui ui/dialogs/OfflineLoginDialog.ui ui/dialogs/AboutDialog.ui - ui/dialogs/LoginDialog.ui ui/dialogs/EditAccountDialog.ui ui/dialogs/ReviewMessageBox.ui ui/dialogs/ScrollMessageBox.ui @@ -1112,6 +1176,14 @@ qt_wrap_ui(LAUNCHER_UI ui/dialogs/ChooseProviderDialog.ui ) +qt_wrap_ui(PRISM_UPDATE_UI + ui/dialogs/UpdateAvailableDialog.ui +) + +if (NOT Apple) + set (LAUNCHER_UI ${LAUNCHER_UI} ${PRISM_UPDATE_UI}) +endif() + qt_add_resources(LAUNCHER_RESOURCES resources/backgrounds/backgrounds.qrc resources/multimc/multimc.qrc @@ -1128,6 +1200,12 @@ qt_add_resources(LAUNCHER_RESOURCES ../${Launcher_Branding_LogoQRC} ) +qt_wrap_ui(PRISMUPDATER_UI + updater/prismupdater/SelectReleaseDialog.ui + ui/widgets/SubTaskProgressBar.ui + ui/dialogs/ProgressDialog.ui +) + ######## Windows resource files ######## if(WIN32) set(LAUNCHER_RCS ${CMAKE_CURRENT_BINARY_DIR}/../${Launcher_Branding_WindowsRC}) @@ -1146,6 +1224,7 @@ set_project_warnings(Launcher_logic "${Launcher_GCC_WARNINGS}") target_compile_definitions(Launcher_logic PUBLIC LAUNCHER_APPLICATION) target_include_directories(Launcher_logic PUBLIC ${CMAKE_CURRENT_SOURCE_DIR}) +target_compile_definitions(Launcher_logic PUBLIC LAUNCHER_APPLICATION) target_link_libraries(Launcher_logic systeminfo Launcher_murmur2 @@ -1227,7 +1306,45 @@ install(TARGETS ${Launcher_Name} FRAMEWORK DESTINATION ${FRAMEWORK_DEST_DIR} COMPONENT Runtime ) -if(WIN32) +if(NOT APPLE OR (DEFINED Launcher_BUILD_UPDATER AND Launcher_BUILD_UPDATER)) + # Updater + add_library(prism_updater_logic STATIC ${PRISMUPDATER_SOURCES} ${TASKS_SOURCES} ${PRISMUPDATER_UI}) + target_include_directories(prism_updater_logic PUBLIC ${CMAKE_CURRENT_SOURCE_DIR}) + target_link_libraries(prism_updater_logic + QuaZip::QuaZip + ${ZLIB_LIBRARIES} + systeminfo + BuildConfig + ghcFilesystem::ghc_filesystem + Qt${QT_VERSION_MAJOR}::Widgets + Qt${QT_VERSION_MAJOR}::Core + Qt${QT_VERSION_MAJOR}::Network + ${Launcher_QT_LIBS} + cmark::cmark + Katabasis + ) + + add_executable("${Launcher_Name}_updater" WIN32 updater/prismupdater/updater_main.cpp) + target_sources("${Launcher_Name}_updater" PRIVATE updater/prismupdater/updater.exe.manifest) + target_link_libraries("${Launcher_Name}_updater" prism_updater_logic) + + if(DEFINED Launcher_APP_BINARY_NAME) + set_target_properties("${Launcher_Name}_updater" PROPERTIES OUTPUT_NAME "${Launcher_APP_BINARY_NAME}_updater") + endif() + if(DEFINED Launcher_BINARY_RPATH) + SET_TARGET_PROPERTIES("${Launcher_Name}_updater" PROPERTIES INSTALL_RPATH "${Launcher_BINARY_RPATH}") + endif() + + install(TARGETS "${Launcher_Name}_updater" + BUNDLE DESTINATION "." COMPONENT Runtime + LIBRARY DESTINATION ${LIBRARY_DEST_DIR} COMPONENT Runtime + RUNTIME DESTINATION ${BINARY_DEST_DIR} COMPONENT Runtime + FRAMEWORK DESTINATION ${FRAMEWORK_DEST_DIR} COMPONENT Runtime + ) +endif() + +if(WIN32 OR (DEFINED Launcher_BUILD_FILELINKER AND Launcher_BUILD_FILELINKER)) + # File link add_library(filelink_logic STATIC ${LINKEXE_SOURCES}) set_project_warnings(filelink_logic "${Launcher_MSVC_WARNINGS}" @@ -1246,7 +1363,7 @@ if(WIN32) ${Launcher_QT_LIBS} ) - add_executable("${Launcher_Name}_filelink" WIN32 filelink/main.cpp) + add_executable("${Launcher_Name}_filelink" WIN32 filelink/filelink_main.cpp) target_sources("${Launcher_Name}_filelink" PRIVATE filelink/filelink.exe.manifest) diff --git a/launcher/FileSystem.cpp b/launcher/FileSystem.cpp index defb2cb9e..652ba2995 100644 --- a/launcher/FileSystem.cpp +++ b/launcher/FileSystem.cpp @@ -194,6 +194,40 @@ void write(const QString& filename, const QByteArray& data) } } +void appendSafe(const QString& filename, const QByteArray& data) +{ + ensureExists(QFileInfo(filename).dir()); + QByteArray buffer; + try { + buffer = read(filename); + } catch (FileSystemException&) { + buffer = QByteArray(); + } + buffer.append(data); + QSaveFile file(filename); + if (!file.open(QSaveFile::WriteOnly)) { + throw FileSystemException("Couldn't open " + filename + " for writing: " + file.errorString()); + } + if (buffer.size() != file.write(buffer)) { + throw FileSystemException("Error writing data to " + filename + ": " + file.errorString()); + } + if (!file.commit()) { + throw FileSystemException("Error while committing data to " + filename + ": " + file.errorString()); + } +} + +void append(const QString& filename, const QByteArray& data) +{ + ensureExists(QFileInfo(filename).dir()); + QFile file(filename); + if (!file.open(QFile::Append)) { + throw FileSystemException("Couldn't open " + filename + " for writing: " + file.errorString()); + } + if (data.size() != file.write(data)) { + throw FileSystemException("Error writing data to " + filename + ": " + file.errorString()); + } +} + QByteArray read(const QString& filename) { QFile file(filename); @@ -238,6 +272,28 @@ bool ensureFolderPathExists(QString foldernamepath) return success; } +bool copyFileAttributes(QString src, QString dst) +{ +#ifdef Q_OS_WIN32 + auto attrs = GetFileAttributesW(src.toStdWString().c_str()); + if (attrs == INVALID_FILE_ATTRIBUTES) + return false; + return SetFileAttributesW(dst.toStdWString().c_str(), attrs); +#endif + return true; +} + +// needs folders to exists +void copyFolderAttributes(QString src, QString dst, QString relative) +{ + auto path = PathCombine(src, relative); + QDir dsrc(src); + while ((path = QFileInfo(path).path()).length() >= src.length()) { + auto dst_path = PathCombine(dst, dsrc.relativeFilePath(path)); + copyFileAttributes(path, dst_path); + } +} + /** * @brief Copies a directory and it's contents from src to dest * @param offset subdirectory form src to copy to dest @@ -265,6 +321,9 @@ bool copy::operator()(const QString& offset, bool dryRun) if (!m_followSymlinks) opt |= copy_opts::copy_symlinks; + if (m_overwrite) + opt |= copy_opts::overwrite_existing; + // Function that'll do the actual copying auto copy_file = [&](QString src_path, QString relative_dst_path) { if (m_matcher && (m_matcher->matches(relative_dst_path) != m_whitelist)) @@ -273,6 +332,9 @@ bool copy::operator()(const QString& offset, bool dryRun) auto dst_path = PathCombine(dst, relative_dst_path); if (!dryRun) { ensureFilePathExists(dst_path); +#ifdef Q_OS_WIN32 + copyFolderAttributes(src, dst, relative_dst_path); +#endif fs::copy(StringUtils::toStdString(src_path), StringUtils::toStdString(dst_path), opt, err); } if (err) { diff --git a/launcher/FileSystem.h b/launcher/FileSystem.h index bfed576c1..861cfa267 100644 --- a/launcher/FileSystem.h +++ b/launcher/FileSystem.h @@ -61,6 +61,16 @@ class FileSystemException : public ::Exception { */ void write(const QString& filename, const QByteArray& data); +/** + * append data to a file safely + */ +void appendSafe(const QString& filename, const QByteArray& data); + +/** + * append data to a file + */ +void append(const QString& filename, const QByteArray& data); + /** * read data from a file safely\ */ @@ -109,6 +119,11 @@ class copy : public QObject { m_whitelist = whitelist; return *this; } + copy& overwrite(const bool overwrite) + { + m_overwrite = overwrite; + return *this; + } bool operator()(bool dryRun = false) { return operator()(QString(), dryRun); } @@ -128,6 +143,7 @@ class copy : public QObject { bool m_followSymlinks = true; const IPathMatcher* m_matcher = nullptr; bool m_whitelist = false; + bool m_overwrite = false; QDir m_src; QDir m_dst; qsizetype m_copied; diff --git a/launcher/InstanceList.cpp b/launcher/InstanceList.cpp index 856eee816..756ff93dd 100644 --- a/launcher/InstanceList.cpp +++ b/launcher/InstanceList.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 @@ -237,8 +238,11 @@ GroupId InstanceList::getInstanceGroup(const InstanceId& id) const return GroupId(); } -void InstanceList::setInstanceGroup(const InstanceId& id, const GroupId& name) +void InstanceList::setInstanceGroup(const InstanceId& id, GroupId name) { + if (name.isEmpty() && !name.isNull()) + name = QString(); + auto inst = getInstanceById(id); if (!inst) { qDebug() << "Attempt to set a null instance's group"; @@ -249,6 +253,7 @@ void InstanceList::setInstanceGroup(const InstanceId& id, const GroupId& name) auto iter = m_instanceGroupIndex.find(inst->id()); if (iter != m_instanceGroupIndex.end()) { if (*iter != name) { + decreaseGroupCount(*iter); *iter = name; changed = true; } @@ -258,7 +263,7 @@ void InstanceList::setInstanceGroup(const InstanceId& id, const GroupId& name) } if (changed) { - m_groupNameCache.insert(name); + increaseGroupCount(name); auto idx = getInstIndex(inst.get()); emit dataChanged(index(idx), index(idx), { GroupRole }); saveGroupList(); @@ -267,29 +272,55 @@ void InstanceList::setInstanceGroup(const InstanceId& id, const GroupId& name) QStringList InstanceList::getGroups() { - return m_groupNameCache.values(); + return m_groupNameCache.keys(); } -void InstanceList::deleteGroup(const QString& name) +void InstanceList::deleteGroup(const GroupId& name) { + m_groupNameCache.remove(name); + m_collapsedGroups.remove(name); + bool removed = false; qDebug() << "Delete group" << name; for (auto& instance : m_instances) { - const auto& instID = instance->id(); - auto instGroupName = getInstanceGroup(instID); + const QString& instID = instance->id(); + const QString instGroupName = getInstanceGroup(instID); if (instGroupName == name) { m_instanceGroupIndex.remove(instID); qDebug() << "Remove" << instID << "from group" << name; removed = true; auto idx = getInstIndex(instance.get()); - if (idx > 0) { + if (idx > 0) emit dataChanged(index(idx), index(idx), { GroupRole }); - } } } - if (removed) { + if (removed) saveGroupList(); +} + +void InstanceList::renameGroup(const QString& src, const QString& dst) +{ + m_groupNameCache.remove(src); + if (m_collapsedGroups.remove(src)) + m_collapsedGroups.insert(dst); + + bool modified = false; + qDebug() << "Rename group" << src << "to" << dst; + for (auto& instance : m_instances) { + const QString& instID = instance->id(); + const QString instGroupName = getInstanceGroup(instID); + if (instGroupName == src) { + m_instanceGroupIndex[instID] = dst; + increaseGroupCount(dst); + qDebug() << "Set" << instID << "group to" << dst; + modified = true; + auto idx = getInstIndex(instance.get()); + if (idx > 0) + emit dataChanged(index(idx), index(idx), { GroupRole }); + } } + if (modified) + saveGroupList(); } bool InstanceList::isGroupCollapsed(const QString& group) @@ -305,12 +336,13 @@ bool InstanceList::trashInstance(const InstanceId& id) return false; } - auto cachedGroupId = m_instanceGroupIndex[id]; + QString cachedGroupId = m_instanceGroupIndex[id]; qDebug() << "Will trash instance" << id; QString trashedLoc; if (m_instanceGroupIndex.remove(id)) { + decreaseGroupCount(cachedGroupId); saveGroupList(); } @@ -348,7 +380,7 @@ void InstanceList::undoTrashInstance() QFile(top.trashPath).rename(top.polyPath); m_instanceGroupIndex[top.id] = top.groupName; - m_groupNameCache.insert(top.groupName); + increaseGroupCount(top.groupName); saveGroupList(); emit instancesChanged(); @@ -362,7 +394,10 @@ void InstanceList::deleteInstance(const InstanceId& id) return; } + QString cachedGroupId = m_instanceGroupIndex[id]; + if (m_instanceGroupIndex.remove(id)) { + decreaseGroupCount(cachedGroupId); saveGroupList(); } @@ -610,6 +645,25 @@ InstancePtr InstanceList::loadInstance(const InstanceId& id) return inst; } +void InstanceList::increaseGroupCount(const QString& group) +{ + if (group.isEmpty()) + return; + + ++m_groupNameCache[group]; +} + +void InstanceList::decreaseGroupCount(const QString& group) +{ + if (group.isEmpty()) + return; + + if (--m_groupNameCache[group] < 1) { + m_groupNameCache.remove(group); + m_collapsedGroups.remove(group); + } +} + void InstanceList::saveGroupList() { qDebug() << "Will save group list now."; @@ -621,7 +675,7 @@ void InstanceList::saveGroupList() QString groupFileName = m_instDir + "/instgroups.json"; QMap> reverseGroupMap; for (auto iter = m_instanceGroupIndex.begin(); iter != m_instanceGroupIndex.end(); iter++) { - QString id = iter.key(); + const QString& id = iter.key(); QString group = iter.value(); if (group.isEmpty()) continue; @@ -711,17 +765,22 @@ void InstanceList::loadGroupList() return; } - QSet groupSet; m_instanceGroupIndex.clear(); + m_groupNameCache.clear(); // Iterate through all the groups. QJsonObject groupMapping = rootObj.value("groups").toObject(); for (QJsonObject::iterator iter = groupMapping.begin(); iter != groupMapping.end(); iter++) { QString groupName = iter.key(); + if (iter.key().isEmpty()) { + qWarning() << "Redundant empty group found"; + continue; + } + // If not an object, complain and skip to the next one. if (!iter.value().isObject()) { - qWarning() << QString("Group '%1' in the group list should be an object.").arg(groupName).toUtf8(); + qWarning() << QString("Group '%1' in the group list should be an object").arg(groupName).toUtf8(); continue; } @@ -733,23 +792,19 @@ void InstanceList::loadGroupList() continue; } - // keep a list/set of groups for choosing - groupSet.insert(groupName); - auto hidden = groupObj.value("hidden").toBool(false); - if (hidden) { + if (hidden) m_collapsedGroups.insert(groupName); - } // Iterate through the list of instances in the group. QJsonArray instancesArray = groupObj.value("instances").toArray(); - for (QJsonArray::iterator iter2 = instancesArray.begin(); iter2 != instancesArray.end(); iter2++) { - m_instanceGroupIndex[(*iter2).toString()] = groupName; + for (auto value : instancesArray) { + m_instanceGroupIndex[value.toString()] = groupName; + increaseGroupCount(groupName); } } m_groupsLoaded = true; - m_groupNameCache.unite(groupSet); qDebug() << "Group list loaded."; } @@ -925,7 +980,7 @@ bool InstanceList::commitStagedInstance(const QString& path, } m_instanceGroupIndex[instID] = groupName; - m_groupNameCache.insert(groupName); + increaseGroupCount(groupName); } instanceSet.insert(instID); diff --git a/launcher/InstanceList.h b/launcher/InstanceList.h index ee4578ffd..6b0bcd810 100644 --- a/launcher/InstanceList.h +++ b/launcher/InstanceList.h @@ -1,16 +1,36 @@ -/* Copyright 2013-2021 MultiMC Contributors +// SPDX-License-Identifier: GPL-3.0-only +/* + * Prism Launcher - Minecraft Launcher + * Copyright (C) 2023 TheKodeToad * - * 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 @@ -86,9 +106,10 @@ class InstanceList : public QAbstractListModel { bool isGroupCollapsed(const QString& groupName); GroupId getInstanceGroup(const InstanceId& id) const; - void setInstanceGroup(const InstanceId& id, const GroupId& name); + void setInstanceGroup(const InstanceId& id, GroupId name); void deleteGroup(const GroupId& name); + void renameGroup(const GroupId& src, const GroupId& dst); bool trashInstance(const InstanceId& id); bool trashedSomething(); void undoTrashInstance(); @@ -158,12 +179,16 @@ class InstanceList : public QAbstractListModel { QList discoverInstances(); InstancePtr loadInstance(const InstanceId& id); + void increaseGroupCount(const QString& group); + void decreaseGroupCount(const QString& group); + private: int m_watchLevel = 0; int totalPlayTime = 0; bool m_dirty = false; QList m_instances; - QSet m_groupNameCache; + // id -> refs + QMap m_groupNameCache; SettingsObjectPtr m_globalSettings; QString m_instDir; diff --git a/launcher/LaunchController.cpp b/launcher/LaunchController.cpp index b21eb281f..21a146062 100644 --- a/launcher/LaunchController.cpp +++ b/launcher/LaunchController.cpp @@ -88,8 +88,8 @@ void LaunchController::decideAccount() if (accounts->count() <= 0) { // Tell the user they need to log in at least one account in order to play. auto reply = CustomMessageBox::selectable(m_parentWidget, tr("No Accounts"), - tr("In order to play Minecraft, you must have at least one Microsoft or Mojang " - "account logged in. Mojang accounts can only be used offline. " + tr("In order to play Minecraft, you must have at least one Microsoft " + "account which owns Minecraft logged in." "Would you like to open the account manager to add an account now?"), QMessageBox::Information, QMessageBox::Yes | QMessageBox::No) ->exec(); diff --git a/launcher/MMCZip.cpp b/launcher/MMCZip.cpp index acd6bf7e4..3bfe16ab5 100644 --- a/launcher/MMCZip.cpp +++ b/launcher/MMCZip.cpp @@ -42,7 +42,11 @@ #include #include +#include + +#if defined(LAUNCHER_APPLICATION) #include +#endif namespace MMCZip { // ours @@ -132,6 +136,7 @@ bool compressDirFiles(QString fileCompressed, QString dir, QFileInfoList files, return result; } +#if defined(LAUNCHER_APPLICATION) // ours bool createModdedJar(QString sourceJarPath, QString targetJarPath, const QList& mods) { @@ -217,6 +222,7 @@ bool createModdedJar(QString sourceJarPath, QString targetJarPath, const QList #include #include + +#if defined(LAUNCHER_APPLICATION) #include "minecraft/mod/Mod.h" +#endif #include "tasks/Task.h" namespace MMCZip { @@ -79,11 +82,12 @@ bool compressDirFiles(QuaZip* zip, QString dir, QFileInfoList files, bool follow */ bool compressDirFiles(QString fileCompressed, QString dir, QFileInfoList files, bool followSymlinks = false); +#if defined(LAUNCHER_APPLICATION) /** * take a source jar, add mods to it, resulting in target jar */ bool createModdedJar(QString sourceJarPath, QString targetJarPath, const QList& mods); - +#endif /** * Find a single file in archive by file name (not path) * @@ -147,6 +151,7 @@ bool extractFile(QString fileCompressed, QString file, QString dir); */ bool collectFileListRecursively(const QString& rootDir, const QString& subDir, QFileInfoList* files, FilterFunction excludeFilter); +#if defined(LAUNCHER_APPLICATION) class ExportToZipTask : public Task { public: ExportToZipTask(QString outputPath, QDir dir, QFileInfoList files, QString destinationPrefix = "", bool followSymlinks = false) @@ -189,4 +194,5 @@ class ExportToZipTask : public Task { QFuture m_build_zip_future; QFutureWatcher m_build_zip_watcher; }; +#endif } // namespace MMCZip diff --git a/launcher/StringUtils.cpp b/launcher/StringUtils.cpp index e08e6fdce..72ccdfbff 100644 --- a/launcher/StringUtils.cpp +++ b/launcher/StringUtils.cpp @@ -35,6 +35,7 @@ */ #include "StringUtils.h" +#include #include #include @@ -149,7 +150,7 @@ QString StringUtils::truncateUrlHumanFriendly(QUrl& url, int max_len, bool hard_ } if ((url_compact.length() >= max_len) && hard_limit) { - // still too long, truncate normaly + // still too long, truncate normally url_compact = QString(str_url); auto to_remove = url_compact.length() - max_len + 3; url_compact.remove(url_compact.length() - to_remove - 1, to_remove); @@ -182,3 +183,32 @@ QString StringUtils::getRandomAlphaNumeric() { return QUuid::createUuid().toString(QUuid::Id128); } + +QPair StringUtils::splitFirst(const QString& s, const QString& sep, Qt::CaseSensitivity cs) +{ + QString left, right; + auto index = s.indexOf(sep, 0, cs); + left = s.mid(0, index); + right = s.mid(index + sep.length()); + return qMakePair(left, right); +} + +QPair StringUtils::splitFirst(const QString& s, QChar sep, Qt::CaseSensitivity cs) +{ + QString left, right; + auto index = s.indexOf(sep, 0, cs); + left = s.mid(0, index); + right = s.mid(left.length() + 1); + return qMakePair(left, right); +} + +QPair StringUtils::splitFirst(const QString& s, const QRegularExpression& re) +{ + QString left, right; + QRegularExpressionMatch match; + auto index = s.indexOf(re, 0, &match); + left = s.mid(0, index); + auto end = match.hasMatch() ? left.length() + match.capturedLength() : left.length() + 1; + right = s.mid(end); + return qMakePair(left, right); +} diff --git a/launcher/StringUtils.h b/launcher/StringUtils.h index eac0d5a7d..9d2bdd85e 100644 --- a/launcher/StringUtils.h +++ b/launcher/StringUtils.h @@ -36,8 +36,10 @@ #pragma once +#include #include #include +#include namespace StringUtils { @@ -70,12 +72,17 @@ int naturalCompare(const QString& s1, const QString& s2, Qt::CaseSensitivity cs) /** * @brief Truncate a url while keeping its readability py placing the `...` in the middle of the path * @param url Url to truncate - * @param max_len max lenght of url in charaters - * @param hard_limit if truncating the path can't get the url short enough, truncate it normaly. + * @param max_len max length of url in characters + * @param hard_limit if truncating the path can't get the url short enough, truncate it normally. */ QString truncateUrlHumanFriendly(QUrl& url, int max_len, bool hard_limit = false); QString humanReadableFileSize(double bytes, bool use_si = false, int decimal_points = 1); QString getRandomAlphaNumeric(); + +QPair splitFirst(const QString& s, const QString& sep, Qt::CaseSensitivity cs = Qt::CaseSensitive); +QPair splitFirst(const QString& s, QChar sep, Qt::CaseSensitivity cs = Qt::CaseSensitive); +QPair splitFirst(const QString& s, const QRegularExpression& re); + } // namespace StringUtils diff --git a/launcher/Version.h b/launcher/Version.h index 9c043b013..b06e256aa 100644 --- a/launcher/Version.h +++ b/launcher/Version.h @@ -56,6 +56,7 @@ class Version { bool operator!=(const Version& other) const; QString toString() const { return m_string; } + bool isEmpty() const { return m_string.isEmpty(); } friend QDebug operator<<(QDebug debug, const Version& v); diff --git a/launcher/filelink/FileLink.cpp b/launcher/filelink/FileLink.cpp index 1ea0e382f..bdf173ebc 100644 --- a/launcher/filelink/FileLink.cpp +++ b/launcher/filelink/FileLink.cpp @@ -93,6 +93,7 @@ FileLinkApp::FileLinkApp(int& argc, char** argv) : QCoreApplication(argc, argv), joinServer(serverToJoin); } else { qDebug() << "no server to join"; + m_status = Failed; exit(); } } @@ -108,6 +109,7 @@ void FileLinkApp::joinServer(QString server) connect(&socket, &QLocalSocket::readyRead, this, &FileLinkApp::readPathPairs); connect(&socket, &QLocalSocket::errorOccurred, this, [&](QLocalSocket::LocalSocketError socketError) { + m_status = Failed; switch (socketError) { case QLocalSocket::ServerNotFoundError: qDebug() @@ -132,6 +134,7 @@ void FileLinkApp::joinServer(QString server) connect(&socket, &QLocalSocket::disconnected, this, [&]() { qDebug() << "disconnected from server, should exit"; + m_status = Succeeded; exit(); }); diff --git a/launcher/filelink/FileLink.h b/launcher/filelink/FileLink.h index 4c47d9bbb..583d0d43a 100644 --- a/launcher/filelink/FileLink.h +++ b/launcher/filelink/FileLink.h @@ -41,8 +41,10 @@ class FileLinkApp : public QCoreApplication { // friends for the purpose of limiting access to deprecated stuff Q_OBJECT public: + enum Status { Starting, Failed, Succeeded, Initialized }; FileLinkApp(int& argc, char** argv); virtual ~FileLinkApp(); + Status status() const { return m_status; } private: void joinServer(QString server); @@ -50,6 +52,8 @@ class FileLinkApp : public QCoreApplication { void runLink(); void sendResults(); + Status m_status = Status::Starting; + bool m_useHardLinks = false; QDateTime m_startTime; diff --git a/launcher/filelink/main.cpp b/launcher/filelink/filelink_main.cpp similarity index 75% rename from launcher/filelink/main.cpp rename to launcher/filelink/filelink_main.cpp index 83566a3c6..2a8bcb703 100644 --- a/launcher/filelink/main.cpp +++ b/launcher/filelink/filelink_main.cpp @@ -26,5 +26,16 @@ int main(int argc, char* argv[]) { FileLinkApp ldh(argc, argv); - return ldh.exec(); + switch (ldh.status()) { + case FileLinkApp::Starting: + case FileLinkApp::Initialized: { + return ldh.exec(); + } + case FileLinkApp::Failed: + return 1; + case FileLinkApp::Succeeded: + return 0; + default: + return -1; + } } diff --git a/launcher/java/JavaInstall.h b/launcher/java/JavaInstall.h index 30815b5a8..49e9ed06e 100644 --- a/launcher/java/JavaInstall.h +++ b/launcher/java/JavaInstall.h @@ -24,11 +24,11 @@ struct JavaInstall : public BaseVersion { JavaInstall() {} JavaInstall(QString id, QString arch, QString path) : id(id), arch(arch), path(path) {} - virtual QString descriptor() { return id.toString(); } + virtual QString descriptor() override { return id.toString(); } - virtual QString name() { return id.toString(); } + virtual QString name() override { return id.toString(); } - virtual QString typeString() const { return arch; } + virtual QString typeString() const override { return arch; } virtual bool operator<(BaseVersion& a) override; virtual bool operator>(BaseVersion& a) override; diff --git a/launcher/java/JavaUtils.cpp b/launcher/java/JavaUtils.cpp index 3512c3079..cca1ed6d4 100644 --- a/launcher/java/JavaUtils.cpp +++ b/launcher/java/JavaUtils.cpp @@ -403,6 +403,14 @@ QList JavaUtils::FindJavaPaths() scanJavaDirs("/opt/jdks"); // flatpak scanJavaDirs("/app/jdk"); + + auto home = qEnvironmentVariable("HOME"); + + // javas downloaded by IntelliJ + scanJavaDirs(FS::PathCombine(home, ".jdks")); + // javas downloaded by sdkman + scanJavaDirs(FS::PathCombine(home, ".sdkman/candidates/java")); + javas = addJavasFromEnv(javas); javas.removeDuplicates(); return javas; diff --git a/launcher/java/JavaVersion.cpp b/launcher/java/JavaVersion.cpp index f9ac47824..b77bf2adf 100644 --- a/launcher/java/JavaVersion.cpp +++ b/launcher/java/JavaVersion.cpp @@ -45,10 +45,12 @@ QString JavaVersion::toString() const bool JavaVersion::requiresPermGen() { - if (m_parseable) { - return m_major < 8; - } - return true; + return !m_parseable || m_major < 8; +} + +bool JavaVersion::isModular() +{ + return m_parseable && m_major >= 9; } bool JavaVersion::operator<(const JavaVersion& rhs) diff --git a/launcher/java/JavaVersion.h b/launcher/java/JavaVersion.h index 7e66269cb..421578ea1 100644 --- a/launcher/java/JavaVersion.h +++ b/launcher/java/JavaVersion.h @@ -25,6 +25,8 @@ class JavaVersion { bool requiresPermGen(); + bool isModular(); + QString toString() const; int major() { return m_major; } diff --git a/launcher/minecraft/MinecraftInstance.cpp b/launcher/minecraft/MinecraftInstance.cpp index 86ef3b30e..55fd864bb 100644 --- a/launcher/minecraft/MinecraftInstance.cpp +++ b/launcher/minecraft/MinecraftInstance.cpp @@ -184,6 +184,10 @@ void MinecraftInstance::loadSpecificSettings() m_settings->registerOverride(global_settings->getSetting("CloseAfterLaunch"), miscellaneousOverride); m_settings->registerOverride(global_settings->getSetting("QuitAfterGameStop"), miscellaneousOverride); + // Legacy-related options + auto legacySettings = m_settings->registerSetting("OverrideLegacySettings", false); + m_settings->registerOverride(global_settings->getSetting("OnlineFixes"), legacySettings); + m_settings->set("InstanceType", "OneSix"); } @@ -513,20 +517,28 @@ QStringList MinecraftInstance::javaArguments() args << "-Duser.language=en"; + if (javaVersion.isModular() && shouldApplyOnlineFixes()) + // allow reflective access to java.net - required by the skin fix + args << "--add-opens" + << "java.base/java.net=ALL-UNNAMED"; + return args; } QString MinecraftInstance::getLauncher() { - auto profile = m_components->getProfile(); - // use legacy launcher if the traits are set - if (profile->getTraits().contains("legacyLaunch") || profile->getTraits().contains("alphaLaunch")) + if (traits().contains("legacyLaunch") || traits().contains("alphaLaunch")) return "legacy"; return "standard"; } +bool MinecraftInstance::shouldApplyOnlineFixes() +{ + return traits().contains("legacyServices") && settings()->get("OnlineFixes").toBool(); +} + QMap MinecraftInstance::getVariables() { QMap out; @@ -716,6 +728,9 @@ QString MinecraftInstance::createLaunchScript(AuthSessionPtr session, MinecraftS launchScript += "traits " + trait + "\n"; } + if (shouldApplyOnlineFixes()) + launchScript += "onlineFixes true\n"; + launchScript += "launcher " + getLauncher() + "\n"; // qDebug() << "Generated launch script:" << launchScript; @@ -856,9 +871,6 @@ QMap MinecraftInstance::createCensorFilterFromSession(AuthSess if (sessionRef.access_token != "0") { addToFilter(sessionRef.access_token, tr("")); } - if (sessionRef.client_token.size()) { - addToFilter(sessionRef.client_token, tr("")); - } addToFilter(sessionRef.uuid, tr("")); return filter; diff --git a/launcher/minecraft/MinecraftInstance.h b/launcher/minecraft/MinecraftInstance.h index dabd44ba7..a816a3e60 100644 --- a/launcher/minecraft/MinecraftInstance.h +++ b/launcher/minecraft/MinecraftInstance.h @@ -129,6 +129,7 @@ class MinecraftInstance : public BaseInstance { /// get arguments passed to java QStringList javaArguments(); QString getLauncher(); + bool shouldApplyOnlineFixes(); /// get variables for launch command variable substitution/environment QMap getVariables() override; diff --git a/launcher/minecraft/PackProfile.cpp b/launcher/minecraft/PackProfile.cpp index 9e42c5dd6..9e706ae0a 100644 --- a/launcher/minecraft/PackProfile.cpp +++ b/launcher/minecraft/PackProfile.cpp @@ -1018,8 +1018,7 @@ std::optional PackProfile::getSupportedModLoaders() // 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) + if (getComponentVersion("net.minecraft") == "1.20.1" && (loaders & ModPlatform::NeoForge)) loaders |= ModPlatform::Forge; return loaders; } diff --git a/launcher/minecraft/auth/AccountData.cpp b/launcher/minecraft/auth/AccountData.cpp index 474bf7c6e..e1f1e9b1e 100644 --- a/launcher/minecraft/auth/AccountData.cpp +++ b/launcher/minecraft/auth/AccountData.cpp @@ -278,67 +278,6 @@ bool entitlementFromJSONV3(const QJsonObject& parent, MinecraftEntitlement& out) } // namespace -bool AccountData::resumeStateFromV2(QJsonObject data) -{ - // The JSON object must at least have a username for it to be valid. - if (!data.value("username").isString()) { - qCritical() << "Can't load Mojang account info from JSON object. Username field is missing or of the wrong type."; - return false; - } - - QString userName = data.value("username").toString(""); - QString clientToken = data.value("clientToken").toString(""); - QString accessToken = data.value("accessToken").toString(""); - - QJsonArray profileArray = data.value("profiles").toArray(); - if (profileArray.size() < 1) { - qCritical() << "Can't load Mojang account with username \"" << userName << "\". No profiles found."; - return false; - } - - struct AccountProfile { - QString id; - QString name; - bool legacy; - }; - - QList profiles; - int currentProfileIndex = 0; - int index = -1; - QString currentProfile = data.value("activeProfile").toString(""); - for (QJsonValue profileVal : profileArray) { - index++; - QJsonObject profileObject = profileVal.toObject(); - QString id = profileObject.value("id").toString(""); - QString name = profileObject.value("name").toString(""); - 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; - } - if (id == currentProfile) { - currentProfileIndex = index; - } - profiles.append({ id, name, legacy_ }); - } - auto& profile = profiles[currentProfileIndex]; - - type = AccountType::Mojang; - legacy = profile.legacy; - - minecraftProfile.id = profile.id; - minecraftProfile.name = profile.name; - minecraftProfile.validity = Katabasis::Validity::Assumed; - - yggdrasilToken.token = accessToken; - yggdrasilToken.extra["clientToken"] = clientToken; - yggdrasilToken.extra["userName"] = userName; - yggdrasilToken.validity = Katabasis::Validity::Assumed; - - validity_ = minecraftProfile.validity; - return true; -} - bool AccountData::resumeStateFromV3(QJsonObject data) { auto typeV = data.value("type"); @@ -349,8 +288,6 @@ bool AccountData::resumeStateFromV3(QJsonObject data) auto typeS = typeV.toString(); if (typeS == "MSA") { type = AccountType::MSA; - } else if (typeS == "Mojang") { - type = AccountType::Mojang; } else if (typeS == "Offline") { type = AccountType::Offline; } else { @@ -358,11 +295,6 @@ bool AccountData::resumeStateFromV3(QJsonObject data) return false; } - if (type == AccountType::Mojang) { - legacy = data.value("legacy").toBool(false); - canMigrateToMSA = data.value("canMigrateToMSA").toBool(false); - } - if (type == AccountType::MSA) { auto clientIDV = data.value("msa-client-id"); if (clientIDV.isString()) { @@ -395,15 +327,7 @@ bool AccountData::resumeStateFromV3(QJsonObject data) QJsonObject AccountData::saveState() const { QJsonObject output; - if (type == AccountType::Mojang) { - output["type"] = "Mojang"; - if (legacy) { - output["legacy"] = true; - } - if (canMigrateToMSA) { - output["canMigrateToMSA"] = true; - } - } else if (type == AccountType::MSA) { + if (type == AccountType::MSA) { output["type"] = "MSA"; output["msa-client-id"] = msaClientID; tokenToJSONV3(output, msaToken, "msa"); @@ -420,51 +344,11 @@ QJsonObject AccountData::saveState() const return output; } -QString AccountData::userName() const -{ - if (type == AccountType::MSA) { - return QString(); - } - return yggdrasilToken.extra["userName"].toString(); -} - QString AccountData::accessToken() const { return yggdrasilToken.token; } -QString AccountData::clientToken() const -{ - if (type != AccountType::Mojang) { - return QString(); - } - return yggdrasilToken.extra["clientToken"].toString(); -} - -void AccountData::setClientToken(QString clientToken) -{ - if (type != AccountType::Mojang) { - return; - } - yggdrasilToken.extra["clientToken"] = clientToken; -} - -void AccountData::generateClientTokenIfMissing() -{ - if (yggdrasilToken.extra.contains("clientToken")) { - return; - } - invalidateClientToken(); -} - -void AccountData::invalidateClientToken() -{ - if (type != AccountType::Mojang) { - return; - } - yggdrasilToken.extra["clientToken"] = QUuid::createUuid().toString().remove(QRegularExpression("[{-}]")); -} - QString AccountData::profileId() const { return minecraftProfile.id; @@ -482,9 +366,6 @@ QString AccountData::profileName() const QString AccountData::accountDisplayString() const { switch (type) { - case AccountType::Mojang: { - return userName(); - } case AccountType::Offline: { return QObject::tr(""); } diff --git a/launcher/minecraft/auth/AccountData.h b/launcher/minecraft/auth/AccountData.h index 9b626c34e..bac77e17f 100644 --- a/launcher/minecraft/auth/AccountData.h +++ b/launcher/minecraft/auth/AccountData.h @@ -71,27 +71,17 @@ struct MinecraftProfile { Katabasis::Validity validity = Katabasis::Validity::None; }; -enum class AccountType { MSA, Mojang, Offline }; +enum class AccountType { MSA, Offline }; enum class AccountState { Unchecked, Offline, Working, Online, Disabled, Errored, Expired, Gone }; struct AccountData { QJsonObject saveState() const; - bool resumeStateFromV2(QJsonObject data); bool resumeStateFromV3(QJsonObject data); //! userName for Mojang accounts, gamertag for MSA QString accountDisplayString() const; - //! Only valid for Mojang accounts. MSA does not preserve this information - QString userName() const; - - //! Only valid for Mojang accounts. - QString clientToken() const; - void setClientToken(QString clientToken); - void invalidateClientToken(); - void generateClientTokenIfMissing(); - //! Yggdrasil access token, as passed to the game. QString accessToken() const; @@ -101,8 +91,6 @@ struct AccountData { QString lastError() const; AccountType type = AccountType::MSA; - bool legacy = false; - bool canMigrateToMSA = false; QString msaClientID; Katabasis::Token msaToken; diff --git a/launcher/minecraft/auth/AccountList.cpp b/launcher/minecraft/auth/AccountList.cpp index 84dbd8416..b141909a9 100644 --- a/launcher/minecraft/auth/AccountList.cpp +++ b/launcher/minecraft/auth/AccountList.cpp @@ -54,7 +54,7 @@ #include -enum AccountListVersion { MojangOnly = 2, MojangMSA = 3 }; +enum AccountListVersion { MojangMSA = 3 }; AccountList::AccountList(QObject* parent) : QAbstractListModel(parent) { @@ -320,17 +320,6 @@ QVariant AccountList::data(const QModelIndex& index, int role) const } } - case MigrationColumn: { - if (account->isMSA() || account->isOffline()) { - return tr("N/A", "Can Migrate"); - } - if (account->canMigrate()) { - return tr("Yes", "Can Migrate"); - } else { - return tr("No", "Can Migrate"); - } - } - default: return QVariant(); } @@ -366,8 +355,6 @@ QVariant AccountList::headerData(int section, [[maybe_unused]] Qt::Orientation o return tr("Type"); case StatusColumn: return tr("Status"); - case MigrationColumn: - return tr("Can Migrate?"); default: return QVariant(); } @@ -379,11 +366,9 @@ QVariant AccountList::headerData(int section, [[maybe_unused]] Qt::Orientation o case NameColumn: return tr("User name of the account."); case TypeColumn: - return tr("Type of the account - Mojang or MSA."); + return tr("Type of the account (MSA or Offline)"); case StatusColumn: return tr("Current status of the account."); - case MigrationColumn: - return tr("Can this account migrate to a Microsoft account?"); default: return QVariant(); } @@ -473,9 +458,6 @@ bool AccountList::loadList() // Make sure the format version matches. auto listVersion = root.value("formatVersion").toVariant().toInt(); switch (listVersion) { - case AccountListVersion::MojangOnly: { - return loadV2(root); - } break; case AccountListVersion::MojangMSA: { return loadV3(root); } break; @@ -489,36 +471,6 @@ bool AccountList::loadList() } } -bool AccountList::loadV2(QJsonObject& root) -{ - beginResetModel(); - auto defaultUserName = root.value("activeAccount").toString(""); - QJsonArray accounts = root.value("accounts").toArray(); - for (QJsonValue accountVal : accounts) { - QJsonObject accountObj = accountVal.toObject(); - MinecraftAccountPtr account = MinecraftAccount::loadFromJsonV2(accountObj); - if (account.get() != nullptr) { - auto profileId = account->profileId(); - if (!profileId.size()) { - continue; - } - if (findAccountByProfileId(profileId) != -1) { - continue; - } - connect(account.get(), &MinecraftAccount::changed, this, &AccountList::accountChanged); - connect(account.get(), &MinecraftAccount::activityChanged, this, &AccountList::accountActivityChanged); - m_accounts.append(account); - if (defaultUserName.size() && account->mojangUserName() == defaultUserName) { - m_defaultAccount = account; - } - } else { - qWarning() << "Failed to load an account."; - } - } - endResetModel(); - return true; -} - bool AccountList::loadV3(QJsonObject& root) { beginResetModel(); diff --git a/launcher/minecraft/auth/AccountList.h b/launcher/minecraft/auth/AccountList.h index 6a0b01916..051d8f958 100644 --- a/launcher/minecraft/auth/AccountList.h +++ b/launcher/minecraft/auth/AccountList.h @@ -55,7 +55,6 @@ class AccountList : public QAbstractListModel { // TODO: Add icon column. ProfileNameColumn = 0, NameColumn, - MigrationColumn, TypeColumn, StatusColumn, @@ -97,7 +96,6 @@ class AccountList : public QAbstractListModel { void setListFilePath(QString path, bool autosave = false); bool loadList(); - bool loadV2(QJsonObject& root); bool loadV3(QJsonObject& root); bool saveList(); diff --git a/launcher/minecraft/auth/AuthSession.h b/launcher/minecraft/auth/AuthSession.h index 40519476d..074b2d6e3 100644 --- a/launcher/minecraft/auth/AuthSession.h +++ b/launcher/minecraft/auth/AuthSession.h @@ -24,10 +24,6 @@ struct AuthSession { GoneOrMigrated } status = Undetermined; - // client token - QString client_token; - // account user name - QString username; // combined session ID QString session; // volatile auth token diff --git a/launcher/minecraft/auth/MinecraftAccount.cpp b/launcher/minecraft/auth/MinecraftAccount.cpp index 6c2f0805f..545d06aed 100644 --- a/launcher/minecraft/auth/MinecraftAccount.cpp +++ b/launcher/minecraft/auth/MinecraftAccount.cpp @@ -51,7 +51,6 @@ #include #include "flows/MSA.h" -#include "flows/Mojang.h" #include "flows/Offline.h" MinecraftAccount::MinecraftAccount(QObject* parent) : QObject(parent) @@ -59,15 +58,6 @@ MinecraftAccount::MinecraftAccount(QObject* parent) : QObject(parent) data.internalId = QUuid::createUuid().toString().remove(QRegularExpression("[{}-]")); } -MinecraftAccountPtr MinecraftAccount::loadFromJsonV2(const QJsonObject& json) -{ - MinecraftAccountPtr account(new MinecraftAccount()); - if (account->data.resumeStateFromV2(json)) { - return account; - } - return nullptr; -} - MinecraftAccountPtr MinecraftAccount::loadFromJsonV3(const QJsonObject& json) { MinecraftAccountPtr account(new MinecraftAccount()); @@ -77,15 +67,6 @@ MinecraftAccountPtr MinecraftAccount::loadFromJsonV3(const QJsonObject& json) return nullptr; } -MinecraftAccountPtr MinecraftAccount::createFromUsername(const QString& username) -{ - auto account = makeShared(); - account->data.type = AccountType::Mojang; - account->data.yggdrasilToken.extra["userName"] = username; - account->data.yggdrasilToken.extra["clientToken"] = QUuid::createUuid().toString().remove(QRegularExpression("[{}-]")); - return account; -} - MinecraftAccountPtr MinecraftAccount::createBlankMSA() { MinecraftAccountPtr account(new MinecraftAccount()); @@ -138,18 +119,6 @@ QPixmap MinecraftAccount::getFace() const return skin.scaled(64, 64, Qt::KeepAspectRatio); } -shared_qobject_ptr MinecraftAccount::login(QString password) -{ - Q_ASSERT(m_currentTask.get() == nullptr); - - m_currentTask.reset(new MojangLogin(&data, password)); - connect(m_currentTask.get(), &Task::succeeded, this, &MinecraftAccount::authSucceeded); - connect(m_currentTask.get(), &Task::failed, this, &MinecraftAccount::authFailed); - connect(m_currentTask.get(), &Task::aborted, this, [this] { authFailed(tr("Aborted")); }); - emit activityChanged(true); - return m_currentTask; -} - shared_qobject_ptr MinecraftAccount::loginMSA() { Q_ASSERT(m_currentTask.get() == nullptr); @@ -182,10 +151,8 @@ shared_qobject_ptr MinecraftAccount::refresh() if (data.type == AccountType::MSA) { m_currentTask.reset(new MSASilent(&data)); - } else if (data.type == AccountType::Offline) { - m_currentTask.reset(new OfflineRefresh(&data)); } else { - m_currentTask.reset(new MojangRefresh(&data)); + m_currentTask.reset(new OfflineRefresh(&data)); } connect(m_currentTask.get(), &Task::succeeded, this, &MinecraftAccount::authSucceeded); @@ -296,13 +263,8 @@ void MinecraftAccount::fillSession(AuthSessionPtr session) } } - // the user name. you have to have an user name - // FIXME: not with MSA - session->username = data.userName(); // volatile auth token session->access_token = data.accessToken(); - // the semi-permanent client token - session->client_token = data.clientToken(); // profile name session->player_name = data.profileName(); // profile ID diff --git a/launcher/minecraft/auth/MinecraftAccount.h b/launcher/minecraft/auth/MinecraftAccount.h index f04f947fa..910f4a28a 100644 --- a/launcher/minecraft/auth/MinecraftAccount.h +++ b/launcher/minecraft/auth/MinecraftAccount.h @@ -85,13 +85,10 @@ class MinecraftAccount : public QObject, public Usable { //! Default constructor explicit MinecraftAccount(QObject* parent = 0); - static MinecraftAccountPtr createFromUsername(const QString& username); - static MinecraftAccountPtr createBlankMSA(); static MinecraftAccountPtr createOffline(const QString& username); - static MinecraftAccountPtr loadFromJsonV2(const QJsonObject& json); static MinecraftAccountPtr loadFromJsonV3(const QJsonObject& json); static QUuid uuidFromUsername(QString username); @@ -100,12 +97,6 @@ class MinecraftAccount : public QObject, public Usable { QJsonObject saveToJson() const; public: /* manipulation */ - /** - * Attempt to login. Empty password means we use the token. - * If the attempt fails because we already are performing some task, it returns false. - */ - shared_qobject_ptr login(QString password); - shared_qobject_ptr loginMSA(); shared_qobject_ptr loginOffline(); @@ -119,8 +110,6 @@ class MinecraftAccount : public QObject, public Usable { QString accountDisplayString() const { return data.accountDisplayString(); } - QString mojangUserName() const { return data.userName(); } - QString accessToken() const { return data.accessToken(); } QString profileId() const { return data.profileId(); } @@ -129,8 +118,6 @@ class MinecraftAccount : public QObject, public Usable { bool isActive() const; - bool canMigrate() const { return data.canMigrateToMSA; } - bool isMSA() const { return data.type == AccountType::MSA; } bool isOffline() const { return data.type == AccountType::Offline; } @@ -142,12 +129,6 @@ class MinecraftAccount : public QObject, public Usable { QString typeString() const { switch (data.type) { - case AccountType::Mojang: { - if (data.legacy) { - return "legacy"; - } - return "mojang"; - } break; case AccountType::MSA: { return "msa"; } break; diff --git a/launcher/minecraft/auth/Yggdrasil.cpp b/launcher/minecraft/auth/Yggdrasil.cpp deleted file mode 100644 index 97f2a78d4..000000000 --- a/launcher/minecraft/auth/Yggdrasil.cpp +++ /dev/null @@ -1,342 +0,0 @@ -/* Copyright 2013-2021 MultiMC Contributors - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -#include "Yggdrasil.h" -#include "AccountData.h" - -#include -#include -#include -#include -#include -#include - -#include - -#include "Application.h" - -Yggdrasil::Yggdrasil(AccountData* data, QObject* parent) : AccountTask(data, parent) -{ - changeState(AccountTaskState::STATE_CREATED); -} - -void Yggdrasil::sendRequest(QUrl endpoint, QByteArray content) -{ - changeState(AccountTaskState::STATE_WORKING); - - QNetworkRequest netRequest(endpoint); - netRequest.setHeader(QNetworkRequest::ContentTypeHeader, "application/json"); - m_netReply = APPLICATION->network()->post(netRequest, content); - connect(m_netReply, &QNetworkReply::finished, this, &Yggdrasil::processReply); - connect(m_netReply, &QNetworkReply::uploadProgress, this, &Yggdrasil::refreshTimers); - connect(m_netReply, &QNetworkReply::downloadProgress, this, &Yggdrasil::refreshTimers); - connect(m_netReply, &QNetworkReply::sslErrors, this, &Yggdrasil::sslErrors); - timeout_keeper.setSingleShot(true); - timeout_keeper.start(timeout_max); - counter.setSingleShot(false); - counter.start(time_step); - progress(0, timeout_max); - connect(&timeout_keeper, &QTimer::timeout, this, &Yggdrasil::abortByTimeout); - connect(&counter, &QTimer::timeout, this, &Yggdrasil::heartbeat); -} - -void Yggdrasil::executeTask() {} - -void Yggdrasil::refresh() -{ - start(); - /* - * { - * "clientToken": "client identifier" - * "accessToken": "current access token to be refreshed" - * "selectedProfile": // specifying this causes errors - * { - * "id": "profile ID" - * "name": "profile name" - * } - * "requestUser": true/false // request the user structure - * } - */ - QJsonObject req; - req.insert("clientToken", m_data->clientToken()); - req.insert("accessToken", m_data->accessToken()); - /* - { - auto currentProfile = m_account->currentProfile(); - QJsonObject profile; - profile.insert("id", currentProfile->id()); - profile.insert("name", currentProfile->name()); - req.insert("selectedProfile", profile); - } - */ - req.insert("requestUser", false); - QJsonDocument doc(req); - - QUrl reqUrl("https://authserver.mojang.com/refresh"); - QByteArray requestData = doc.toJson(); - - sendRequest(reqUrl, requestData); -} - -void Yggdrasil::login(QString password) -{ - start(); - /* - * { - * "agent": { // optional - * "name": "Minecraft", // So far this is the only encountered value - * "version": 1 // This number might be increased - * // by the vanilla client in the future - * }, - * "username": "mojang account name", // Can be an email address or player name for - * // unmigrated accounts - * "password": "mojang account password", - * "clientToken": "client identifier", // optional - * "requestUser": true/false // request the user structure - * } - */ - QJsonObject req; - - { - QJsonObject agent; - // C++ makes string literals void* for some stupid reason, so we have to tell it - // QString... Thanks Obama. - agent.insert("name", QString("Minecraft")); - agent.insert("version", 1); - req.insert("agent", agent); - } - - req.insert("username", m_data->userName()); - req.insert("password", password); - req.insert("requestUser", false); - - // If we already have a client token, give it to the server. - // Otherwise, let the server give us one. - - m_data->generateClientTokenIfMissing(); - req.insert("clientToken", m_data->clientToken()); - - QJsonDocument doc(req); - - QUrl reqUrl("https://authserver.mojang.com/authenticate"); - QNetworkRequest netRequest(reqUrl); - QByteArray requestData = doc.toJson(); - - sendRequest(reqUrl, requestData); -} - -void Yggdrasil::refreshTimers(qint64, qint64) -{ - timeout_keeper.stop(); - timeout_keeper.start(timeout_max); - progress(count = 0, timeout_max); -} - -void Yggdrasil::heartbeat() -{ - count += time_step; - progress(count, timeout_max); -} - -bool Yggdrasil::abort() -{ - progress(timeout_max, timeout_max); - // TODO: actually use this in a meaningful way - m_aborted = Yggdrasil::BY_USER; - m_netReply->abort(); - return true; -} - -void Yggdrasil::abortByTimeout() -{ - progress(timeout_max, timeout_max); - // TODO: actually use this in a meaningful way - m_aborted = Yggdrasil::BY_TIMEOUT; - m_netReply->abort(); -} - -void Yggdrasil::sslErrors(QList errors) -{ - int i = 1; - for (auto error : errors) { - qCritical() << "LOGIN SSL Error #" << i << " : " << error.errorString(); - auto cert = error.certificate(); - qCritical() << "Certificate in question:\n" << cert.toText(); - i++; - } -} - -void Yggdrasil::processResponse(QJsonObject responseData) -{ - // Read the response data. We need to get the client token, access token, and the selected - // profile. - qDebug() << "Processing authentication response."; - - // qDebug() << responseData; - // If we already have a client token, make sure the one the server gave us matches our - // existing one. - QString clientToken = responseData.value("clientToken").toString(""); - if (clientToken.isEmpty()) { - // Fail if the server gave us an empty client token - changeState(AccountTaskState::STATE_FAILED_HARD, tr("Authentication server didn't send a client token.")); - return; - } - if (m_data->clientToken().isEmpty()) { - m_data->setClientToken(clientToken); - } else if (clientToken != m_data->clientToken()) { - changeState(AccountTaskState::STATE_FAILED_HARD, - tr("Authentication server attempted to change the client token. This isn't supported.")); - return; - } - - // Now, we set the access token. - qDebug() << "Getting access token."; - QString accessToken = responseData.value("accessToken").toString(""); - if (accessToken.isEmpty()) { - // Fail if the server didn't give us an access token. - changeState(AccountTaskState::STATE_FAILED_HARD, tr("Authentication server didn't send an access token.")); - return; - } - // Set the access token. - m_data->yggdrasilToken.token = accessToken; - m_data->yggdrasilToken.validity = Katabasis::Validity::Certain; - m_data->yggdrasilToken.issueInstant = QDateTime::currentDateTimeUtc(); - - // Get UUID here since we need it for later - auto profile = responseData.value("selectedProfile"); - if (!profile.isObject()) { - changeState(AccountTaskState::STATE_FAILED_HARD, tr("Authentication server didn't send a selected profile.")); - return; - } - - auto profileObj = profile.toObject(); - for (auto i = profileObj.constBegin(); i != profileObj.constEnd(); ++i) { - if (i.key() == "name" && i.value().isString()) { - m_data->minecraftProfile.name = i->toString(); - } else if (i.key() == "id" && i.value().isString()) { - m_data->minecraftProfile.id = i->toString(); - } - } - - if (m_data->minecraftProfile.id.isEmpty()) { - changeState(AccountTaskState::STATE_FAILED_HARD, tr("Authentication server didn't send a UUID in selected profile.")); - return; - } - - // We've made it through the minefield of possible errors. Return true to indicate that - // we've succeeded. - qDebug() << "Finished reading authentication response."; - changeState(AccountTaskState::STATE_SUCCEEDED); -} - -void Yggdrasil::processReply() -{ - changeState(AccountTaskState::STATE_WORKING); - - switch (m_netReply->error()) { - case QNetworkReply::NoError: - break; - case QNetworkReply::TimeoutError: - changeState(AccountTaskState::STATE_FAILED_SOFT, tr("Authentication operation timed out.")); - return; - case QNetworkReply::OperationCanceledError: - changeState(AccountTaskState::STATE_FAILED_SOFT, tr("Authentication operation cancelled.")); - return; - case QNetworkReply::SslHandshakeFailedError: - changeState(AccountTaskState::STATE_FAILED_SOFT, - tr("SSL Handshake failed.
There might be a few causes for it:
" - "
    " - "
  • You use Windows and need to update your root certificates, please install any outstanding updates.
  • " - "
  • Some device on your network is interfering with SSL traffic. In that case, " - "you have bigger worries than Minecraft not starting.
  • " - "
  • Possibly something else. Check the log file for details
  • " - "
")); - return; - // used for invalid credentials and similar errors. Fall through. - case QNetworkReply::ContentAccessDenied: - case QNetworkReply::ContentOperationNotPermittedError: - break; - case QNetworkReply::ContentGoneError: { - changeState(AccountTaskState::STATE_FAILED_GONE, - tr("The Mojang account no longer exists. It may have been migrated to a Microsoft account.")); - return; - } - default: - changeState(AccountTaskState::STATE_FAILED_SOFT, tr("Authentication operation failed due to a network error: %1 (%2)") - .arg(m_netReply->errorString()) - .arg(m_netReply->error())); - return; - } - - // Try to parse the response regardless of the response code. - // Sometimes the auth server will give more information and an error code. - QJsonParseError jsonError; - QByteArray replyData = m_netReply->readAll(); - QJsonDocument doc = QJsonDocument::fromJson(replyData, &jsonError); - // Check the response code. - int responseCode = m_netReply->attribute(QNetworkRequest::HttpStatusCodeAttribute).toInt(); - - if (responseCode == 200) { - // If the response code was 200, then there shouldn't be an error. Make sure - // anyways. - // Also, sometimes an empty reply indicates success. If there was no data received, - // pass an empty json object to the processResponse function. - if (jsonError.error == QJsonParseError::NoError || replyData.size() == 0) { - processResponse(replyData.size() > 0 ? doc.object() : QJsonObject()); - return; - } else { - changeState(AccountTaskState::STATE_FAILED_SOFT, - tr("Failed to parse authentication server response JSON response: %1 at offset %2.") - .arg(jsonError.errorString()) - .arg(jsonError.offset)); - qCritical() << replyData; - } - return; - } - - // If the response code was not 200, then Yggdrasil may have given us information - // about the error. - // If we can parse the response, then get information from it. Otherwise just say - // there was an unknown error. - if (jsonError.error == QJsonParseError::NoError) { - // We were able to parse the server's response. Woo! - // Call processError. If a subclass has overridden it then they'll handle their - // stuff there. - qDebug() << "The request failed, but the server gave us an error message. Processing error."; - processError(doc.object()); - } else { - // The server didn't say anything regarding the error. Give the user an unknown - // error. - qDebug() << "The request failed and the server gave no error message. Unknown error."; - changeState( - AccountTaskState::STATE_FAILED_SOFT, - tr("An unknown error occurred when trying to communicate with the authentication server: %1").arg(m_netReply->errorString())); - } -} - -void Yggdrasil::processError(QJsonObject responseData) -{ - QJsonValue errorVal = responseData.value("error"); - QJsonValue errorMessageValue = responseData.value("errorMessage"); - QJsonValue causeVal = responseData.value("cause"); - - if (errorVal.isString() && errorMessageValue.isString()) { - m_error = std::shared_ptr(new Error{ errorVal.toString(""), errorMessageValue.toString(""), causeVal.toString("") }); - changeState(AccountTaskState::STATE_FAILED_HARD, m_error->m_errorMessageVerbose); - } else { - // Error is not in standard format. Don't set m_error and return unknown error. - changeState(AccountTaskState::STATE_FAILED_HARD, tr("An unknown Yggdrasil error occurred.")); - } -} diff --git a/launcher/minecraft/auth/Yggdrasil.h b/launcher/minecraft/auth/Yggdrasil.h deleted file mode 100644 index 560d7fb81..000000000 --- a/launcher/minecraft/auth/Yggdrasil.h +++ /dev/null @@ -1,92 +0,0 @@ -/* Copyright 2013-2021 MultiMC Contributors - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -#pragma once - -#include "AccountTask.h" - -#include -#include -#include -#include - -#include "MinecraftAccount.h" - -class QNetworkAccessManager; -class QNetworkReply; - -/** - * A Yggdrasil task is a task that performs an operation on a given mojang account. - */ -class Yggdrasil : public AccountTask { - Q_OBJECT - public: - explicit Yggdrasil(AccountData* data, QObject* parent = 0); - virtual ~Yggdrasil() = default; - - void refresh(); - void login(QString password); - - struct Error { - QString m_errorMessageShort; - QString m_errorMessageVerbose; - QString m_cause; - }; - std::shared_ptr m_error; - - enum AbortedBy { BY_NOTHING, BY_USER, BY_TIMEOUT } m_aborted = BY_NOTHING; - - protected: - void executeTask() override; - - /** - * Processes the response received from the server. - * If an error occurred, this should emit a failed signal. - * If Yggdrasil gave an error response, it should call setError() first, and then return false. - * Otherwise, it should return true. - * Note: If the response from the server was blank, and the HTTP code was 200, this function is called with - * an empty QJsonObject. - */ - void processResponse(QJsonObject responseData); - - /** - * Processes an error response received from the server. - * The default implementation will read data from Yggdrasil's standard error response format and set it as this task's Error. - * \returns a QString error message that will be passed to emitFailed. - */ - virtual void processError(QJsonObject responseData); - - protected slots: - void processReply(); - void refreshTimers(qint64, qint64); - void heartbeat(); - void sslErrors(QList); - void abortByTimeout(); - - public slots: - virtual bool abort() override; - - private: - void sendRequest(QUrl endpoint, QByteArray content); - - protected: - QNetworkReply* m_netReply = nullptr; - QTimer timeout_keeper; - QTimer counter; - int count = 0; // num msec since time reset - - const int timeout_max = 30000; - const int time_step = 50; -}; diff --git a/launcher/minecraft/auth/flows/AuthFlow.h b/launcher/minecraft/auth/flows/AuthFlow.h index c2c412abc..e39e926dd 100644 --- a/launcher/minecraft/auth/flows/AuthFlow.h +++ b/launcher/minecraft/auth/flows/AuthFlow.h @@ -12,7 +12,6 @@ #include "minecraft/auth/AccountData.h" #include "minecraft/auth/AccountTask.h" #include "minecraft/auth/AuthStep.h" -#include "minecraft/auth/Yggdrasil.h" class AuthFlow : public AccountTask { Q_OBJECT diff --git a/launcher/minecraft/auth/flows/Mojang.cpp b/launcher/minecraft/auth/flows/Mojang.cpp deleted file mode 100644 index 7e2db16fa..000000000 --- a/launcher/minecraft/auth/flows/Mojang.cpp +++ /dev/null @@ -1,22 +0,0 @@ -#include "Mojang.h" - -#include "minecraft/auth/steps/GetSkinStep.h" -#include "minecraft/auth/steps/MigrationEligibilityStep.h" -#include "minecraft/auth/steps/MinecraftProfileStepMojang.h" -#include "minecraft/auth/steps/YggdrasilStep.h" - -MojangRefresh::MojangRefresh(AccountData* data, QObject* parent) : AuthFlow(data, parent) -{ - m_steps.append(makeShared(m_data, QString())); - m_steps.append(makeShared(m_data)); - m_steps.append(makeShared(m_data)); - m_steps.append(makeShared(m_data)); -} - -MojangLogin::MojangLogin(AccountData* data, QString password, QObject* parent) : AuthFlow(data, parent), m_password(password) -{ - m_steps.append(makeShared(m_data, m_password)); - m_steps.append(makeShared(m_data)); - m_steps.append(makeShared(m_data)); - m_steps.append(makeShared(m_data)); -} diff --git a/launcher/minecraft/auth/flows/Mojang.h b/launcher/minecraft/auth/flows/Mojang.h deleted file mode 100644 index 779ca7e34..000000000 --- a/launcher/minecraft/auth/flows/Mojang.h +++ /dev/null @@ -1,17 +0,0 @@ -#pragma once -#include "AuthFlow.h" - -class MojangRefresh : public AuthFlow { - Q_OBJECT - public: - explicit MojangRefresh(AccountData* data, QObject* parent = 0); -}; - -class MojangLogin : public AuthFlow { - Q_OBJECT - public: - explicit MojangLogin(AccountData* data, QString password, QObject* parent = 0); - - private: - QString m_password; -}; diff --git a/launcher/minecraft/auth/steps/MigrationEligibilityStep.cpp b/launcher/minecraft/auth/steps/MigrationEligibilityStep.cpp deleted file mode 100644 index 5ce953dfb..000000000 --- a/launcher/minecraft/auth/steps/MigrationEligibilityStep.cpp +++ /dev/null @@ -1,45 +0,0 @@ -#include "MigrationEligibilityStep.h" - -#include - -#include "minecraft/auth/AuthRequest.h" -#include "minecraft/auth/Parsers.h" - -MigrationEligibilityStep::MigrationEligibilityStep(AccountData* data) : AuthStep(data) {} - -MigrationEligibilityStep::~MigrationEligibilityStep() noexcept = default; - -QString MigrationEligibilityStep::describe() -{ - return tr("Checking for migration eligibility."); -} - -void MigrationEligibilityStep::perform() -{ - auto url = QUrl("https://api.minecraftservices.com/rollout/v1/msamigration"); - QNetworkRequest request = QNetworkRequest(url); - request.setHeader(QNetworkRequest::ContentTypeHeader, "application/json"); - request.setRawHeader("Authorization", QString("Bearer %1").arg(m_data->yggdrasilToken.token).toUtf8()); - - AuthRequest* requestor = new AuthRequest(this); - connect(requestor, &AuthRequest::finished, this, &MigrationEligibilityStep::onRequestDone); - requestor->get(request); -} - -void MigrationEligibilityStep::rehydrate() -{ - // NOOP, for now. We only save bools and there's nothing to check. -} - -void MigrationEligibilityStep::onRequestDone(QNetworkReply::NetworkError error, - QByteArray data, - QList headers) -{ - auto requestor = qobject_cast(QObject::sender()); - requestor->deleteLater(); - - if (error == QNetworkReply::NoError) { - Parsers::parseRolloutResponse(data, m_data->canMigrateToMSA); - } - emit finished(AccountTaskState::STATE_WORKING, tr("Got migration flags")); -} diff --git a/launcher/minecraft/auth/steps/MigrationEligibilityStep.h b/launcher/minecraft/auth/steps/MigrationEligibilityStep.h deleted file mode 100644 index 8638975d8..000000000 --- a/launcher/minecraft/auth/steps/MigrationEligibilityStep.h +++ /dev/null @@ -1,21 +0,0 @@ -#pragma once -#include - -#include "QObjectPtr.h" -#include "minecraft/auth/AuthStep.h" - -class MigrationEligibilityStep : public AuthStep { - Q_OBJECT - - public: - explicit MigrationEligibilityStep(AccountData* data); - virtual ~MigrationEligibilityStep() noexcept; - - void perform() override; - void rehydrate() override; - - QString describe() override; - - private slots: - void onRequestDone(QNetworkReply::NetworkError, QByteArray, QList); -}; diff --git a/launcher/minecraft/auth/steps/MinecraftProfileStep.cpp b/launcher/minecraft/auth/steps/MinecraftProfileStep.cpp index 7cdce23f0..a854342bc 100644 --- a/launcher/minecraft/auth/steps/MinecraftProfileStep.cpp +++ b/launcher/minecraft/auth/steps/MinecraftProfileStep.cpp @@ -41,10 +41,6 @@ void MinecraftProfileStep::onRequestDone(QNetworkReply::NetworkError error, QByt qCDebug(authCredentials()) << data; if (error == QNetworkReply::ContentNotFoundError) { // NOTE: Succeed even if we do not have a profile. This is a valid account state. - if (m_data->type == AccountType::Mojang) { - m_data->minecraftEntitlement.canPlayMinecraft = false; - m_data->minecraftEntitlement.ownsMinecraft = false; - } m_data->minecraftProfile = MinecraftProfile(); emit finished(AccountTaskState::STATE_SUCCEEDED, tr("Account has no Minecraft profile.")); return; @@ -73,10 +69,5 @@ void MinecraftProfileStep::onRequestDone(QNetworkReply::NetworkError error, QByt return; } - if (m_data->type == AccountType::Mojang) { - auto validProfile = m_data->minecraftProfile.validity == Katabasis::Validity::Certain; - m_data->minecraftEntitlement.canPlayMinecraft = validProfile; - m_data->minecraftEntitlement.ownsMinecraft = validProfile; - } emit finished(AccountTaskState::STATE_WORKING, tr("Minecraft Java profile acquisition succeeded.")); } diff --git a/launcher/minecraft/auth/steps/MinecraftProfileStepMojang.cpp b/launcher/minecraft/auth/steps/MinecraftProfileStepMojang.cpp deleted file mode 100644 index d035e39a0..000000000 --- a/launcher/minecraft/auth/steps/MinecraftProfileStepMojang.cpp +++ /dev/null @@ -1,87 +0,0 @@ -#include "MinecraftProfileStepMojang.h" - -#include - -#include "Logging.h" -#include "minecraft/auth/AuthRequest.h" -#include "minecraft/auth/Parsers.h" -#include "net/NetUtils.h" - -MinecraftProfileStepMojang::MinecraftProfileStepMojang(AccountData* data) : AuthStep(data) {} - -MinecraftProfileStepMojang::~MinecraftProfileStepMojang() noexcept = default; - -QString MinecraftProfileStepMojang::describe() -{ - return tr("Fetching the Minecraft profile."); -} - -void MinecraftProfileStepMojang::perform() -{ - if (m_data->minecraftProfile.id.isEmpty()) { - emit finished(AccountTaskState::STATE_FAILED_HARD, tr("A UUID is required to get the profile.")); - return; - } - - // use session server instead of profile due to profile endpoint being locked for locked Mojang accounts - QUrl url = QUrl("https://sessionserver.mojang.com/session/minecraft/profile/" + m_data->minecraftProfile.id); - QNetworkRequest req = QNetworkRequest(url); - AuthRequest* request = new AuthRequest(this); - connect(request, &AuthRequest::finished, this, &MinecraftProfileStepMojang::onRequestDone); - request->get(req); -} - -void MinecraftProfileStepMojang::rehydrate() -{ - // NOOP, for now. We only save bools and there's nothing to check. -} - -void MinecraftProfileStepMojang::onRequestDone(QNetworkReply::NetworkError error, - QByteArray data, - QList headers) -{ - auto requestor = qobject_cast(QObject::sender()); - requestor->deleteLater(); - - qCDebug(authCredentials()) << data; - if (error == QNetworkReply::ContentNotFoundError) { - // NOTE: Succeed even if we do not have a profile. This is a valid account state. - if (m_data->type == AccountType::Mojang) { - m_data->minecraftEntitlement.canPlayMinecraft = false; - m_data->minecraftEntitlement.ownsMinecraft = false; - } - m_data->minecraftProfile = MinecraftProfile(); - emit finished(AccountTaskState::STATE_SUCCEEDED, tr("Account has no Minecraft profile.")); - return; - } - if (error != QNetworkReply::NoError) { - qWarning() << "Error getting profile:"; - qWarning() << " HTTP Status: " << requestor->httpStatus_; - qWarning() << " Internal error no.: " << error; - qWarning() << " Error string: " << requestor->errorString_; - - qWarning() << " Response:"; - qWarning() << QString::fromUtf8(data); - - if (Net::isApplicationError(error)) { - emit finished(AccountTaskState::STATE_FAILED_SOFT, - tr("Minecraft Java profile acquisition failed: %1").arg(requestor->errorString_)); - } else { - emit finished(AccountTaskState::STATE_OFFLINE, - tr("Minecraft Java profile acquisition failed: %1").arg(requestor->errorString_)); - } - return; - } - if (!Parsers::parseMinecraftProfileMojang(data, m_data->minecraftProfile)) { - m_data->minecraftProfile = MinecraftProfile(); - emit finished(AccountTaskState::STATE_FAILED_SOFT, tr("Minecraft Java profile response could not be parsed")); - return; - } - - if (m_data->type == AccountType::Mojang) { - auto validProfile = m_data->minecraftProfile.validity == Katabasis::Validity::Certain; - m_data->minecraftEntitlement.canPlayMinecraft = validProfile; - m_data->minecraftEntitlement.ownsMinecraft = validProfile; - } - emit finished(AccountTaskState::STATE_WORKING, tr("Minecraft Java profile acquisition succeeded.")); -} diff --git a/launcher/minecraft/auth/steps/MinecraftProfileStepMojang.h b/launcher/minecraft/auth/steps/MinecraftProfileStepMojang.h deleted file mode 100644 index 730ec3f68..000000000 --- a/launcher/minecraft/auth/steps/MinecraftProfileStepMojang.h +++ /dev/null @@ -1,21 +0,0 @@ -#pragma once -#include - -#include "QObjectPtr.h" -#include "minecraft/auth/AuthStep.h" - -class MinecraftProfileStepMojang : public AuthStep { - Q_OBJECT - - public: - explicit MinecraftProfileStepMojang(AccountData* data); - virtual ~MinecraftProfileStepMojang() noexcept; - - void perform() override; - void rehydrate() override; - - QString describe() override; - - private slots: - void onRequestDone(QNetworkReply::NetworkError, QByteArray, QList); -}; diff --git a/launcher/minecraft/auth/steps/XboxUserStep.cpp b/launcher/minecraft/auth/steps/XboxUserStep.cpp index 61c33a18d..856036d23 100644 --- a/launcher/minecraft/auth/steps/XboxUserStep.cpp +++ b/launcher/minecraft/auth/steps/XboxUserStep.cpp @@ -38,7 +38,7 @@ void XboxUserStep::perform() QNetworkRequest request = QNetworkRequest(QUrl("https://user.auth.xboxlive.com/user/authenticate")); request.setHeader(QNetworkRequest::ContentTypeHeader, "application/json"); request.setRawHeader("Accept", "application/json"); - // set contract-verison header (prevent err 400 bad-request?) + // set contract-version header (prevent err 400 bad-request?) // https://learn.microsoft.com/en-us/gaming/gdk/_content/gc/reference/live/rest/additional/httpstandardheaders request.setRawHeader("x-xbl-contract-version", "1"); diff --git a/launcher/minecraft/auth/steps/YggdrasilStep.cpp b/launcher/minecraft/auth/steps/YggdrasilStep.cpp deleted file mode 100644 index fdcaa0d67..000000000 --- a/launcher/minecraft/auth/steps/YggdrasilStep.cpp +++ /dev/null @@ -1,57 +0,0 @@ -#include "YggdrasilStep.h" - -#include "minecraft/auth/AuthRequest.h" -#include "minecraft/auth/Parsers.h" -#include "minecraft/auth/Yggdrasil.h" - -YggdrasilStep::YggdrasilStep(AccountData* data, QString password) : AuthStep(data), m_password(password) -{ - m_yggdrasil = new Yggdrasil(m_data, this); - - connect(m_yggdrasil, &Task::failed, this, &YggdrasilStep::onAuthFailed); - connect(m_yggdrasil, &Task::succeeded, this, &YggdrasilStep::onAuthSucceeded); - connect(m_yggdrasil, &Task::aborted, this, &YggdrasilStep::onAuthFailed); -} - -YggdrasilStep::~YggdrasilStep() noexcept = default; - -QString YggdrasilStep::describe() -{ - return tr("Logging in with Mojang account."); -} - -void YggdrasilStep::rehydrate() -{ - // NOOP, for now. -} - -void YggdrasilStep::perform() -{ - if (m_password.size()) { - m_yggdrasil->login(m_password); - } else { - m_yggdrasil->refresh(); - } -} - -void YggdrasilStep::onAuthSucceeded() -{ - emit finished(AccountTaskState::STATE_WORKING, tr("Logged in with Mojang")); -} - -void YggdrasilStep::onAuthFailed() -{ - // TODO: hook these in again, expand to MSA - // m_error = m_yggdrasil->m_error; - // m_aborted = m_yggdrasil->m_aborted; - - auto state = m_yggdrasil->taskState(); - QString errorMessage = tr("Mojang user authentication failed."); - - // NOTE: soft error in the first step means 'offline' - if (state == AccountTaskState::STATE_FAILED_SOFT) { - state = AccountTaskState::STATE_OFFLINE; - errorMessage = tr("Mojang user authentication ended with a network error."); - } - emit finished(state, errorMessage); -} diff --git a/launcher/minecraft/auth/steps/YggdrasilStep.h b/launcher/minecraft/auth/steps/YggdrasilStep.h deleted file mode 100644 index ef31f34d5..000000000 --- a/launcher/minecraft/auth/steps/YggdrasilStep.h +++ /dev/null @@ -1,28 +0,0 @@ -#pragma once -#include - -#include "QObjectPtr.h" -#include "minecraft/auth/AuthStep.h" - -class Yggdrasil; - -class YggdrasilStep : public AuthStep { - Q_OBJECT - - public: - explicit YggdrasilStep(AccountData* data, QString password); - virtual ~YggdrasilStep() noexcept; - - void perform() override; - void rehydrate() override; - - QString describe() override; - - private slots: - void onAuthSucceeded(); - void onAuthFailed(); - - private: - Yggdrasil* m_yggdrasil = nullptr; - QString m_password; -}; diff --git a/launcher/minecraft/launch/LauncherPartLaunch.cpp b/launcher/minecraft/launch/LauncherPartLaunch.cpp index 44e5d0a63..aa94edb5d 100644 --- a/launcher/minecraft/launch/LauncherPartLaunch.cpp +++ b/launcher/minecraft/launch/LauncherPartLaunch.cpp @@ -105,6 +105,17 @@ void LauncherPartLaunch::executeTask() auto instance = m_parent->instance(); std::shared_ptr minecraftInstance = std::dynamic_pointer_cast(instance); + QString legacyJarPath; + if (minecraftInstance->getLauncher() == "legacy" || minecraftInstance->shouldApplyOnlineFixes()) { + legacyJarPath = APPLICATION->getJarPath("NewLaunchLegacy.jar"); + if (legacyJarPath.isEmpty()) { + const char* reason = QT_TR_NOOP("Legacy launcher library could not be found. Please check your installation."); + emit logLine(tr(reason), MessageLevel::Fatal); + emitFailed(tr(reason)); + return; + } + } + m_launchScript = minecraftInstance->createLaunchScript(m_session, m_serverToJoin); QStringList args = minecraftInstance->javaArguments(); QString allArgs = args.join(", "); @@ -120,6 +131,9 @@ void LauncherPartLaunch::executeTask() auto classPath = minecraftInstance->getClassPath(); classPath.prepend(jarPath); + if (!legacyJarPath.isEmpty()) + classPath.prepend(legacyJarPath); + auto natPath = minecraftInstance->getNativePath(); #ifdef Q_OS_WIN if (!fitsInLocal8bit(natPath)) { diff --git a/launcher/minecraft/mod/MetadataHandler.h b/launcher/minecraft/mod/MetadataHandler.h index 88e9ff2b6..3496da2a0 100644 --- a/launcher/minecraft/mod/MetadataHandler.h +++ b/launcher/minecraft/mod/MetadataHandler.h @@ -31,6 +31,7 @@ class Mod; class Metadata { public: using ModStruct = Packwiz::V1::Mod; + using ModSide = Packwiz::V1::Side; static auto create(QDir& index_dir, ModPlatform::IndexedPack& mod_pack, ModPlatform::IndexedVersion& mod_version) -> ModStruct { diff --git a/launcher/minecraft/mod/ResourceFolderModel.h b/launcher/minecraft/mod/ResourceFolderModel.h index 80c31e456..60b8879c0 100644 --- a/launcher/minecraft/mod/ResourceFolderModel.h +++ b/launcher/minecraft/mod/ResourceFolderModel.h @@ -330,7 +330,8 @@ void ResourceFolderModel::applyUpdates(QSet& current_set, QSet // When you have a Qt build with assertions turned on, proceeding here will abort the application if (added_set.size() > 0) { - beginInsertRows(QModelIndex(), m_resources.size(), m_resources.size() + added_set.size() - 1); + beginInsertRows(QModelIndex(), static_cast(m_resources.size()), + static_cast(m_resources.size() + added_set.size() - 1)); for (auto& added : added_set) { auto res = new_resources[added]; diff --git a/launcher/minecraft/mod/ShaderPack.cpp b/launcher/minecraft/mod/ShaderPack.cpp index 6a9641de2..2c094f26a 100644 --- a/launcher/minecraft/mod/ShaderPack.cpp +++ b/launcher/minecraft/mod/ShaderPack.cpp @@ -22,7 +22,7 @@ #include "ShaderPack.h" -#include "minecraft/mod/tasks/LocalShaderPackParseTask.h" +#include void ShaderPack::setPackFormat(ShaderPackFormat new_format) { @@ -35,3 +35,8 @@ bool ShaderPack::valid() const { return m_pack_format != ShaderPackFormat::INVALID; } + +bool ShaderPack::applyFilter(QRegularExpression filter) const +{ + return valid() && Resource::applyFilter(filter); +} diff --git a/launcher/minecraft/mod/ShaderPack.h b/launcher/minecraft/mod/ShaderPack.h index ec0f9404e..d07c124be 100644 --- a/launcher/minecraft/mod/ShaderPack.h +++ b/launcher/minecraft/mod/ShaderPack.h @@ -54,6 +54,7 @@ class ShaderPack : public Resource { void setPackFormat(ShaderPackFormat new_format); bool valid() const override; + [[nodiscard]] bool applyFilter(QRegularExpression filter) const override; protected: mutable QMutex m_data_lock; diff --git a/launcher/minecraft/mod/ShaderPackFolderModel.h b/launcher/minecraft/mod/ShaderPackFolderModel.h index 44ed37a47..186d02139 100644 --- a/launcher/minecraft/mod/ShaderPackFolderModel.h +++ b/launcher/minecraft/mod/ShaderPackFolderModel.h @@ -1,6 +1,9 @@ #pragma once #include "ResourceFolderModel.h" +#include "minecraft/mod/ShaderPack.h" +#include "minecraft/mod/tasks/BasicFolderLoadTask.h" +#include "minecraft/mod/tasks/LocalShaderPackParseTask.h" class ShaderPackFolderModel : public ResourceFolderModel { Q_OBJECT @@ -9,4 +12,14 @@ class ShaderPackFolderModel : public ResourceFolderModel { explicit ShaderPackFolderModel(const QString& dir, BaseInstance* instance) : ResourceFolderModel(QDir(dir), instance) {} virtual QString id() const override { return "shaderpacks"; } + + [[nodiscard]] Task* createUpdateTask() override + { + return new BasicFolderLoadTask(m_dir, [](QFileInfo const& entry) { return makeShared(entry); }); + } + + [[nodiscard]] Task* createParseTask(Resource& resource) override + { + return new LocalShaderPackParseTask(m_next_resolution_ticket, static_cast(resource)); + } }; diff --git a/launcher/minecraft/mod/tasks/GetModDependenciesTask.cpp b/launcher/minecraft/mod/tasks/GetModDependenciesTask.cpp index bd1fe9401..df8c690af 100644 --- a/launcher/minecraft/mod/tasks/GetModDependenciesTask.cpp +++ b/launcher/minecraft/mod/tasks/GetModDependenciesTask.cpp @@ -94,7 +94,7 @@ QList GetModDependenciesTask::getDependenciesForVersion for (auto ver_dep : version.dependencies) { if (ver_dep.type != ModPlatform::DependencyType::REQUIRED) continue; - + ver_dep = getOverride(ver_dep, providerName); auto isOnlyVersion = providerName == ModPlatform::ResourceProvider::MODRINTH && ver_dep.addonId.toString().isEmpty(); if (auto dep = std::find_if(c_dependencies.begin(), c_dependencies.end(), [&ver_dep, isOnlyVersion](const ModPlatform::Dependency& i) { @@ -127,7 +127,7 @@ QList GetModDependenciesTask::getDependenciesForVersion dep != m_pack_dependencies.end()) // check loaded dependencies continue; - c_dependencies.append(getOverride(ver_dep, providerName)); + c_dependencies.append(ver_dep); } return c_dependencies; } diff --git a/launcher/minecraft/mod/tasks/ModFolderLoadTask.cpp b/launcher/minecraft/mod/tasks/ModFolderLoadTask.cpp index 9f79ba098..2094df4fc 100644 --- a/launcher/minecraft/mod/tasks/ModFolderLoadTask.cpp +++ b/launcher/minecraft/mod/tasks/ModFolderLoadTask.cpp @@ -122,7 +122,7 @@ void ModFolderLoadTask::getFromMetadata() auto metadata = Metadata::get(m_index_dir, entry); if (!metadata.isValid()) { - return; + continue; } auto* mod = new Mod(m_mods_dir, metadata); diff --git a/launcher/modplatform/CheckUpdateTask.h b/launcher/modplatform/CheckUpdateTask.h index 61e6a3a98..8bd83d988 100644 --- a/launcher/modplatform/CheckUpdateTask.h +++ b/launcher/modplatform/CheckUpdateTask.h @@ -24,6 +24,7 @@ class CheckUpdateTask : public Task { QString old_hash; QString old_version; QString new_version; + std::optional new_version_type; QString changelog; ModPlatform::ResourceProvider provider; shared_qobject_ptr download; @@ -33,10 +34,18 @@ class CheckUpdateTask : public Task { QString old_h, QString old_v, QString new_v, + std::optional new_v_type, QString changelog, ModPlatform::ResourceProvider p, shared_qobject_ptr t) - : name(name), old_hash(old_h), old_version(old_v), new_version(new_v), changelog(changelog), provider(p), download(t) + : name(name) + , old_hash(old_h) + , old_version(old_v) + , new_version(new_v) + , new_version_type(new_v_type) + , changelog(changelog) + , provider(p) + , download(t) {} }; diff --git a/launcher/modplatform/ModIndex.cpp b/launcher/modplatform/ModIndex.cpp index e8e4a38dd..fc79dff15 100644 --- a/launcher/modplatform/ModIndex.cpp +++ b/launcher/modplatform/ModIndex.cpp @@ -24,6 +24,40 @@ namespace ModPlatform { +static const QMap s_indexed_version_type_names = { + { "release", IndexedVersionType::VersionType::Release }, + { "beta", IndexedVersionType::VersionType::Beta }, + { "alpha", IndexedVersionType::VersionType::Alpha } +}; + +IndexedVersionType::IndexedVersionType(const QString& type) : IndexedVersionType(enumFromString(type)) {} + +IndexedVersionType::IndexedVersionType(const IndexedVersionType::VersionType& type) +{ + m_type = type; +} + +IndexedVersionType::IndexedVersionType(const IndexedVersionType& other) +{ + m_type = other.m_type; +} + +IndexedVersionType& IndexedVersionType::operator=(const IndexedVersionType& other) +{ + m_type = other.m_type; + return *this; +} + +const QString IndexedVersionType::toString(const IndexedVersionType::VersionType& type) +{ + return s_indexed_version_type_names.key(type, "unknown"); +} + +IndexedVersionType::VersionType IndexedVersionType::enumFromString(const QString& type) +{ + return s_indexed_version_type_names.value(type, IndexedVersionType::VersionType::Unknown); +} + auto ProviderCapabilities::name(ResourceProvider p) -> const char* { switch (p) { diff --git a/launcher/modplatform/ModIndex.h b/launcher/modplatform/ModIndex.h index 7d144176d..72294c399 100644 --- a/launcher/modplatform/ModIndex.h +++ b/launcher/modplatform/ModIndex.h @@ -25,6 +25,7 @@ #include #include #include +#include class QIODevice; @@ -58,6 +59,34 @@ struct DonationData { QString url; }; +struct IndexedVersionType { + enum class VersionType { Release = 1, Beta, Alpha, Unknown }; + IndexedVersionType(const QString& type); + IndexedVersionType(const IndexedVersionType::VersionType& type); + IndexedVersionType(const IndexedVersionType& type); + IndexedVersionType() : IndexedVersionType(IndexedVersionType::VersionType::Unknown) {} + static const QString toString(const IndexedVersionType::VersionType& type); + static IndexedVersionType::VersionType enumFromString(const QString& type); + bool isValid() const { return m_type != IndexedVersionType::VersionType::Unknown; } + IndexedVersionType& operator=(const IndexedVersionType& other); + bool operator==(const IndexedVersionType& other) const { return m_type == other.m_type; } + bool operator==(const IndexedVersionType::VersionType& type) const { return m_type == type; } + bool operator!=(const IndexedVersionType& other) const { return m_type != other.m_type; } + bool operator!=(const IndexedVersionType::VersionType& type) const { return m_type != type; } + bool operator<(const IndexedVersionType& other) const { return m_type < other.m_type; } + bool operator<(const IndexedVersionType::VersionType& type) const { return m_type < type; } + bool operator<=(const IndexedVersionType& other) const { return m_type <= other.m_type; } + bool operator<=(const IndexedVersionType::VersionType& type) const { return m_type <= type; } + bool operator>(const IndexedVersionType& other) const { return m_type > other.m_type; } + bool operator>(const IndexedVersionType::VersionType& type) const { return m_type > type; } + bool operator>=(const IndexedVersionType& other) const { return m_type >= other.m_type; } + bool operator>=(const IndexedVersionType::VersionType& type) const { return m_type >= type; } + + QString toString() const { return toString(m_type); } + + IndexedVersionType::VersionType m_type; +}; + struct Dependency { QVariant addonId; DependencyType type; @@ -69,6 +98,7 @@ struct IndexedVersion { QVariant fileId; QString version; QString version_number = {}; + IndexedVersionType version_type; QStringList mcVersion; QString downloadUrl; QString date; @@ -107,6 +137,7 @@ struct IndexedPack { QString logoName; QString logoUrl; QString websiteUrl; + QString side; bool versionsLoaded = false; QVector versions; diff --git a/launcher/modplatform/atlauncher/ATLPackIndex.cpp b/launcher/modplatform/atlauncher/ATLPackIndex.cpp index 3be169739..678db63cc 100644 --- a/launcher/modplatform/atlauncher/ATLPackIndex.cpp +++ b/launcher/modplatform/atlauncher/ATLPackIndex.cpp @@ -43,5 +43,5 @@ void ATLauncher::loadIndexedPack(ATLauncher::IndexedPack& m, QJsonObject& obj) m.system = Json::ensureBoolean(obj, QString("system"), false); m.description = Json::ensureString(obj, "description", ""); - m.safeName = Json::requireString(obj, "name").replace(QRegularExpression("[^A-Za-z0-9]"), ""); + m.safeName = Json::requireString(obj, "name").replace(QRegularExpression("[^A-Za-z0-9]"), "").toLower() + ".png"; } diff --git a/launcher/modplatform/atlauncher/ATLPackInstallTask.cpp b/launcher/modplatform/atlauncher/ATLPackInstallTask.cpp index e5771b7cd..8ae8145de 100644 --- a/launcher/modplatform/atlauncher/ATLPackInstallTask.cpp +++ b/launcher/modplatform/atlauncher/ATLPackInstallTask.cpp @@ -37,6 +37,7 @@ #include "ATLPackInstallTask.h" #include +#include #include @@ -50,6 +51,7 @@ #include "minecraft/MinecraftInstance.h" #include "minecraft/OneSixVersionFormat.h" #include "minecraft/PackProfile.h" +#include "modplatform/atlauncher/ATLPackManifest.h" #include "net/ChecksumValidator.h" #include "settings/INISettingsObject.h" @@ -57,6 +59,7 @@ #include "Application.h" #include "BuildConfig.h" +#include "ui/dialogs/BlockedModsDialog.h" namespace ATLauncher { @@ -717,6 +720,8 @@ void PackInstallTask::downloadMods() jarmods.clear(); jobPtr.reset(new NetJob(tr("Mod download"), APPLICATION->network())); + + QList blocked_mods; for (const auto& mod : m_version.mods) { // skip non-client mods if (!mod.client) @@ -731,9 +736,10 @@ void PackInstallTask::downloadMods() case DownloadType::Server: url = BuildConfig.ATL_DOWNLOAD_SERVER_URL + mod.url; break; - case DownloadType::Browser: - emitFailed(tr("Unsupported download type: %1").arg(mod.download_raw)); - return; + case DownloadType::Browser: { + blocked_mods.append(mod); + continue; + } case DownloadType::Direct: url = mod.url; break; @@ -805,24 +811,86 @@ void PackInstallTask::downloadMods() modsToCopy[entry->getFullPath()] = path; } } + if (!blocked_mods.isEmpty()) { + QList mods; + + for (auto mod : blocked_mods) { + BlockedMod blocked_mod; + blocked_mod.name = mod.file; + blocked_mod.websiteUrl = mod.url; + blocked_mod.hash = mod.md5; + blocked_mod.matched = false; + blocked_mod.localPath = ""; + + mods.append(blocked_mod); + } + + qWarning() << "Blocked mods found, displaying mod list"; + + BlockedModsDialog message_dialog(nullptr, tr("Blocked mods found"), + tr("The following files are not available for download in third party launchers.
" + "You will need to manually download them and add them to the instance."), + mods, "md5"); + + message_dialog.setModal(true); + + if (message_dialog.exec()) { + qDebug() << "Post dialog blocked mods list: " << mods; + for (auto blocked : mods) { + if (!blocked.matched) { + qDebug() << blocked.name << "was not matched to a local file, skipping copy"; + continue; + } + auto modIter = std::find_if(blocked_mods.begin(), blocked_mods.end(), + [blocked](const VersionMod& mod) { return mod.url == blocked.websiteUrl; }); + if (modIter == blocked_mods.end()) + continue; + auto mod = *modIter; + if (mod.type == ModType::Extract || mod.type == ModType::TexturePackExtract || mod.type == ModType::ResourcePackExtract) { + modsToExtract.insert(blocked.localPath, mod); + } else if (mod.type == ModType::Decomp) { + modsToDecomp.insert(blocked.localPath, mod); + } else { + auto relpath = getDirForModType(mod.type, mod.type_raw); + if (relpath == Q_NULLPTR) + continue; + + auto path = FS::PathCombine(m_stagingPath, "minecraft", relpath, mod.file); + + if (mod.type == ModType::Forge) { + auto ver = getComponentVersion("net.minecraftforge", mod.version); + if (ver) { + componentsToInstall.insert("net.minecraftforge", ver); + continue; + } + + qDebug() << "Jarmod: " + path; + jarmods.push_back(path); + } + + if (mod.type == ModType::Jar) { + qDebug() << "Jarmod: " + path; + jarmods.push_back(path); + } + + modsToCopy[blocked.localPath] = path; + } + } + } else { + emitFailed(tr("Unknown download type: %1").arg("browser")); + return; + } + } connect(jobPtr.get(), &NetJob::succeeded, this, &PackInstallTask::onModsDownloaded); - connect(jobPtr.get(), &NetJob::failed, [&](QString reason) { - abortable = false; - jobPtr.reset(); - emitFailed(reason); - }); - connect(jobPtr.get(), &NetJob::progress, [&](qint64 current, qint64 total) { + connect(jobPtr.get(), &NetJob::progress, [this](qint64 current, qint64 total) { setDetails(tr("%1 out of %2 complete").arg(current).arg(total)); abortable = true; setProgress(current, total); }); connect(jobPtr.get(), &NetJob::stepProgress, this, &PackInstallTask::propagateStepProgress); - connect(jobPtr.get(), &NetJob::aborted, [&] { - abortable = false; - jobPtr.reset(); - emitAborted(); - }); + connect(jobPtr.get(), &NetJob::aborted, &PackInstallTask::emitAborted); + connect(jobPtr.get(), &NetJob::failed, &PackInstallTask::emitFailed); jobPtr->start(); } @@ -843,7 +911,7 @@ void PackInstallTask::onModsDownloaded() QtConcurrent::run(QThreadPool::globalInstance(), this, &PackInstallTask::extractMods, modsToExtract, modsToDecomp, modsToCopy); #endif connect(&m_modExtractFutureWatcher, &QFutureWatcher::finished, this, &PackInstallTask::onModsExtracted); - connect(&m_modExtractFutureWatcher, &QFutureWatcher::canceled, this, [&]() { emitAborted(); }); + connect(&m_modExtractFutureWatcher, &QFutureWatcher::canceled, this, &PackInstallTask::emitAborted); m_modExtractFutureWatcher.setFuture(m_modExtractFuture); } else { install(); diff --git a/launcher/modplatform/flame/FlameCheckUpdate.cpp b/launcher/modplatform/flame/FlameCheckUpdate.cpp index 0501dda62..c014863a3 100644 --- a/launcher/modplatform/flame/FlameCheckUpdate.cpp +++ b/launcher/modplatform/flame/FlameCheckUpdate.cpp @@ -173,7 +173,7 @@ void FlameCheckUpdate::executeTask() } auto download_task = makeShared(pack, latest_ver, m_mods_folder); - m_updatable.emplace_back(pack->name, mod->metadata()->hash, old_version, latest_ver.version, + m_updatable.emplace_back(pack->name, mod->metadata()->hash, old_version, latest_ver.version, latest_ver.version_type, api.getModFileChangelog(latest_ver.addonId.toInt(), latest_ver.fileId.toInt()), ModPlatform::ResourceProvider::FLAME, download_task); } diff --git a/launcher/modplatform/flame/FlameInstanceCreationTask.cpp b/launcher/modplatform/flame/FlameInstanceCreationTask.cpp index 45b4e2125..2a26ce944 100644 --- a/launcher/modplatform/flame/FlameInstanceCreationTask.cpp +++ b/launcher/modplatform/flame/FlameInstanceCreationTask.cpp @@ -62,6 +62,7 @@ #include "minecraft/World.h" #include "minecraft/mod/tasks/LocalResourceParse.h" #include "net/ApiDownload.h" +#include "ui/pages/modplatform/OptionalModDialog.h" static const FlameAPI api; @@ -509,13 +510,33 @@ void FlameCreationTask::idResolverSucceeded(QEventLoop& loop) void FlameCreationTask::setupDownloadJob(QEventLoop& loop) { m_files_job.reset(new NetJob(tr("Mod Download Flame"), APPLICATION->network())); - for (const auto& result : m_mod_id_resolver->getResults().files) { - QString filename = result.fileName; + auto results = m_mod_id_resolver->getResults().files; + + QStringList optionalFiles; + for (auto& result : results) { if (!result.required) { - filename += ".disabled"; + optionalFiles << FS::PathCombine(result.targetFolder, result.fileName); + } + } + + QStringList selectedOptionalMods; + if (!optionalFiles.empty()) { + OptionalModDialog optionalModDialog(m_parent, optionalFiles); + if (optionalModDialog.exec() == QDialog::Rejected) { + emitAborted(); + loop.quit(); + return; } - auto relpath = FS::PathCombine("minecraft", result.targetFolder, filename); + selectedOptionalMods = optionalModDialog.getResult(); + } + for (const auto& result : results) { + auto relpath = FS::PathCombine(result.targetFolder, result.fileName); + if (!result.required && !selectedOptionalMods.contains(relpath)) { + relpath += ".disabled"; + } + + relpath = FS::PathCombine("minecraft", relpath); auto path = FS::PathCombine(m_stagingPath, relpath); switch (result.type) { @@ -547,7 +568,7 @@ void FlameCreationTask::setupDownloadJob(QEventLoop& loop) m_mod_id_resolver.reset(); connect(m_files_job.get(), &NetJob::succeeded, this, [&]() { m_files_job.reset(); - validateZIPResouces(); + validateZIPResources(); }); connect(m_files_job.get(), &NetJob::failed, [&](QString reason) { m_files_job.reset(); @@ -596,7 +617,7 @@ void FlameCreationTask::copyBlockedMods(QList const& blocked_mods) setAbortable(true); } -void FlameCreationTask::validateZIPResouces() +void FlameCreationTask::validateZIPResources() { qDebug() << "Validating whether resources stored as .zip are in the right place"; for (auto [fileName, targetFolder] : m_ZIP_resources) { @@ -649,8 +670,8 @@ void FlameCreationTask::validateZIPResouces() validatePath(fileName, targetFolder, "datapacks"); break; case PackedResourceType::ShaderPack: - // in theroy flame API can't do this but who knows, that *may* change ? - // better to handle it if it *does* occure in the future + // in theory flame API can't do this but who knows, that *may* change ? + // better to handle it if it *does* occur in the future validatePath(fileName, targetFolder, "shaderpacks"); break; case PackedResourceType::WorldSave: diff --git a/launcher/modplatform/flame/FlameInstanceCreationTask.h b/launcher/modplatform/flame/FlameInstanceCreationTask.h index 603d3693e..02ad48f2e 100644 --- a/launcher/modplatform/flame/FlameInstanceCreationTask.h +++ b/launcher/modplatform/flame/FlameInstanceCreationTask.h @@ -74,7 +74,7 @@ class FlameCreationTask final : public InstanceCreationTask { void idResolverSucceeded(QEventLoop&); void setupDownloadJob(QEventLoop&); void copyBlockedMods(QList const& blocked_mods); - void validateZIPResouces(); + void validateZIPResources(); QString getVersionForLoader(QString uid, QString loaderType, QString version, QString mcVersion); private: diff --git a/launcher/modplatform/flame/FlameModIndex.cpp b/launcher/modplatform/flame/FlameModIndex.cpp index 494dc2a7e..345883c17 100644 --- a/launcher/modplatform/flame/FlameModIndex.cpp +++ b/launcher/modplatform/flame/FlameModIndex.cpp @@ -139,6 +139,22 @@ auto FlameMod::loadIndexedPackVersion(QJsonObject& obj, bool load_changelog) -> file.downloadUrl = Json::ensureString(obj, "downloadUrl"); file.fileName = Json::requireString(obj, "fileName"); + ModPlatform::IndexedVersionType::VersionType ver_type; + switch (Json::requireInteger(obj, "releaseType")) { + case 1: + ver_type = ModPlatform::IndexedVersionType::VersionType::Release; + break; + case 2: + ver_type = ModPlatform::IndexedVersionType::VersionType::Beta; + break; + case 3: + ver_type = ModPlatform::IndexedVersionType::VersionType::Alpha; + break; + default: + ver_type = ModPlatform::IndexedVersionType::VersionType::Unknown; + } + file.version_type = ModPlatform::IndexedVersionType(ver_type); + auto hash_list = Json::ensureArray(obj, "hashes"); for (auto h : hash_list) { auto hash_entry = Json::ensureObject(h); diff --git a/launcher/modplatform/flame/FlamePackIndex.cpp b/launcher/modplatform/flame/FlamePackIndex.cpp index 21835a543..ca8e0a853 100644 --- a/launcher/modplatform/flame/FlamePackIndex.cpp +++ b/launcher/modplatform/flame/FlamePackIndex.cpp @@ -1,4 +1,6 @@ #include "FlamePackIndex.h" +#include +#include #include "Json.h" @@ -9,8 +11,8 @@ void Flame::loadIndexedPack(Flame::IndexedPack& pack, QJsonObject& obj) pack.description = Json::ensureString(obj, "summary", ""); auto logo = Json::requireObject(obj, "logo"); - pack.logoName = Json::requireString(logo, "title"); pack.logoUrl = Json::requireString(logo, "thumbnailUrl"); + pack.logoName = Json::requireString(obj, "slug") + "." + QFileInfo(QUrl(pack.logoUrl).fileName()).suffix(); auto authors = Json::requireArray(obj, "authors"); for (auto authorIter : authors) { @@ -89,6 +91,22 @@ void Flame::loadIndexedPackVersions(Flame::IndexedPack& pack, QJsonArray& arr) // pick the latest version supported file.mcVersion = versionArray[0].toString(); file.version = Json::requireString(version, "displayName"); + + ModPlatform::IndexedVersionType::VersionType ver_type; + switch (Json::requireInteger(version, "releaseType")) { + case 1: + ver_type = ModPlatform::IndexedVersionType::VersionType::Release; + break; + case 2: + ver_type = ModPlatform::IndexedVersionType::VersionType::Beta; + break; + case 3: + ver_type = ModPlatform::IndexedVersionType::VersionType::Alpha; + break; + default: + ver_type = ModPlatform::IndexedVersionType::VersionType::Unknown; + } + file.version_type = ModPlatform::IndexedVersionType(ver_type); file.downloadUrl = Json::ensureString(version, "downloadUrl"); // only add if we have a download URL (third party distribution is enabled) diff --git a/launcher/modplatform/flame/FlamePackIndex.h b/launcher/modplatform/flame/FlamePackIndex.h index b089b722c..b2a12a67f 100644 --- a/launcher/modplatform/flame/FlamePackIndex.h +++ b/launcher/modplatform/flame/FlamePackIndex.h @@ -17,6 +17,7 @@ struct IndexedVersion { int addonId; int fileId; QString version; + ModPlatform::IndexedVersionType version_type; QString mcVersion; QString downloadUrl; }; diff --git a/launcher/modplatform/flame/PackManifest.h b/launcher/modplatform/flame/PackManifest.h index 854cdbc41..4417c2430 100644 --- a/launcher/modplatform/flame/PackManifest.h +++ b/launcher/modplatform/flame/PackManifest.h @@ -48,7 +48,7 @@ struct File { int projectId = 0; int fileId = 0; - // NOTE: the opposite to 'optional'. This is at the time of writing unused. + // NOTE: the opposite to 'optional' bool required = true; QString hash; // NOTE: only set on blocked files ! Empty otherwise. diff --git a/launcher/modplatform/legacy_ftb/PackInstallTask.cpp b/launcher/modplatform/legacy_ftb/PackInstallTask.cpp index 761f622bb..091296751 100644 --- a/launcher/modplatform/legacy_ftb/PackInstallTask.cpp +++ b/launcher/modplatform/legacy_ftb/PackInstallTask.cpp @@ -70,16 +70,18 @@ void PackInstallTask::downloadPack() setProgress(1, 4); setAbortable(false); - archivePath = QString("%1/%2/%3").arg(m_pack.dir, m_version.replace(".", "_"), m_pack.file); - + auto path = QString("%1/%2/%3").arg(m_pack.dir, m_version.replace(".", "_"), m_pack.file); + auto entry = APPLICATION->metacache()->resolveEntry("FTBPacks", path); + entry->setStale(true); + archivePath = entry->getFullPath(); netJobContainer.reset(new NetJob("Download FTB Pack", m_network)); QString url; if (m_pack.type == PackType::Private) { - url = QString(BuildConfig.LEGACY_FTB_CDN_BASE_URL + "privatepacks/%1").arg(archivePath); + url = QString(BuildConfig.LEGACY_FTB_CDN_BASE_URL + "privatepacks/%1").arg(path); } else { - url = QString(BuildConfig.LEGACY_FTB_CDN_BASE_URL + "modpacks/%1").arg(archivePath); + url = QString(BuildConfig.LEGACY_FTB_CDN_BASE_URL + "modpacks/%1").arg(path); } - netJobContainer->addNetAction(Net::ApiDownload::makeFile(url, archivePath)); + netJobContainer->addNetAction(Net::ApiDownload::makeCached(url, entry)); connect(netJobContainer.get(), &NetJob::succeeded, this, &PackInstallTask::unzip); connect(netJobContainer.get(), &NetJob::failed, this, &PackInstallTask::emitFailed); diff --git a/launcher/modplatform/modrinth/ModrinthCheckUpdate.cpp b/launcher/modplatform/modrinth/ModrinthCheckUpdate.cpp index ab07a363c..9b7c53854 100644 --- a/launcher/modplatform/modrinth/ModrinthCheckUpdate.cpp +++ b/launcher/modplatform/modrinth/ModrinthCheckUpdate.cpp @@ -161,8 +161,8 @@ void ModrinthCheckUpdate::executeTask() auto download_task = makeShared(pack, project_ver, m_mods_folder); - m_updatable.emplace_back(pack->name, hash, mod->version(), project_ver.version_number, project_ver.changelog, - ModPlatform::ResourceProvider::MODRINTH, download_task); + m_updatable.emplace_back(pack->name, hash, mod->version(), project_ver.version_number, project_ver.version_type, + project_ver.changelog, ModPlatform::ResourceProvider::MODRINTH, download_task); } m_deps.append(std::make_shared(pack, project_ver)); } diff --git a/launcher/modplatform/modrinth/ModrinthInstanceCreationTask.cpp b/launcher/modplatform/modrinth/ModrinthInstanceCreationTask.cpp index 9ff6b374d..e732ad39c 100644 --- a/launcher/modplatform/modrinth/ModrinthInstanceCreationTask.cpp +++ b/launcher/modplatform/modrinth/ModrinthInstanceCreationTask.cpp @@ -9,6 +9,7 @@ #include "modplatform/helpers/OverrideUtils.h" +#include "modplatform/modrinth/ModrinthPackManifest.h" #include "net/ChecksumValidator.h" #include "net/ApiDownload.h" @@ -16,8 +17,10 @@ #include "settings/INISettingsObject.h" #include "ui/dialogs/CustomMessageBox.h" +#include "ui/pages/modplatform/OptionalModDialog.h" #include +#include bool ModrinthCreationTask::abort() { @@ -319,10 +322,10 @@ bool ModrinthCreationTask::parseManifest(const QString& index_path, } auto jsonFiles = Json::requireIsArrayOf(obj, "files", "modrinth.index.json"); - bool had_optional = false; + std::vector optionalFiles; for (const auto& modInfo : jsonFiles) { Modrinth::File file; - file.path = Json::requireString(modInfo, "path"); + file.path = Json::requireString(modInfo, "path").replace("\\", "/"); auto env = Json::ensureObject(modInfo, "env"); // 'env' field is optional @@ -331,18 +334,7 @@ bool ModrinthCreationTask::parseManifest(const QString& index_path, if (support == "unsupported") { continue; } else if (support == "optional") { - // TODO: Make a review dialog for choosing which ones the user wants! - if (!had_optional && show_optional_dialog) { - had_optional = true; - auto info = CustomMessageBox::selectable( - m_parent, tr("Optional mod detected!"), - tr("One or more mods from this modpack are optional. They will be downloaded, but disabled by default!"), - QMessageBox::Information); - info->exec(); - } - - if (file.path.endsWith(".jar")) - file.path += ".disabled"; + file.required = false; } } @@ -385,9 +377,29 @@ bool ModrinthCreationTask::parseManifest(const QString& index_path, } } - files.push_back(file); + (file.required ? files : optionalFiles).push_back(file); } + if (!optionalFiles.empty()) { + QStringList oFiles; + for (auto file : optionalFiles) + oFiles.push_back(file.path); + OptionalModDialog optionalModDialog(m_parent, oFiles); + if (optionalModDialog.exec() == QDialog::Rejected) { + emitAborted(); + return false; + } + + auto selectedMods = optionalModDialog.getResult(); + for (auto file : optionalFiles) { + if (selectedMods.contains(file.path)) { + file.required = true; + } else { + file.path += ".disabled"; + } + files.push_back(file); + } + } if (set_internal_data) { auto dependencies = Json::requireObject(obj, "dependencies", "modrinth.index.json"); for (auto it = dependencies.begin(), end = dependencies.end(); it != end; ++it) { diff --git a/launcher/modplatform/modrinth/ModrinthPackExportTask.cpp b/launcher/modplatform/modrinth/ModrinthPackExportTask.cpp index a9ddb0c91..e9e8a3b75 100644 --- a/launcher/modplatform/modrinth/ModrinthPackExportTask.cpp +++ b/launcher/modplatform/modrinth/ModrinthPackExportTask.cpp @@ -25,6 +25,7 @@ #include "Json.h" #include "MMCZip.h" #include "minecraft/PackProfile.h" +#include "minecraft/mod/MetadataHandler.h" #include "minecraft/mod/ModFolderModel.h" const QStringList ModrinthPackExportTask::PREFIXES({ "mods/", "coremods/", "resourcepacks/", "texturepacks/", "shaderpacks/" }); @@ -129,7 +130,8 @@ void ModrinthPackExportTask::collectHashes() QCryptographicHash sha1(QCryptographicHash::Algorithm::Sha1); sha1.addData(data); - ResolvedFile resolvedFile{ sha1.result().toHex(), sha512.result().toHex(), url.toEncoded(), openFile.size() }; + ResolvedFile resolvedFile{ sha1.result().toHex(), sha512.result().toHex(), url.toEncoded(), openFile.size(), + mod->metadata()->side }; resolvedFiles[relative] = resolvedFile; // nice! we've managed to resolve based on local metadata! @@ -272,22 +274,33 @@ QByteArray ModrinthPackExportTask::generateIndex() QString path = iterator.key(); const ResolvedFile& value = iterator.value(); - 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; - } + QJsonObject env; + + // detect disabled mod + const QFileInfo pathInfo(path); + if (optionalFiles && pathInfo.suffix() == "disabled") { + // rename it + path = pathInfo.dir().filePath(pathInfo.completeBaseName()); + env["client"] = "optional"; + env["server"] = "optional"; + } else { + env["client"] = "required"; + env["server"] = "required"; } + switch (iterator->side) { + case Metadata::ModSide::ClientSide: + env["server"] = "unsupported"; + break; + case Metadata::ModSide::ServerSide: + env["client"] = "unsupported"; + break; + case Metadata::ModSide::UniversalSide: + break; + } + fileOut["env"] = env; fileOut["path"] = path; - fileOut["downloads"] = QJsonArray{ iterator.value().url }; + fileOut["downloads"] = QJsonArray{ iterator->url }; QJsonObject hashes; hashes["sha1"] = value.sha1; diff --git a/launcher/modplatform/modrinth/ModrinthPackExportTask.h b/launcher/modplatform/modrinth/ModrinthPackExportTask.h index 83540dfac..33c42e817 100644 --- a/launcher/modplatform/modrinth/ModrinthPackExportTask.h +++ b/launcher/modplatform/modrinth/ModrinthPackExportTask.h @@ -44,6 +44,7 @@ class ModrinthPackExportTask : public Task { struct ResolvedFile { QString sha1, sha512, url; qint64 size; + Metadata::ModSide side; }; static const QStringList PREFIXES; diff --git a/launcher/modplatform/modrinth/ModrinthPackIndex.cpp b/launcher/modplatform/modrinth/ModrinthPackIndex.cpp index 107b99006..7d0893261 100644 --- a/launcher/modplatform/modrinth/ModrinthPackIndex.cpp +++ b/launcher/modplatform/modrinth/ModrinthPackIndex.cpp @@ -27,6 +27,11 @@ static ModrinthAPI api; static ModPlatform::ProviderCapabilities ProviderCaps; +bool shouldDownloadOnSide(QString side) +{ + return side == "required" || side == "optional"; +} + // https://docs.modrinth.com/api-spec/#tag/projects/operation/getProject void Modrinth::loadIndexedPack(ModPlatform::IndexedPack& pack, QJsonObject& obj) { @@ -53,6 +58,17 @@ void Modrinth::loadIndexedPack(ModPlatform::IndexedPack& pack, QJsonObject& obj) modAuthor.url = api.getAuthorURL(modAuthor.name); pack.authors.append(modAuthor); + auto client = shouldDownloadOnSide(Json::ensureString(obj, "client_side")); + auto server = shouldDownloadOnSide(Json::ensureString(obj, "server_side")); + + if (server && client) { + pack.side = "both"; + } else if (server) { + pack.side = "server"; + } else if (client) { + pack.side = "client"; + } + // Modrinth can have more data than what's provided by the basic search :) pack.extraDataLoaded = false; } @@ -149,6 +165,8 @@ auto Modrinth::loadIndexedPackVersion(QJsonObject& obj, QString preferred_hash_t } file.version = Json::requireString(obj, "name"); file.version_number = Json::requireString(obj, "version_number"); + file.version_type = ModPlatform::IndexedVersionType(Json::requireString(obj, "version_type")); + file.changelog = Json::requireString(obj, "changelog"); auto dependencies = Json::ensureArray(obj, "dependencies"); diff --git a/launcher/modplatform/modrinth/ModrinthPackManifest.cpp b/launcher/modplatform/modrinth/ModrinthPackManifest.cpp index 0d07c6361..d86f1c0e5 100644 --- a/launcher/modplatform/modrinth/ModrinthPackManifest.cpp +++ b/launcher/modplatform/modrinth/ModrinthPackManifest.cpp @@ -35,6 +35,7 @@ */ #include "ModrinthPackManifest.h" +#include #include "Json.h" #include "modplatform/modrinth/ModrinthAPI.h" @@ -56,8 +57,8 @@ void loadIndexedPack(Modpack& pack, QJsonObject& obj) pack.description = Json::ensureString(obj, "description"); auto temp_author_name = Json::ensureString(obj, "author"); pack.author = std::make_tuple(temp_author_name, api.getAuthorURL(temp_author_name)); - pack.iconName = QString("modrinth_%1").arg(Json::ensureString(obj, "slug")); pack.iconUrl = Json::ensureString(obj, "icon_url"); + pack.iconName = QString("modrinth_%1.%2").arg(Json::ensureString(obj, "slug"), QFileInfo(pack.iconUrl.fileName()).suffix()); } void loadIndexedInfo(Modpack& pack, QJsonObject& obj) @@ -128,6 +129,7 @@ auto loadIndexedVersion(QJsonObject& obj) -> ModpackVersion file.name = Json::requireString(obj, "name"); file.version = Json::requireString(obj, "version_number"); + file.version_type = ModPlatform::IndexedVersionType(Json::requireString(obj, "version_type")); file.changelog = Json::ensureString(obj, "changelog"); file.id = Json::requireString(obj, "id"); diff --git a/launcher/modplatform/modrinth/ModrinthPackManifest.h b/launcher/modplatform/modrinth/ModrinthPackManifest.h index effa1a84a..6fc55a043 100644 --- a/launcher/modplatform/modrinth/ModrinthPackManifest.h +++ b/launcher/modplatform/modrinth/ModrinthPackManifest.h @@ -45,6 +45,8 @@ #include #include +#include "modplatform/ModIndex.h" + class MinecraftInstance; namespace Modrinth { @@ -55,6 +57,7 @@ struct File { QCryptographicHash::Algorithm hashAlgorithm; QByteArray hash; QQueue downloads; + bool required = true; }; struct DonationData { @@ -79,6 +82,7 @@ struct ModpackExtra { struct ModpackVersion { QString name; QString version; + ModPlatform::IndexedVersionType version_type; QString changelog; QString id; diff --git a/launcher/modplatform/packwiz/Packwiz.cpp b/launcher/modplatform/packwiz/Packwiz.cpp index 71f66bf3e..e35567f24 100644 --- a/launcher/modplatform/packwiz/Packwiz.cpp +++ b/launcher/modplatform/packwiz/Packwiz.cpp @@ -21,6 +21,8 @@ #include #include #include +#include +#include #include "FileSystem.h" #include "StringUtils.h" @@ -111,6 +113,7 @@ auto V1::createModFormat([[maybe_unused]] QDir& index_dir, ModPlatform::IndexedP mod.provider = mod_pack.provider; mod.file_id = mod_version.fileId; mod.project_id = mod_pack.addonId; + mod.side = stringToSide(mod_pack.side); return mod; } @@ -154,38 +157,52 @@ void V1::updateModIndex(QDir& index_dir, Mod& mod) FS::ensureFilePathExists(index_file.fileName()); } + toml::table update; + switch (mod.provider) { + case (ModPlatform::ResourceProvider::FLAME): + if (mod.file_id.toInt() == 0 || mod.project_id.toInt() == 0) { + qCritical() << QString("Did not write file %1 because missing information!").arg(normalized_fname); + return; + } + update = toml::table{ + { "file-id", mod.file_id.toInt() }, + { "project-id", mod.project_id.toInt() }, + }; + break; + case (ModPlatform::ResourceProvider::MODRINTH): + if (mod.mod_id().toString().isEmpty() || mod.version().toString().isEmpty()) { + qCritical() << QString("Did not write file %1 because missing information!").arg(normalized_fname); + return; + } + update = toml::table{ + { "mod-id", mod.mod_id().toString().toStdString() }, + { "version", mod.version().toString().toStdString() }, + }; + break; + } + if (!index_file.open(QIODevice::ReadWrite)) { - qCritical() << QString("Could not open file %1!").arg(indexFileName(mod.name)); + qCritical() << QString("Could not open file %1!").arg(normalized_fname); return; } // Put TOML data into the file QTextStream in_stream(&index_file); - auto addToStream = [&in_stream](QString&& key, QString value) { in_stream << QString("%1 = \"%2\"\n").arg(key, value); }; - { - addToStream("name", mod.name); - addToStream("filename", mod.filename); - addToStream("side", mod.side); - - in_stream << QString("\n[download]\n"); - addToStream("mode", mod.mode); - addToStream("url", mod.url.toString()); - addToStream("hash-format", mod.hash_format); - addToStream("hash", mod.hash); - - in_stream << QString("\n[update]\n"); - in_stream << QString("[update.%1]\n").arg(ProviderCaps.name(mod.provider)); - switch (mod.provider) { - case (ModPlatform::ResourceProvider::FLAME): - in_stream << QString("file-id = %1\n").arg(mod.file_id.toString()); - in_stream << QString("project-id = %1\n").arg(mod.project_id.toString()); - break; - case (ModPlatform::ResourceProvider::MODRINTH): - addToStream("mod-id", mod.mod_id().toString()); - addToStream("version", mod.version().toString()); - break; - } + auto tbl = toml::table{ { "name", mod.name.toStdString() }, + { "filename", mod.filename.toStdString() }, + { "side", sideToString(mod.side).toStdString() }, + { "download", + toml::table{ + { "mode", mod.mode.toStdString() }, + { "url", mod.url.toString().toStdString() }, + { "hash-format", mod.hash_format.toStdString() }, + { "hash", mod.hash.toStdString() }, + } }, + { "update", toml::table{ { ProviderCaps.name(mod.provider), update } } } }; + std::stringstream ss; + ss << tbl; + in_stream << QString::fromStdString(ss.str()); } index_file.flush(); @@ -258,7 +275,7 @@ auto V1::getIndexForMod(QDir& index_dir, QString slug) -> Mod { // Basic info mod.name = stringEntry(table, "name"); mod.filename = stringEntry(table, "filename"); - mod.side = stringEntry(table, "side"); + mod.side = stringToSide(stringEntry(table, "side")); } { // [download] info @@ -313,4 +330,28 @@ auto V1::getIndexForMod(QDir& index_dir, QVariant& mod_id) -> Mod return {}; } +auto V1::sideToString(Side side) -> QString +{ + switch (side) { + case Side::ClientSide: + return "client"; + case Side::ServerSide: + return "server"; + case Side::UniversalSide: + return "both"; + } + return {}; +} + +auto V1::stringToSide(QString side) -> Side +{ + if (side == "client") + return Side::ClientSide; + if (side == "server") + return Side::ServerSide; + if (side == "both") + return Side::UniversalSide; + return Side::UniversalSide; +} + } // namespace Packwiz diff --git a/launcher/modplatform/packwiz/Packwiz.h b/launcher/modplatform/packwiz/Packwiz.h index 7edc18cde..dce198b0e 100644 --- a/launcher/modplatform/packwiz/Packwiz.h +++ b/launcher/modplatform/packwiz/Packwiz.h @@ -35,12 +35,12 @@ auto getRealIndexName(QDir& index_dir, QString normalized_index_name, bool shoul class V1 { public: + enum class Side { ClientSide = 1 << 0, ServerSide = 1 << 1, UniversalSide = ClientSide | ServerSide }; struct Mod { QString slug{}; QString name{}; QString filename{}; - // FIXME: make side an enum - QString side{ "both" }; + Side side{ Side::UniversalSide }; // [download] QString mode{}; @@ -93,6 +93,9 @@ class V1 { * If the mod doesn't have a metadata, it simply returns an empty Mod object. * */ static auto getIndexForMod(QDir& index_dir, QVariant& mod_id) -> Mod; + + static auto sideToString(Side side) -> QString; + static auto stringToSide(QString side) -> Side; }; } // namespace Packwiz diff --git a/launcher/net/Download.cpp b/launcher/net/Download.cpp index d25447b2d..bae364f12 100644 --- a/launcher/net/Download.cpp +++ b/launcher/net/Download.cpp @@ -89,4 +89,4 @@ QNetworkReply* Download::getReply(QNetworkRequest& request) { return m_network->get(request); } -} // namespace Net \ No newline at end of file +} // namespace Net diff --git a/launcher/net/NetJob.cpp b/launcher/net/NetJob.cpp index dede3e62e..27f690497 100644 --- a/launcher/net/NetJob.cpp +++ b/launcher/net/NetJob.cpp @@ -36,13 +36,18 @@ */ #include "NetJob.h" -#include "Application.h" #include "tasks/ConcurrentTask.h" +#if defined(LAUNCHER_APPLICATION) +#include "Application.h" #include "ui/dialogs/CustomMessageBox.h" +#endif -NetJob::NetJob(QString job_name, shared_qobject_ptr network) - : ConcurrentTask(nullptr, job_name, APPLICATION->settings()->get("NumberOfConcurrentDownloads").toInt()), m_network(network) -{} +NetJob::NetJob(QString job_name, shared_qobject_ptr network) : ConcurrentTask(nullptr, job_name), m_network(network) +{ +#if defined(LAUNCHER_APPLICATION) + setMaxConcurrent(APPLICATION->settings()->get("NumberOfConcurrentDownloads").toInt()); +#endif +} auto NetJob::addNetAction(NetAction::Ptr action) -> bool { @@ -140,6 +145,7 @@ void NetJob::updateState() void NetJob::emitFailed(QString reason) { +#if defined(LAUNCHER_APPLICATION) auto response = CustomMessageBox::selectable(nullptr, "Confirm retry", "The tasks failed\n" "Failed urls\n" + @@ -155,5 +161,6 @@ void NetJob::emitFailed(QString reason) startNext(); return; } +#endif ConcurrentTask::emitFailed(reason); } \ No newline at end of file diff --git a/launcher/net/NetRequest.cpp b/launcher/net/NetRequest.cpp index ff59da18b..cb675be1b 100644 --- a/launcher/net/NetRequest.cpp +++ b/launcher/net/NetRequest.cpp @@ -46,6 +46,7 @@ #if defined(LAUNCHER_APPLICATION) #include "Application.h" #endif +#include "BuildConfig.h" #include "net/NetAction.h" diff --git a/launcher/translations/TranslationsModel.cpp b/launcher/translations/TranslationsModel.cpp index 933fe2d35..56ade8e32 100644 --- a/launcher/translations/TranslationsModel.cpp +++ b/launcher/translations/TranslationsModel.cpp @@ -206,7 +206,7 @@ void TranslationsModel::indexReceived() reloadLocalFiles(); auto language = d->m_system_locale; - if (!findLanguage(language)) { + if (!findLanguageAsOptional(language).has_value()) { language = d->m_system_language; } selectLanguage(language); @@ -417,14 +417,17 @@ int TranslationsModel::columnCount([[maybe_unused]] const QModelIndex& parent) c return 2; } -Language* TranslationsModel::findLanguage(const QString& key) +QVector::Iterator TranslationsModel::findLanguage(const QString& key) { - auto found = std::find_if(d->m_languages.begin(), d->m_languages.end(), [&](Language& lang) { return lang.key == key; }); - if (found == d->m_languages.end()) { - return nullptr; - } else { - return found; - } + return std::find_if(d->m_languages.begin(), d->m_languages.end(), [&](Language& lang) { return lang.key == key; }); +} + +std::optional TranslationsModel::findLanguageAsOptional(const QString& key) +{ + auto found = findLanguage(key); + if (found != d->m_languages.end()) + return *found; + return {}; } void TranslationsModel::setUseSystemLocale(bool useSystemLocale) @@ -436,13 +439,13 @@ void TranslationsModel::setUseSystemLocale(bool useSystemLocale) bool TranslationsModel::selectLanguage(QString key) { QString& langCode = key; - auto langPtr = findLanguage(key); + auto langPtr = findLanguageAsOptional(key); if (langCode.isEmpty()) { d->no_language_set = true; } - if (!langPtr) { + if (!langPtr.has_value()) { qWarning() << "Selected invalid language" << key << ", defaulting to" << defaultLangCode; langCode = defaultLangCode; } else { @@ -527,9 +530,8 @@ bool TranslationsModel::selectLanguage(QString key) QModelIndex TranslationsModel::selectedIndex() { auto found = findLanguage(d->m_selectedLanguage); - if (found) { - // QVector iterator freely converts to pointer to contained type - return index(found - d->m_languages.begin(), 0, QModelIndex()); + if (found != d->m_languages.end()) { + return index(std::distance(d->m_languages.begin(), found), 0, QModelIndex()); } return QModelIndex(); } @@ -562,8 +564,8 @@ void TranslationsModel::updateLanguage(QString key) qWarning() << "Cannot update builtin language" << key; return; } - auto found = findLanguage(key); - if (!found) { + auto found = findLanguageAsOptional(key); + if (!found.has_value()) { qWarning() << "Cannot update invalid language" << key; return; } @@ -578,8 +580,8 @@ void TranslationsModel::downloadTranslation(QString key) d->m_nextDownload = key; return; } - auto lang = findLanguage(key); - if (!lang) { + auto lang = findLanguageAsOptional(key); + if (!lang.has_value()) { qWarning() << "Will not download an unknown translation" << key; return; } diff --git a/launcher/translations/TranslationsModel.h b/launcher/translations/TranslationsModel.h index cff23ce74..96a0e9f8b 100644 --- a/launcher/translations/TranslationsModel.h +++ b/launcher/translations/TranslationsModel.h @@ -17,6 +17,7 @@ #include #include +#include struct Language; @@ -40,7 +41,8 @@ class TranslationsModel : public QAbstractListModel { void setUseSystemLocale(bool useSystemLocale); private: - Language* findLanguage(const QString& key); + QVector::Iterator findLanguage(const QString& key); + std::optional findLanguageAsOptional(const QString& key); void reloadLocalFiles(); void downloadTranslation(QString key); void downloadNext(); diff --git a/launcher/ui/MainWindow.cpp b/launcher/ui/MainWindow.cpp index 5e55a5abb..1da982dad 100644 --- a/launcher/ui/MainWindow.cpp +++ b/launcher/ui/MainWindow.cpp @@ -219,7 +219,7 @@ MainWindow::MainWindow(QWidget* parent) : QMainWindow(parent), ui(new Ui::MainWi ui->actionDISCORD->setVisible(!BuildConfig.DISCORD_URL.isEmpty()); ui->actionREDDIT->setVisible(!BuildConfig.SUBREDDIT_URL.isEmpty()); - ui->actionCheckUpdate->setVisible(BuildConfig.UPDATER_ENABLED); + ui->actionCheckUpdate->setVisible(APPLICATION->updaterEnabled()); #ifndef Q_OS_MAC ui->actionAddToPATH->setVisible(false); @@ -363,7 +363,7 @@ MainWindow::MainWindow(QWidget* parent) : QMainWindow(parent), ui(new Ui::MainWi // Shouldn't have to use lambdas here like this, but if I don't, the compiler throws a fit. // Template hell sucks... connect(APPLICATION->accounts().get(), &AccountList::defaultAccountChanged, [this] { defaultAccountChanged(); }); - connect(APPLICATION->accounts().get(), &AccountList::listChanged, [this] { repopulateAccountsMenu(); }); + connect(APPLICATION->accounts().get(), &AccountList::listChanged, [this] { defaultAccountChanged(); }); // Show initial account defaultAccountChanged(); @@ -377,7 +377,7 @@ MainWindow::MainWindow(QWidget* parent) : QMainWindow(parent), ui(new Ui::MainWi updateNewsLabel(); } - if (BuildConfig.UPDATER_ENABLED) { + if (APPLICATION->updaterEnabled()) { bool updatesAllowed = APPLICATION->updatesAreAllowed(); updatesAllowedChanged(updatesAllowed); @@ -514,10 +514,10 @@ void MainWindow::showInstanceContextMenu(const QPoint& pos) } else { auto group = view->groupNameAt(pos); - QAction* actionVoid = new QAction(BuildConfig.LAUNCHER_DISPLAYNAME, this); + QAction* actionVoid = new QAction(group.isNull() ? BuildConfig.LAUNCHER_DISPLAYNAME : group, this); actionVoid->setEnabled(false); - QAction* actionCreateInstance = new QAction(tr("Create instance"), this); + QAction* actionCreateInstance = new QAction(tr("&Create instance"), this); actionCreateInstance->setToolTip(ui->actionAddInstance->toolTip()); if (!group.isNull()) { QVariantMap instance_action_data; @@ -531,12 +531,13 @@ void MainWindow::showInstanceContextMenu(const QPoint& pos) actions.prepend(actionVoid); actions.append(actionCreateInstance); if (!group.isNull()) { - QAction* actionDeleteGroup = new QAction(tr("Delete group '%1'").arg(group), this); - QVariantMap delete_group_action_data; - delete_group_action_data["group"] = group; - actionDeleteGroup->setData(delete_group_action_data); - connect(actionDeleteGroup, SIGNAL(triggered(bool)), SLOT(deleteGroup())); + QAction* actionDeleteGroup = new QAction(tr("&Delete group"), this); + connect(actionDeleteGroup, &QAction::triggered, this, [this, group] { deleteGroup(group); }); actions.append(actionDeleteGroup); + + QAction* actionRenameGroup = new QAction(tr("&Rename group"), this); + connect(actionRenameGroup, &QAction::triggered, this, [this, group] { renameGroup(group); }); + actions.append(actionRenameGroup); } } QMenu myMenu; @@ -676,7 +677,7 @@ void MainWindow::repopulateAccountsMenu() void MainWindow::updatesAllowedChanged(bool allowed) { - if (!BuildConfig.UPDATER_ENABLED) { + if (!APPLICATION->updaterEnabled()) { return; } ui->actionCheckUpdate->setEnabled(allowed); @@ -872,7 +873,7 @@ 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 Microsoft or Mojang account."), + "one account added.\nPlease add a Microsoft account."), QMessageBox::Warning) ->show(); } @@ -1128,40 +1129,49 @@ void MainWindow::on_actionChangeInstGroup_triggered() if (!m_selectedInstance) return; - bool ok = false; InstanceId instId = m_selectedInstance->id(); - QString name(APPLICATION->instances()->getInstanceGroup(instId)); - auto groups = APPLICATION->instances()->getGroups(); - groups.insert(0, ""); - groups.sort(Qt::CaseInsensitive); - int foo = groups.indexOf(name); + QString src(APPLICATION->instances()->getInstanceGroup(instId)); + + QStringList groups = APPLICATION->instances()->getGroups(); + groups.prepend(""); + int index = groups.indexOf(src); + bool ok = false; + QString dst = QInputDialog::getItem(this, tr("Group name"), tr("Enter a new group name."), groups, index, true, &ok); + dst = dst.simplified(); - name = QInputDialog::getItem(this, tr("Group name"), tr("Enter a new group name."), groups, foo, true, &ok); - name = name.simplified(); if (ok) { - APPLICATION->instances()->setInstanceGroup(instId, name); + APPLICATION->instances()->setInstanceGroup(instId, dst); } } -void MainWindow::deleteGroup() +void MainWindow::deleteGroup(QString group) { - QObject* obj = sender(); - if (!obj) + Q_ASSERT(!group.isEmpty()); + + const int reply = QMessageBox::question(this, tr("Delete group"), tr("Are you sure you want to delete the group '%1'?").arg(group), + QMessageBox::Yes | QMessageBox::No); + if (reply == QMessageBox::Yes) + APPLICATION->instances()->deleteGroup(group); +} + +void MainWindow::renameGroup(QString group) +{ + Q_ASSERT(!group.isEmpty()); + + QString name = QInputDialog::getText(this, tr("Rename group"), tr("Enter a new group name."), QLineEdit::Normal, group); + name = name.simplified(); + if (name.isNull() || name == group) return; - QAction* action = qobject_cast(obj); - if (!action) + + const bool empty = name.isEmpty(); + const bool duplicate = APPLICATION->instances()->getGroups().contains(name, Qt::CaseInsensitive) && group.toLower() != name.toLower(); + + if (empty || duplicate) { + QMessageBox::warning(this, tr("Cannot rename group"), empty ? tr("Cannot set empty name.") : tr("Group already exists. :/")); return; - auto map = action->data().toMap(); - if (!map.contains("group")) - return; - QString groupName = map["group"].toString(); - if (!groupName.isEmpty()) { - auto reply = QMessageBox::question(this, tr("Delete group"), tr("Are you sure you want to delete the group %1?").arg(groupName), - QMessageBox::Yes | QMessageBox::No); - if (reply == QMessageBox::Yes) { - APPLICATION->instances()->deleteGroup(groupName); - } } + + APPLICATION->instances()->renameGroup(group, name); } void MainWindow::undoTrashInstance() @@ -1208,7 +1218,7 @@ void MainWindow::refreshInstances() void MainWindow::checkForUpdates() { - if (BuildConfig.UPDATER_ENABLED) { + if (APPLICATION->updaterEnabled()) { APPLICATION->triggerUpdateCheck(); } else { qWarning() << "Updater not set up. Cannot check for updates."; diff --git a/launcher/ui/MainWindow.h b/launcher/ui/MainWindow.h index 0b6144522..0b7287404 100644 --- a/launcher/ui/MainWindow.h +++ b/launcher/ui/MainWindow.h @@ -148,7 +148,8 @@ class MainWindow : public QMainWindow { void on_actionDeleteInstance_triggered(); - void deleteGroup(); + void deleteGroup(QString group); + void renameGroup(QString group); void undoTrashInstance(); inline void on_actionExportInstance_triggered() { on_actionExportInstanceZip_triggered(); } diff --git a/launcher/ui/dialogs/BlockedModsDialog.cpp b/launcher/ui/dialogs/BlockedModsDialog.cpp index 5a1a2f80e..7a5a16818 100644 --- a/launcher/ui/dialogs/BlockedModsDialog.cpp +++ b/launcher/ui/dialogs/BlockedModsDialog.cpp @@ -41,8 +41,8 @@ #include #include -BlockedModsDialog::BlockedModsDialog(QWidget* parent, const QString& title, const QString& text, QList& mods) - : QDialog(parent), ui(new Ui::BlockedModsDialog), m_mods(mods) +BlockedModsDialog::BlockedModsDialog(QWidget* parent, const QString& title, const QString& text, QList& mods, QString hash_type) + : QDialog(parent), ui(new Ui::BlockedModsDialog), m_mods(mods), m_hash_type(hash_type) { m_hashing_task = shared_qobject_ptr( new ConcurrentTask(this, "MakeHashesTask", APPLICATION->settings()->get("NumberOfConcurrentTasks").toInt())); @@ -255,7 +255,7 @@ void BlockedModsDialog::addHashTask(QString path) /// @param path the path to the local file being hashed void BlockedModsDialog::buildHashTask(QString path) { - auto hash_task = Hashing::createBlockedModHasher(path, ModPlatform::ResourceProvider::FLAME, "sha1"); + auto hash_task = Hashing::createBlockedModHasher(path, ModPlatform::ResourceProvider::FLAME, m_hash_type); qDebug() << "[Blocked Mods Dialog] Creating Hash task for path: " << path; @@ -335,6 +335,13 @@ bool BlockedModsDialog::checkValidPath(QString path) for (auto& mod : m_mods) { if (compare(filename, mod.name)) { + // if the mod is not yet matched and doesn't have a hash then + // just match it with the file that has the exact same name + if (!mod.matched && mod.hash.isEmpty()) { + mod.matched = true; + mod.localPath = path; + return false; + } qDebug() << "[Blocked Mods Dialog] Name match found:" << mod.name << "| From path:" << path; return true; } diff --git a/launcher/ui/dialogs/BlockedModsDialog.h b/launcher/ui/dialogs/BlockedModsDialog.h index e3b7c9756..09722bce9 100644 --- a/launcher/ui/dialogs/BlockedModsDialog.h +++ b/launcher/ui/dialogs/BlockedModsDialog.h @@ -54,7 +54,7 @@ class BlockedModsDialog : public QDialog { Q_OBJECT public: - BlockedModsDialog(QWidget* parent, const QString& title, const QString& text, QList& mods); + BlockedModsDialog(QWidget* parent, const QString& title, const QString& text, QList& mods, QString hash_type = "sha1"); ~BlockedModsDialog() override; @@ -73,6 +73,7 @@ class BlockedModsDialog : public QDialog { QSet m_pending_hash_paths; bool m_rehash_pending; QPushButton* m_openMissingButton; + QString m_hash_type; void openAll(bool missingOnly); void addDownloadFolder(); diff --git a/launcher/ui/dialogs/CopyInstanceDialog.cpp b/launcher/ui/dialogs/CopyInstanceDialog.cpp index 8b2f5bb80..770741a61 100644 --- a/launcher/ui/dialogs/CopyInstanceDialog.cpp +++ b/launcher/ui/dialogs/CopyInstanceDialog.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 @@ -61,22 +62,14 @@ CopyInstanceDialog::CopyInstanceDialog(InstancePtr original, QWidget* parent) ui->iconButton->setIcon(APPLICATION->icons()->getIcon(InstIconKey)); ui->instNameTextBox->setText(original->name()); ui->instNameTextBox->setFocus(); -#if QT_VERSION >= QT_VERSION_CHECK(5, 14, 0) - auto groupList = APPLICATION->instances()->getGroups(); - QSet groups(groupList.begin(), groupList.end()); - groupList = QStringList(groups.values()); -#else - auto groups = APPLICATION->instances()->getGroups().toSet(); - auto groupList = QStringList(groups.toList()); -#endif - groupList.sort(Qt::CaseInsensitive); - groupList.removeOne(""); - groupList.push_front(""); - ui->groupBox->addItems(groupList); - int index = groupList.indexOf(APPLICATION->instances()->getInstanceGroup(m_original->id())); - if (index == -1) { + + QStringList groups = APPLICATION->instances()->getGroups(); + groups.prepend(""); + ui->groupBox->addItems(groups); + int index = groups.indexOf(APPLICATION->instances()->getInstanceGroup(m_original->id())); + if (index == -1) index = 0; - } + ui->groupBox->setCurrentIndex(index); ui->groupBox->lineEdit()->setPlaceholderText(tr("No group")); ui->copySavesCheckbox->setChecked(m_selectedOptions.isCopySavesEnabled()); diff --git a/launcher/ui/dialogs/LoginDialog.cpp b/launcher/ui/dialogs/LoginDialog.cpp deleted file mode 100644 index 7296a13ef..000000000 --- a/launcher/ui/dialogs/LoginDialog.cpp +++ /dev/null @@ -1,115 +0,0 @@ -/* Copyright 2013-2021 MultiMC Contributors - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -#include "LoginDialog.h" -#include "ui_LoginDialog.h" - -#include "minecraft/auth/AccountTask.h" - -#include - -LoginDialog::LoginDialog(QWidget* parent) : QDialog(parent), ui(new Ui::LoginDialog) -{ - ui->setupUi(this); - ui->progressBar->setVisible(false); - ui->buttonBox->button(QDialogButtonBox::Ok)->setEnabled(false); - - connect(ui->buttonBox, &QDialogButtonBox::accepted, this, &QDialog::accept); - connect(ui->buttonBox, &QDialogButtonBox::rejected, this, &QDialog::reject); -} - -LoginDialog::~LoginDialog() -{ - delete ui; -} - -// Stage 1: User interaction -void LoginDialog::accept() -{ - setUserInputsEnabled(false); - ui->progressBar->setVisible(true); - - // Setup the login task and start it - m_account = MinecraftAccount::createFromUsername(ui->userTextBox->text()); - m_loginTask = m_account->login(ui->passTextBox->text()); - connect(m_loginTask.get(), &Task::failed, this, &LoginDialog::onTaskFailed); - connect(m_loginTask.get(), &Task::succeeded, this, &LoginDialog::onTaskSucceeded); - connect(m_loginTask.get(), &Task::status, this, &LoginDialog::onTaskStatus); - connect(m_loginTask.get(), &Task::progress, this, &LoginDialog::onTaskProgress); - m_loginTask->start(); -} - -void LoginDialog::setUserInputsEnabled(bool enable) -{ - ui->userTextBox->setEnabled(enable); - ui->passTextBox->setEnabled(enable); - ui->buttonBox->setEnabled(enable); -} - -// Enable the OK button only when both textboxes contain something. -void LoginDialog::on_userTextBox_textEdited(const QString& newText) -{ - ui->buttonBox->button(QDialogButtonBox::Ok)->setEnabled(!newText.isEmpty() && !ui->passTextBox->text().isEmpty()); -} -void LoginDialog::on_passTextBox_textEdited(const QString& newText) -{ - ui->buttonBox->button(QDialogButtonBox::Ok)->setEnabled(!newText.isEmpty() && !ui->userTextBox->text().isEmpty()); -} - -void LoginDialog::onTaskFailed(const QString& reason) -{ - // Set message - auto lines = reason.split('\n'); - QString processed; - for (auto line : lines) { - if (line.size()) { - processed += "" + line + "
"; - } else { - processed += "
"; - } - } - ui->label->setText(processed); - - // Re-enable user-interaction - setUserInputsEnabled(true); - ui->progressBar->setVisible(false); -} - -void LoginDialog::onTaskSucceeded() -{ - QDialog::accept(); -} - -void LoginDialog::onTaskStatus(const QString& status) -{ - ui->label->setText(status); -} - -void LoginDialog::onTaskProgress(qint64 current, qint64 total) -{ - ui->progressBar->setMaximum(total); - ui->progressBar->setValue(current); -} - -// Public interface -MinecraftAccountPtr LoginDialog::newAccount(QWidget* parent, QString msg) -{ - LoginDialog dlg(parent); - dlg.ui->label->setText(msg); - if (dlg.exec() == QDialog::Accepted) { - return dlg.m_account; - } - return nullptr; -} diff --git a/launcher/ui/dialogs/LoginDialog.h b/launcher/ui/dialogs/LoginDialog.h deleted file mode 100644 index 601b5fa77..000000000 --- a/launcher/ui/dialogs/LoginDialog.h +++ /dev/null @@ -1,56 +0,0 @@ -/* Copyright 2013-2021 MultiMC Contributors - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -#pragma once - -#include -#include - -#include "minecraft/auth/MinecraftAccount.h" -#include "tasks/Task.h" - -namespace Ui { -class LoginDialog; -} - -class LoginDialog : public QDialog { - Q_OBJECT - - public: - ~LoginDialog(); - - static MinecraftAccountPtr newAccount(QWidget* parent, QString message); - - private: - explicit LoginDialog(QWidget* parent = 0); - - void setUserInputsEnabled(bool enable); - - protected slots: - void accept(); - - void onTaskFailed(const QString& reason); - void onTaskSucceeded(); - void onTaskStatus(const QString& status); - void onTaskProgress(qint64 current, qint64 total); - - void on_userTextBox_textEdited(const QString& newText); - void on_passTextBox_textEdited(const QString& newText); - - private: - Ui::LoginDialog* ui; - MinecraftAccountPtr m_account; - Task::Ptr m_loginTask; -}; diff --git a/launcher/ui/dialogs/LoginDialog.ui b/launcher/ui/dialogs/LoginDialog.ui deleted file mode 100644 index 8fa4a45d2..000000000 --- a/launcher/ui/dialogs/LoginDialog.ui +++ /dev/null @@ -1,77 +0,0 @@ - - - LoginDialog - - - - 0 - 0 - 421 - 198 - - - - - 0 - 0 - - - - Add Account - - - - - - Message label placeholder. - - - Qt::RichText - - - Qt::LinksAccessibleByKeyboard|Qt::LinksAccessibleByMouse|Qt::TextBrowserInteraction|Qt::TextSelectableByKeyboard|Qt::TextSelectableByMouse - - - - - - - Email - - - - - - - QLineEdit::Password - - - Password - - - - - - - 24 - - - false - - - - - - - Qt::Horizontal - - - QDialogButtonBox::Cancel|QDialogButtonBox::Ok - - - - - - - - diff --git a/launcher/ui/dialogs/ModUpdateDialog.cpp b/launcher/ui/dialogs/ModUpdateDialog.cpp index ee64b392d..1a70ea59a 100644 --- a/launcher/ui/dialogs/ModUpdateDialog.cpp +++ b/launcher/ui/dialogs/ModUpdateDialog.cpp @@ -219,8 +219,10 @@ void ModUpdateDialog::checkCandidates() if (dep->pack->provider == ModPlatform::ResourceProvider::FLAME) changelog = api.getModFileChangelog(dep->version.addonId.toInt(), dep->version.fileId.toInt()); auto download_task = makeShared(dep->pack, dep->version, m_mod_model); - CheckUpdateTask::UpdatableMod updatable = { dep->pack->name, dep->version.hash, "", dep->version.version, - changelog, dep->pack->provider, download_task }; + CheckUpdateTask::UpdatableMod updatable = { + dep->pack->name, dep->version.hash, "", dep->version.version, dep->version.version_type, + changelog, dep->pack->provider, download_task + }; appendMod(updatable, getRequiredBy.value(dep->version.addonId.toString())); m_tasks.insert(updatable.name, updatable.download); @@ -415,6 +417,11 @@ void ModUpdateDialog::appendMod(CheckUpdateTask::UpdatableMod const& info, QStri auto new_version_item = new QTreeWidgetItem(item_top); new_version_item->setText(0, tr("New version: %1").arg(info.new_version)); + if (info.new_version_type.has_value()) { + auto new_version_type_itme = new QTreeWidgetItem(item_top); + new_version_type_itme->setText(0, tr("New Version Type: %1").arg(info.new_version_type.value().toString())); + } + if (!requiredBy.isEmpty()) { auto requiredByItem = new QTreeWidgetItem(item_top); if (requiredBy.length() == 1) { diff --git a/launcher/ui/dialogs/NewInstanceDialog.cpp b/launcher/ui/dialogs/NewInstanceDialog.cpp index 9613c6b00..6f3f7f7ec 100644 --- a/launcher/ui/dialogs/NewInstanceDialog.cpp +++ b/launcher/ui/dialogs/NewInstanceDialog.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 @@ -75,23 +76,14 @@ NewInstanceDialog::NewInstanceDialog(const QString& initialGroup, InstIconKey = "default"; ui->iconButton->setIcon(APPLICATION->icons()->getIcon(InstIconKey)); -#if QT_VERSION >= QT_VERSION_CHECK(5, 14, 0) - auto groupList = APPLICATION->instances()->getGroups(); - auto groups = QSet(groupList.begin(), groupList.end()); - groupList = groups.values(); -#else - auto groups = APPLICATION->instances()->getGroups().toSet(); - auto groupList = QStringList(groups.toList()); -#endif - groupList.sort(Qt::CaseInsensitive); - groupList.removeOne(""); - groupList.push_front(initialGroup); - groupList.push_front(""); - ui->groupBox->addItems(groupList); - int index = groupList.indexOf(initialGroup); + QStringList groups = APPLICATION->instances()->getGroups(); + groups.prepend(""); + int index = groups.indexOf(initialGroup); if (index == -1) { - index = 0; + index = 1; + groups.insert(index, initialGroup); } + ui->groupBox->addItems(groups); ui->groupBox->setCurrentIndex(index); ui->groupBox->lineEdit()->setPlaceholderText(tr("No group")); @@ -237,8 +229,7 @@ void NewInstanceDialog::setSuggestedIcon(const QString& key) InstanceTask* NewInstanceDialog::extractTask() { - InstanceTask* extracted = creationTask.get(); - creationTask.release(); + InstanceTask* extracted = creationTask.release(); InstanceName inst_name(ui->instNameTextBox->placeholderText().trimmed(), importVersion); inst_name.setName(ui->instNameTextBox->text().trimmed()); diff --git a/launcher/ui/dialogs/ProgressDialog.ui b/launcher/ui/dialogs/ProgressDialog.ui index a4d08124c..156ff247f 100644 --- a/launcher/ui/dialogs/ProgressDialog.ui +++ b/launcher/ui/dialogs/ProgressDialog.ui @@ -48,6 +48,9 @@ Global Task Status... + + true + @@ -109,8 +112,8 @@ 0 0 - 464 - 96 + 460 + 108 diff --git a/launcher/ui/dialogs/ResourceDownloadDialog.cpp b/launcher/ui/dialogs/ResourceDownloadDialog.cpp index c40b15469..dc7cfff06 100644 --- a/launcher/ui/dialogs/ResourceDownloadDialog.cpp +++ b/launcher/ui/dialogs/ResourceDownloadDialog.cpp @@ -167,8 +167,8 @@ void ResourceDownloadDialog::confirm() }); for (auto& task : selected) { confirm_dialog->appendResource({ task->getName(), task->getFilename(), task->getCustomPath(), - ProviderCaps.name(task->getProvider()), - getRequiredBy.value(task->getPack()->addonId.toString()) }); + ProviderCaps.name(task->getProvider()), getRequiredBy.value(task->getPack()->addonId.toString()), + task->getVersion().version_type.toString() }); } if (confirm_dialog->exec()) { diff --git a/launcher/ui/dialogs/ReviewMessageBox.cpp b/launcher/ui/dialogs/ReviewMessageBox.cpp index 78c2542fd..aa668f8c2 100644 --- a/launcher/ui/dialogs/ReviewMessageBox.cpp +++ b/launcher/ui/dialogs/ReviewMessageBox.cpp @@ -77,6 +77,10 @@ void ReviewMessageBox::appendResource(ResourceInformation&& info) itemTop->insertChildren(childIndx++, { requiredByItem }); } + auto versionTypeItem = new QTreeWidgetItem(itemTop); + versionTypeItem->setText(0, tr("Version Type: %1").arg(info.version_type)); + itemTop->insertChildren(childIndx++, { versionTypeItem }); + ui->modTreeWidget->addTopLevelItem(itemTop); } diff --git a/launcher/ui/dialogs/ReviewMessageBox.h b/launcher/ui/dialogs/ReviewMessageBox.h index a520cc2a6..596f39c8e 100644 --- a/launcher/ui/dialogs/ReviewMessageBox.h +++ b/launcher/ui/dialogs/ReviewMessageBox.h @@ -18,6 +18,7 @@ class ReviewMessageBox : public QDialog { QString custom_file_path{}; QString provider; QStringList required_by; + QString version_type; }; void appendResource(ResourceInformation&& info); diff --git a/launcher/ui/dialogs/UpdateAvailableDialog.cpp b/launcher/ui/dialogs/UpdateAvailableDialog.cpp new file mode 100644 index 000000000..5eebe87a3 --- /dev/null +++ b/launcher/ui/dialogs/UpdateAvailableDialog.cpp @@ -0,0 +1,63 @@ +// SPDX-FileCopyrightText: 2023 Rachel Powers <508861+Ryex@users.noreply.github.com> +// +// 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 "UpdateAvailableDialog.h" +#include +#include "Application.h" +#include "BuildConfig.h" +#include "Markdown.h" +#include "ui_UpdateAvailableDialog.h" + +UpdateAvailableDialog::UpdateAvailableDialog(const QString& currentVersion, + const QString& availableVersion, + const QString& releaseNotes, + QWidget* parent) + : QDialog(parent), ui(new Ui::UpdateAvailableDialog) +{ + ui->setupUi(this); + + QString launcherName = BuildConfig.LAUNCHER_DISPLAYNAME; + + ui->headerLabel->setText(tr("A new version of %1 is available!").arg(launcherName)); + ui->versionAvailableLabel->setText( + tr("Version %1 is now available - you have %2 . Would you like to download it now?").arg(availableVersion).arg(currentVersion)); + ui->icon->setPixmap(APPLICATION->getThemedIcon("checkupdate").pixmap(64)); + + auto releaseNotesHtml = markdownToHTML(releaseNotes); + ui->releaseNotes->setHtml(releaseNotesHtml); + ui->releaseNotes->setOpenExternalLinks(true); + + connect(ui->skipButton, &QPushButton::clicked, this, [this]() { + setResult(ResultCode::Skip); + done(ResultCode::Skip); + }); + + connect(ui->delayButton, &QPushButton::clicked, this, [this]() { + setResult(ResultCode::DontInstall); + done(ResultCode::DontInstall); + }); + + connect(ui->installButton, &QPushButton::clicked, this, [this]() { + setResult(ResultCode::Install); + done(ResultCode::Install); + }); +} diff --git a/launcher/ui/dialogs/UpdateAvailableDialog.h b/launcher/ui/dialogs/UpdateAvailableDialog.h new file mode 100644 index 000000000..6af9ace36 --- /dev/null +++ b/launcher/ui/dialogs/UpdateAvailableDialog.h @@ -0,0 +1,48 @@ +// SPDX-FileCopyrightText: 2023 Rachel Powers <508861+Ryex@users.noreply.github.com> +// +// 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 + +namespace Ui { +class UpdateAvailableDialog; +} + +class UpdateAvailableDialog : public QDialog { + Q_OBJECT + + public: + enum ResultCode { + Install = 10, + DontInstall = 11, + Skip = 12, + }; + + explicit UpdateAvailableDialog(const QString& currentVersion, + const QString& availableVersion, + const QString& releaseNotes, + QWidget* parent = 0); + ~UpdateAvailableDialog() = default; + + private: + Ui::UpdateAvailableDialog* ui; +}; diff --git a/launcher/ui/dialogs/UpdateAvailableDialog.ui b/launcher/ui/dialogs/UpdateAvailableDialog.ui new file mode 100644 index 000000000..b0d85f6f0 --- /dev/null +++ b/launcher/ui/dialogs/UpdateAvailableDialog.ui @@ -0,0 +1,155 @@ + + + UpdateAvailableDialog + + + + 0 + 0 + 636 + 352 + + + + Update Available + + + + + + + + + + + 64 + 64 + + + + + + + + + + + Qt::Vertical + + + + 20 + 40 + + + + + + + + + + 9 + + + 9 + + + 9 + + + 9 + + + + + + 11 + 75 + true + + + + A new version is available! + + + + + + + Version %1 is now available - you have %2 . Would you like to download it now? + + + + + + + + 75 + true + + + + Release Notes: + + + + + + + + + + + + + + + + Skip This Version + + + + + + + Qt::Horizontal + + + + 40 + 20 + + + + + + + + Remind Me Later + + + false + + + false + + + + + + + Install Update + + + true + + + + + + + + + + diff --git a/launcher/ui/pages/global/AccountListPage.cpp b/launcher/ui/pages/global/AccountListPage.cpp index c95bfabdd..50f3ff2b6 100644 --- a/launcher/ui/pages/global/AccountListPage.cpp +++ b/launcher/ui/pages/global/AccountListPage.cpp @@ -45,7 +45,6 @@ #include "net/NetJob.h" #include "ui/dialogs/CustomMessageBox.h" -#include "ui/dialogs/LoginDialog.h" #include "ui/dialogs/MSALoginDialog.h" #include "ui/dialogs/OfflineLoginDialog.h" #include "ui/dialogs/ProgressDialog.h" @@ -64,8 +63,7 @@ AccountListPage::AccountListPage(QWidget* parent) : QMainWindow(parent), ui(new ui->setupUi(this); ui->listView->setEmptyString( tr("Welcome!\n" - "If you're new here, you can select the \"Add Microsoft\" or \"Add Mojang\" buttons to link your Microsoft and/or Mojang " - "accounts.")); + "If you're new here, you can select the \"Add Microsoft\" button to link your Microsoft account.")); ui->listView->setEmptyMode(VersionListView::String); ui->listView->setContextMenuPolicy(Qt::CustomContextMenu); @@ -74,7 +72,6 @@ AccountListPage::AccountListPage(QWidget* parent) : QMainWindow(parent), ui(new ui->listView->setModel(m_accounts.get()); ui->listView->header()->setSectionResizeMode(AccountList::VListColumns::ProfileNameColumn, QHeaderView::Stretch); ui->listView->header()->setSectionResizeMode(AccountList::VListColumns::NameColumn, QHeaderView::Stretch); - ui->listView->header()->setSectionResizeMode(AccountList::VListColumns::MigrationColumn, QHeaderView::ResizeToContents); ui->listView->header()->setSectionResizeMode(AccountList::VListColumns::TypeColumn, QHeaderView::ResizeToContents); ui->listView->header()->setSectionResizeMode(AccountList::VListColumns::StatusColumn, QHeaderView::ResizeToContents); ui->listView->setSelectionMode(QAbstractItemView::SingleSelection); @@ -139,19 +136,6 @@ void AccountListPage::listChanged() updateButtonStates(); } -void AccountListPage::on_actionAddMojang_triggered() -{ - MinecraftAccountPtr account = - LoginDialog::newAccount(this, tr("Please enter your Mojang account email and password to add your account.")); - - if (account) { - m_accounts->addAccount(account); - if (m_accounts->count() == 1) { - m_accounts->setDefaultAccount(account); - } - } -} - void AccountListPage::on_actionAddMicrosoft_triggered() { MinecraftAccountPtr account = @@ -169,7 +153,7 @@ void AccountListPage::on_actionAddOffline_triggered() { if (!m_accounts->anyAccountIsValid()) { QMessageBox::warning(this, tr("Error"), - tr("You must add a Microsoft or Mojang account that owns Minecraft before you can add an offline account." + tr("You must add a Microsoft account that owns Minecraft before you can add an offline account." "

" "If you have lost your account you can contact Microsoft for support.")); return; diff --git a/launcher/ui/pages/global/AccountListPage.h b/launcher/ui/pages/global/AccountListPage.h index add0f4aa0..f3b80191d 100644 --- a/launcher/ui/pages/global/AccountListPage.h +++ b/launcher/ui/pages/global/AccountListPage.h @@ -70,7 +70,6 @@ class AccountListPage : public QMainWindow, public BasePage { void retranslate() override; public slots: - void on_actionAddMojang_triggered(); void on_actionAddMicrosoft_triggered(); void on_actionAddOffline_triggered(); void on_actionRemove_triggered(); diff --git a/launcher/ui/pages/global/AccountListPage.ui b/launcher/ui/pages/global/AccountListPage.ui index 469955b51..d8cf3ac0a 100644 --- a/launcher/ui/pages/global/AccountListPage.ui +++ b/launcher/ui/pages/global/AccountListPage.ui @@ -53,7 +53,6 @@ false - @@ -63,11 +62,6 @@ - - - Add &Mojang - - Remo&ve diff --git a/launcher/ui/pages/global/MinecraftPage.cpp b/launcher/ui/pages/global/MinecraftPage.cpp index 553cefd83..a9530effc 100644 --- a/launcher/ui/pages/global/MinecraftPage.cpp +++ b/launcher/ui/pages/global/MinecraftPage.cpp @@ -119,6 +119,9 @@ void MinecraftPage::applySettings() // Miscellaneous s->set("CloseAfterLaunch", ui->closeAfterLaunchCheck->isChecked()); s->set("QuitAfterGameStop", ui->quitAfterGameStopCheck->isChecked()); + + // Legacy settings + s->set("OnlineFixes", ui->onlineFixes->isChecked()); } void MinecraftPage::loadSettings() @@ -170,6 +173,8 @@ void MinecraftPage::loadSettings() ui->closeAfterLaunchCheck->setChecked(s->get("CloseAfterLaunch").toBool()); ui->quitAfterGameStopCheck->setChecked(s->get("QuitAfterGameStop").toBool()); + + ui->onlineFixes->setChecked(s->get("OnlineFixes").toBool()); } void MinecraftPage::retranslate() diff --git a/launcher/ui/pages/global/MinecraftPage.ui b/launcher/ui/pages/global/MinecraftPage.ui index b5cfa659f..2a3c0d96d 100644 --- a/launcher/ui/pages/global/MinecraftPage.ui +++ b/launcher/ui/pages/global/MinecraftPage.ui @@ -138,7 +138,7 @@
- + Show time spent playing in hours @@ -197,6 +197,25 @@ Tweaks + + + + Legacy settings + + + + + + <html><head/><body><p>Emulates usages of old online services which are no longer operating.</p><p>This currently allows modern skins to be used.</p></body></html> + + + Enable online fixes (experimental) + + + + + + diff --git a/launcher/ui/pages/instance/InstanceSettingsPage.cpp b/launcher/ui/pages/instance/InstanceSettingsPage.cpp index 7aa6bd322..c0ced2415 100644 --- a/launcher/ui/pages/instance/InstanceSettingsPage.cpp +++ b/launcher/ui/pages/instance/InstanceSettingsPage.cpp @@ -3,6 +3,7 @@ * Prism Launcher - Minecraft Launcher * Copyright (c) 2022 Jamie Mansfield * Copyright (C) 2022 Sefa Eyeoglu + * Copyright (C) 2022 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 @@ -253,6 +254,14 @@ void InstanceSettingsPage::applySettings() m_settings->reset("InstanceAccountId"); } + bool overrideLegacySettings = ui->legacySettingsGroupBox->isChecked(); + m_settings->set("OverrideLegacySettings", overrideLegacySettings); + if (overrideLegacySettings) { + m_settings->set("OnlineFixes", ui->onlineFixes->isChecked()); + } else { + m_settings->reset("OnlineFixes"); + } + // FIXME: This should probably be called by a signal instead m_instance->updateRuntimeContext(); } @@ -356,6 +365,9 @@ void InstanceSettingsPage::loadSettings() ui->instanceAccountGroupBox->setChecked(m_settings->get("UseAccountForInstance").toBool()); updateAccountsMenu(); + + ui->legacySettingsGroupBox->setChecked(m_settings->get("OverrideLegacySettings").toBool()); + ui->onlineFixes->setChecked(m_settings->get("OnlineFixes").toBool()); } void InstanceSettingsPage::on_javaDetectBtn_clicked() diff --git a/launcher/ui/pages/instance/InstanceSettingsPage.ui b/launcher/ui/pages/instance/InstanceSettingsPage.ui index 81cf7093e..8defaccb3 100644 --- a/launcher/ui/pages/instance/InstanceSettingsPage.ui +++ b/launcher/ui/pages/instance/InstanceSettingsPage.ui @@ -583,6 +583,31 @@ Miscellaneous + + + + Legacy settings + + + true + + + false + + + + + + <html><head/><body><p>Emulates usages of old online services which are no longer operating.</p><p>This currently allows modern skins to be used.</p></body></html> + + + Enable online fixes (experimental) + + + + + + diff --git a/launcher/ui/pages/instance/ManagedPackPage.cpp b/launcher/ui/pages/instance/ManagedPackPage.cpp index 8fdaf065d..26a8302db 100644 --- a/launcher/ui/pages/instance/ManagedPackPage.cpp +++ b/launcher/ui/pages/instance/ManagedPackPage.cpp @@ -8,6 +8,7 @@ #include #include "ui_ManagedPackPage.h" +#include #include #include #include @@ -223,6 +224,7 @@ ModrinthManagedPackPage::ModrinthManagedPackPage(BaseInstance* inst, InstanceWin Q_ASSERT(inst->isManagedPack()); connect(ui->versionsComboBox, SIGNAL(currentIndexChanged(int)), this, SLOT(suggestVersion())); connect(ui->updateButton, &QPushButton::clicked, this, &ModrinthManagedPackPage::update); + connect(ui->updateFromFileButton, &QPushButton::clicked, this, &ModrinthManagedPackPage::updateFromFile); } // MODRINTH @@ -350,6 +352,27 @@ void ModrinthManagedPackPage::update() m_instance_window->close(); } +void ModrinthManagedPackPage::updateFromFile() +{ + auto output = QFileDialog::getOpenFileUrl(this, tr("Choose update file"), QDir::homePath(), "Modrinth pack (*.mrpack *.zip)"); + QMap extra_info; + extra_info.insert("pack_id", m_inst->getManagedPackID()); + extra_info.insert("pack_version_id", QString()); + extra_info.insert("original_instance_id", m_inst->id()); + + auto extracted = new InstanceImportTask(output, this, std::move(extra_info)); + + extracted->setName(m_inst->name()); + extracted->setGroup(APPLICATION->instances()->getInstanceGroup(m_inst->id())); + extracted->setIcon(m_inst->iconKey()); + extracted->setConfirmUpdate(false); + + auto did_succeed = runUpdateTask(extracted); + + if (m_instance_window && did_succeed) + m_instance_window->close(); +} + // FLAME FlameManagedPackPage::FlameManagedPackPage(BaseInstance* inst, InstanceWindow* instance_window, QWidget* parent) @@ -358,6 +381,7 @@ FlameManagedPackPage::FlameManagedPackPage(BaseInstance* inst, InstanceWindow* i Q_ASSERT(inst->isManagedPack()); connect(ui->versionsComboBox, SIGNAL(currentIndexChanged(int)), this, SLOT(suggestVersion())); connect(ui->updateButton, &QPushButton::clicked, this, &FlameManagedPackPage::update); + connect(ui->updateFromFileButton, &QPushButton::clicked, this, &FlameManagedPackPage::updateFromFile); } void FlameManagedPackPage::parseManagedPack() @@ -492,4 +516,25 @@ void FlameManagedPackPage::update() m_instance_window->close(); } +void FlameManagedPackPage::updateFromFile() +{ + auto output = QFileDialog::getOpenFileUrl(this, tr("Choose update file"), QDir::homePath(), "CurseForge pack (*.zip)"); + + QMap extra_info; + extra_info.insert("pack_id", m_inst->getManagedPackID()); + extra_info.insert("pack_version_id", QString()); + extra_info.insert("original_instance_id", m_inst->id()); + + auto extracted = new InstanceImportTask(output, this, std::move(extra_info)); + + extracted->setName(m_inst->name()); + extracted->setGroup(APPLICATION->instances()->getInstanceGroup(m_inst->id())); + extracted->setIcon(m_inst->iconKey()); + extracted->setConfirmUpdate(false); + + auto did_succeed = runUpdateTask(extracted); + + if (m_instance_window && did_succeed) + m_instance_window->close(); +} #include "ManagedPackPage.moc" diff --git a/launcher/ui/pages/instance/ManagedPackPage.h b/launcher/ui/pages/instance/ManagedPackPage.h index 1ac6fc038..d77cb97b8 100644 --- a/launcher/ui/pages/instance/ManagedPackPage.h +++ b/launcher/ui/pages/instance/ManagedPackPage.h @@ -65,6 +65,7 @@ class ManagedPackPage : public QWidget, public BasePage { virtual void suggestVersion(); virtual void update(){}; + virtual void updateFromFile(){}; protected slots: /** Does the necessary UI changes for when something failed. @@ -123,6 +124,7 @@ class ModrinthManagedPackPage final : public ManagedPackPage { void suggestVersion() override; void update() override; + void updateFromFile() override; private: NetJob::Ptr m_fetch_job = nullptr; @@ -145,6 +147,7 @@ class FlameManagedPackPage final : public ManagedPackPage { void suggestVersion() override; void update() override; + void updateFromFile() override; private: NetJob::Ptr m_fetch_job = nullptr; diff --git a/launcher/ui/pages/instance/ManagedPackPage.ui b/launcher/ui/pages/instance/ManagedPackPage.ui index 05e91bbca..54ff08e94 100644 --- a/launcher/ui/pages/instance/ManagedPackPage.ui +++ b/launcher/ui/pages/instance/ManagedPackPage.ui @@ -153,6 +153,19 @@ + + + + + 0 + 0 + + + + Update from file + + + diff --git a/launcher/ui/pages/instance/ModFolderPage.cpp b/launcher/ui/pages/instance/ModFolderPage.cpp index 69c34dafe..625d37933 100644 --- a/launcher/ui/pages/instance/ModFolderPage.cpp +++ b/launcher/ui/pages/instance/ModFolderPage.cpp @@ -206,6 +206,14 @@ void ModFolderPage::installMods() void ModFolderPage::updateMods() { + if (m_instance->typeName() != "Minecraft") + return; // this is a null instance or a legacy instance + + auto profile = static_cast(m_instance)->getPackProfile(); + if (!profile->getModLoaders().has_value()) { + QMessageBox::critical(this, tr("Error"), tr("Please install a mod loader first!")); + return; + } auto selection = m_filterModel->mapSelectionToSource(ui->treeView->selectionModel()->selection()).indexes(); auto mods_list = m_model->selectedMods(selection); diff --git a/launcher/ui/pages/instance/VersionPage.cpp b/launcher/ui/pages/instance/VersionPage.cpp index e22c764ca..2918261d2 100644 --- a/launcher/ui/pages/instance/VersionPage.cpp +++ b/launcher/ui/pages/instance/VersionPage.cpp @@ -411,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 Microsoft or Mojang account."), + "one account added.\nPlease add a Microsoft account."), QMessageBox::Warning) ->show(); return; diff --git a/launcher/ui/pages/modplatform/ModPage.cpp b/launcher/ui/pages/modplatform/ModPage.cpp index 7eb08f6a2..d6cc1fdcc 100644 --- a/launcher/ui/pages/modplatform/ModPage.cpp +++ b/launcher/ui/pages/modplatform/ModPage.cpp @@ -131,8 +131,10 @@ void ModPage::updateVersionList() } // Only add the version if it's valid or using the 'Any' filter, but never if the version is opted out - if ((valid || m_filter->versions.empty()) && !optedOut(version)) - m_ui->versionSelectionBox->addItem(version.version, QVariant(i)); + if ((valid || m_filter->versions.empty()) && !optedOut(version)) { + auto release_type = version.version_type.isValid() ? QString(" [%1]").arg(version.version_type.toString()) : ""; + m_ui->versionSelectionBox->addItem(QString("%1%2").arg(version.version, release_type), QVariant(i)); + } } if (m_ui->versionSelectionBox->count() == 0) { m_ui->versionSelectionBox->addItem(tr("No valid version found!"), QVariant(-1)); diff --git a/launcher/ui/pages/modplatform/OptionalModDialog.cpp b/launcher/ui/pages/modplatform/OptionalModDialog.cpp new file mode 100644 index 000000000..fc1c8b3cb --- /dev/null +++ b/launcher/ui/pages/modplatform/OptionalModDialog.cpp @@ -0,0 +1,63 @@ +// 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 . + */ + +#include "OptionalModDialog.h" +#include "ui_OptionalModDialog.h" + +OptionalModDialog::OptionalModDialog(QWidget* parent, const QStringList& mods) : QDialog(parent), ui(new Ui::OptionalModDialog) +{ + ui->setupUi(this); + for (const QString& mod : mods) { + auto item = new QListWidgetItem(mod, ui->list); + item->setFlags(item->flags() | Qt::ItemIsUserCheckable); + item->setCheckState(Qt::Unchecked); + item->setData(Qt::UserRole, mod); + } + + connect(ui->selectAllButton, &QPushButton::clicked, ui->list, [this] { + for (int i = 0; i < ui->list->count(); i++) + ui->list->item(i)->setCheckState(Qt::Checked); + }); + connect(ui->clearAllButton, &QPushButton::clicked, ui->list, [this] { + for (int i = 0; i < ui->list->count(); i++) + ui->list->item(i)->setCheckState(Qt::Unchecked); + }); + connect(ui->list, &QListWidget::itemActivated, [](QListWidgetItem* item) { + if (item->checkState() == Qt::Checked) + item->setCheckState(Qt::Unchecked); + else + item->setCheckState(Qt::Checked); + }); +} + +OptionalModDialog::~OptionalModDialog() +{ + delete ui; +} + +QStringList OptionalModDialog::getResult() +{ + QStringList result; + result.reserve(ui->list->count()); + for (int i = 0; i < ui->list->count(); i++) { + auto item = ui->list->item(i); + if (item->checkState() == Qt::Checked) + result.append(item->data(Qt::UserRole).toString()); + } + return result; +} diff --git a/launcher/ui/pages/modplatform/OptionalModDialog.h b/launcher/ui/pages/modplatform/OptionalModDialog.h new file mode 100644 index 000000000..1897c1fca --- /dev/null +++ b/launcher/ui/pages/modplatform/OptionalModDialog.h @@ -0,0 +1,39 @@ +// 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 . + */ + +#pragma once + +#include +#include + +namespace Ui { +class OptionalModDialog; +} + +class OptionalModDialog : public QDialog { + Q_OBJECT + + public: + OptionalModDialog(QWidget* parent, const QStringList& mods); + ~OptionalModDialog() override; + + QStringList getResult(); + + private: + Ui::OptionalModDialog* ui; +}; diff --git a/launcher/ui/pages/modplatform/OptionalModDialog.ui b/launcher/ui/pages/modplatform/OptionalModDialog.ui new file mode 100644 index 000000000..0b809d2cb --- /dev/null +++ b/launcher/ui/pages/modplatform/OptionalModDialog.ui @@ -0,0 +1,113 @@ + + + OptionalModDialog + + + + 0 + 0 + 550 + 310 + + + + Select Optional Mods + + + + + + + + Qt::IgnoreAction + + + true + + + + + + + + + Select All + + + + + + + Deselect All + + + + + + + Qt::Horizontal + + + + 40 + 20 + + + + + + + + Unchecked mods will be disabled. + + + + + + + + + QDialogButtonBox::Cancel|QDialogButtonBox::Ok + + + + + + + + + + + buttonBox + accepted() + OptionalModDialog + accept() + + + 274 + 284 + + + 274 + 154 + + + + + buttonBox + rejected() + OptionalModDialog + reject() + + + 274 + 284 + + + 274 + 154 + + + + + diff --git a/launcher/ui/pages/modplatform/ResourcePage.cpp b/launcher/ui/pages/modplatform/ResourcePage.cpp index fc7d64a48..44a91003d 100644 --- a/launcher/ui/pages/modplatform/ResourcePage.cpp +++ b/launcher/ui/pages/modplatform/ResourcePage.cpp @@ -266,6 +266,9 @@ void ResourcePage::updateVersionList() if (optedOut(version)) continue; + auto release_type = current_pack->versions[i].version_type.isValid() + ? QString(" [%1]").arg(current_pack->versions[i].version_type.toString()) + : ""; m_ui->versionSelectionBox->addItem(current_pack->versions[i].version, QVariant(i)); } diff --git a/launcher/ui/pages/modplatform/atlauncher/AtlListModel.cpp b/launcher/ui/pages/modplatform/atlauncher/AtlListModel.cpp index b6fb71535..d46b97af1 100644 --- a/launcher/ui/pages/modplatform/atlauncher/AtlListModel.cpp +++ b/launcher/ui/pages/modplatform/atlauncher/AtlListModel.cpp @@ -63,7 +63,7 @@ QVariant ListModel::data(const QModelIndex& index, int role) const } auto icon = APPLICATION->getThemedIcon("atlauncher-placeholder"); - auto url = QString(BuildConfig.ATL_DOWNLOAD_SERVER_URL + "launcher/images/%1.png").arg(pack.safeName.toLower()); + auto url = QString(BuildConfig.ATL_DOWNLOAD_SERVER_URL + "launcher/images/%1").arg(pack.safeName); ((ListModel*)this)->requestLogo(pack.safeName, url); return icon; diff --git a/launcher/ui/pages/modplatform/atlauncher/AtlOptionalModDialog.h b/launcher/ui/pages/modplatform/atlauncher/AtlOptionalModDialog.h index 55903003b..767d277d9 100644 --- a/launcher/ui/pages/modplatform/atlauncher/AtlOptionalModDialog.h +++ b/launcher/ui/pages/modplatform/atlauncher/AtlOptionalModDialog.h @@ -38,7 +38,7 @@ #include #include -#include "modplatform/atlauncher/ATLPackIndex.h" +#include "modplatform/atlauncher/ATLPackManifest.h" #include "net/NetJob.h" namespace Ui { diff --git a/launcher/ui/pages/modplatform/atlauncher/AtlPage.cpp b/launcher/ui/pages/modplatform/atlauncher/AtlPage.cpp index c7e800278..e492830c6 100644 --- a/launcher/ui/pages/modplatform/atlauncher/AtlPage.cpp +++ b/launcher/ui/pages/modplatform/atlauncher/AtlPage.cpp @@ -114,8 +114,8 @@ void AtlPage::suggestCurrent() auto uiSupport = new AtlUserInteractionSupportImpl(this); dialog->setSuggestedPack(selected.name, selectedVersion, new ATLauncher::PackInstallTask(uiSupport, selected.name, selectedVersion)); - auto editedLogoName = selected.safeName; - auto url = QString(BuildConfig.ATL_DOWNLOAD_SERVER_URL + "launcher/images/%1.png").arg(selected.safeName.toLower()); + auto editedLogoName = "atl_" + selected.safeName; + auto url = QString(BuildConfig.ATL_DOWNLOAD_SERVER_URL + "launcher/images/%1").arg(selected.safeName); listModel->getLogo(selected.safeName, url, [this, editedLogoName](QString logo) { dialog->setSuggestedIconFromFile(logo, editedLogoName); }); } diff --git a/launcher/ui/pages/modplatform/flame/FlamePage.cpp b/launcher/ui/pages/modplatform/flame/FlamePage.cpp index 50656f427..16055c4bb 100644 --- a/launcher/ui/pages/modplatform/flame/FlamePage.cpp +++ b/launcher/ui/pages/modplatform/flame/FlamePage.cpp @@ -176,7 +176,8 @@ void FlamePage::onSelectionChanged(QModelIndex curr, [[maybe_unused]] QModelInde } for (auto version : current.versions) { - ui->versionSelectionBox->addItem(version.version, QVariant(version.downloadUrl)); + auto release_type = version.version_type.isValid() ? QString(" [%1]").arg(version.version_type.toString()) : ""; + ui->versionSelectionBox->addItem(QString("%1%2").arg(version.version, release_type), QVariant(version.downloadUrl)); } QVariant current_updated; @@ -227,8 +228,7 @@ void FlamePage::suggestCurrent() extra_info.insert("pack_version_id", QString::number(version.fileId)); dialog->setSuggestedPack(current.name, new InstanceImportTask(version.downloadUrl, this, std::move(extra_info))); - QString editedLogoName; - editedLogoName = "curseforge_" + current.logoName; + QString editedLogoName = "curseforge_" + current.logoName; listModel->getLogo(current.logoName, current.logoUrl, [this, editedLogoName](QString logo) { dialog->setSuggestedIconFromFile(logo, editedLogoName); }); } diff --git a/launcher/ui/pages/modplatform/import_ftb/ImportFTBPage.cpp b/launcher/ui/pages/modplatform/import_ftb/ImportFTBPage.cpp index d3ead0837..43ff1c0fe 100644 --- a/launcher/ui/pages/modplatform/import_ftb/ImportFTBPage.cpp +++ b/launcher/ui/pages/modplatform/import_ftb/ImportFTBPage.cpp @@ -90,7 +90,7 @@ void ImportFTBPage::suggestCurrent() } dialog->setSuggestedPack(selected.name, new PackInstallTask(selected)); - QString editedLogoName = QString("ftb_%1").arg(selected.id); + QString editedLogoName = QString("ftb_%1_%2,jpg").arg(selected.name, selected.id); dialog->setSuggestedIconFromFile(FS::PathCombine(selected.path, "folder.jpg"), editedLogoName); } diff --git a/launcher/ui/pages/modplatform/legacy_ftb/Page.cpp b/launcher/ui/pages/modplatform/legacy_ftb/Page.cpp index 4104f1391..0ecaf4625 100644 --- a/launcher/ui/pages/modplatform/legacy_ftb/Page.cpp +++ b/launcher/ui/pages/modplatform/legacy_ftb/Page.cpp @@ -179,15 +179,11 @@ void Page::suggestCurrent() } dialog->setSuggestedPack(selected.name, selectedVersion, new PackInstallTask(APPLICATION->network(), selected, selectedVersion)); - QString editedLogoName; - if (selected.logo.toLower().startsWith("ftb")) { - editedLogoName = selected.logo; - } else { - editedLogoName = "ftb_" + selected.logo; + QString editedLogoName = selected.logo; + if (!selected.logo.toLower().startsWith("ftb")) { + editedLogoName = "ftb_" + editedLogoName; } - editedLogoName = editedLogoName.left(editedLogoName.lastIndexOf(".png")); - if (selected.type == PackType::Public) { publicListModel->getLogo(selected.logo, [this, editedLogoName](QString logo) { dialog->setSuggestedIconFromFile(logo, editedLogoName); }); diff --git a/launcher/ui/pages/modplatform/modrinth/ModrinthPage.cpp b/launcher/ui/pages/modplatform/modrinth/ModrinthPage.cpp index f7fa8fd78..8e2b9a902 100644 --- a/launcher/ui/pages/modplatform/modrinth/ModrinthPage.cpp +++ b/launcher/ui/pages/modplatform/modrinth/ModrinthPage.cpp @@ -217,12 +217,13 @@ void ModrinthPage::onSelectionChanged(QModelIndex curr, [[maybe_unused]] QModelI qDebug() << *response; qWarning() << "Error while reading modrinth modpack version: " << e.cause(); } - for (auto version : current.versions) { + auto release_type = version.version_type.isValid() ? QString(" [%1]").arg(version.version_type.toString()) : ""; if (!version.name.contains(version.version)) - ui->versionSelectionBox->addItem(QString("%1 — %2").arg(version.name, version.version), QVariant(version.id)); + ui->versionSelectionBox->addItem(QString("%1 — %2%3").arg(version.name, version.version, release_type), + QVariant(version.id)); else - ui->versionSelectionBox->addItem(version.name, QVariant(version.id)); + ui->versionSelectionBox->addItem(QString("%1%2").arg(version.name, release_type), QVariant(version.id)); } QVariant current_updated; diff --git a/launcher/ui/pages/modplatform/technic/TechnicModel.cpp b/launcher/ui/pages/modplatform/technic/TechnicModel.cpp index 3cd1d9a2d..6f1810d71 100644 --- a/launcher/ui/pages/modplatform/technic/TechnicModel.cpp +++ b/launcher/ui/pages/modplatform/technic/TechnicModel.cpp @@ -41,7 +41,9 @@ #include "net/ApiDownload.h" #include "ui/widgets/ProjectItem.h" +#include #include +#include Technic::ListModel::ListModel(QObject* parent) : QAbstractListModel(parent) {} @@ -193,7 +195,7 @@ void Technic::ListModel::searchRequestFinished() pack.logoName = "null"; } else { pack.logoUrl = rawURL; - pack.logoName = rawURL.section(QLatin1Char('/'), -1); + pack.logoName = pack.slug + "." + QFileInfo(QUrl(rawURL).fileName()).suffix(); } pack.broken = false; newList.append(pack); @@ -215,7 +217,7 @@ void Technic::ListModel::searchRequestFinished() auto iconUrl = Json::requireString(iconObj, "url"); pack.logoUrl = iconUrl; - pack.logoName = iconUrl.section(QLatin1Char('/'), -1); + pack.logoName = pack.slug + "." + QFileInfo(QUrl(iconUrl).fileName()).suffix(); } else { pack.logoUrl = "null"; pack.logoName = "null"; diff --git a/launcher/ui/widgets/VersionSelectWidget.h b/launcher/ui/widgets/VersionSelectWidget.h index 99729fbdf..d5ef1cc9f 100644 --- a/launcher/ui/widgets/VersionSelectWidget.h +++ b/launcher/ui/widgets/VersionSelectWidget.h @@ -82,7 +82,7 @@ class VersionSelectWidget : public QWidget { void selectedVersionChanged(BaseVersion::Ptr version); protected: - virtual void closeEvent(QCloseEvent*); + virtual void closeEvent(QCloseEvent*) override; private slots: void onTaskSucceeded(); diff --git a/launcher/updater/PrismExternalUpdater.cpp b/launcher/updater/PrismExternalUpdater.cpp new file mode 100644 index 000000000..bee72e3a0 --- /dev/null +++ b/launcher/updater/PrismExternalUpdater.cpp @@ -0,0 +1,354 @@ +// SPDX-FileCopyrightText: 2023 Rachel Powers <508861+Ryex@users.noreply.github.com> +// +// 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 "PrismExternalUpdater.h" +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include "StringUtils.h" + +#include "BuildConfig.h" + +#include "ui/dialogs/UpdateAvailableDialog.h" + +class PrismExternalUpdater::Private { + public: + QDir appDir; + QDir dataDir; + QTimer updateTimer; + bool allowBeta; + bool autoCheck; + double updateInterval; + QDateTime lastCheck; + std::unique_ptr settings; + + QWidget* parent; +}; + +PrismExternalUpdater::PrismExternalUpdater(QWidget* parent, const QString& appDir, const QString& dataDir) +{ + priv = new PrismExternalUpdater::Private(); + priv->appDir = QDir(appDir); + priv->dataDir = QDir(dataDir); + auto settings_file = priv->dataDir.absoluteFilePath("prismlauncher_update.cfg"); + priv->settings = std::make_unique(settings_file, QSettings::Format::IniFormat); + priv->allowBeta = priv->settings->value("allow_beta", false).toBool(); + priv->autoCheck = priv->settings->value("auto_check", false).toBool(); + bool interval_ok; + // default once per day + priv->updateInterval = priv->settings->value("update_interval", 86400).toInt(&interval_ok); + if (!interval_ok) + priv->updateInterval = 86400; + auto last_check = priv->settings->value("last_check"); + if (!last_check.isNull() && last_check.isValid()) { + priv->lastCheck = QDateTime::fromString(last_check.toString(), Qt::ISODate); + } + priv->parent = parent; + connectTimer(); + resetAutoCheckTimer(); +} + +PrismExternalUpdater::~PrismExternalUpdater() +{ + if (priv->updateTimer.isActive()) + priv->updateTimer.stop(); + disconnectTimer(); + priv->settings->sync(); + delete priv; +} + +void PrismExternalUpdater::checkForUpdates() +{ + QProgressDialog progress(tr("Checking for updates..."), "", 0, 0, priv->parent); + progress.setCancelButton(nullptr); + progress.adjustSize(); + progress.show(); + QCoreApplication::processEvents(); + + QProcess proc; + auto exe_name = QStringLiteral("%1_updater").arg(BuildConfig.LAUNCHER_APP_BINARY_NAME); +#if defined Q_OS_WIN32 + exe_name.append(".exe"); + + auto env = QProcessEnvironment::systemEnvironment(); + env.insert("__COMPAT_LAYER", "RUNASINVOKER"); + proc.setProcessEnvironment(env); +#else + exe_name = QString("bin/%1").arg(exe_name); +#endif + + QStringList args = { "--check-only", "--dir", priv->dataDir.absolutePath(), "--debug" }; + if (priv->allowBeta) + args.append("--pre-release"); + + proc.start(priv->appDir.absoluteFilePath(exe_name), args); + auto result_start = proc.waitForStarted(5000); + if (!result_start) { + auto err = proc.error(); + qDebug() << "Failed to start updater after 5 seconds." + << "reason:" << err << proc.errorString(); + auto msgBox = + QMessageBox(QMessageBox::Information, tr("Update Check Failed"), + tr("Failed to start after 5 seconds\nReason: %1.").arg(proc.errorString()), QMessageBox::Ok, priv->parent); + msgBox.setMinimumWidth(460); + msgBox.adjustSize(); + msgBox.exec(); + priv->lastCheck = QDateTime::currentDateTime(); + priv->settings->setValue("last_check", priv->lastCheck.toString(Qt::ISODate)); + priv->settings->sync(); + resetAutoCheckTimer(); + return; + } + QCoreApplication::processEvents(); + + auto result_finished = proc.waitForFinished(60000); + if (!result_finished) { + proc.kill(); + auto err = proc.error(); + auto output = proc.readAll(); + qDebug() << "Updater failed to close after 60 seconds." + << "reason:" << err << proc.errorString(); + auto msgBox = + QMessageBox(QMessageBox::Information, tr("Update Check Failed"), + tr("Updater failed to close 60 seconds\nReason: %1.").arg(proc.errorString()), QMessageBox::Ok, priv->parent); + msgBox.setDetailedText(output); + msgBox.setMinimumWidth(460); + msgBox.adjustSize(); + msgBox.exec(); + priv->lastCheck = QDateTime::currentDateTime(); + priv->settings->setValue("last_check", priv->lastCheck.toString(Qt::ISODate)); + priv->settings->sync(); + resetAutoCheckTimer(); + return; + } + + auto exit_code = proc.exitCode(); + + auto std_output = proc.readAllStandardOutput(); + auto std_error = proc.readAllStandardError(); + + progress.hide(); + QCoreApplication::processEvents(); + + switch (exit_code) { + case 0: + // no update available + { + qDebug() << "No update available"; + auto msgBox = QMessageBox(QMessageBox::Information, tr("No Update Available"), tr("You are running the latest version."), + QMessageBox::Ok, priv->parent); + msgBox.setMinimumWidth(460); + msgBox.adjustSize(); + msgBox.exec(); + } + break; + case 1: + // there was an error + { + qDebug() << "Updater subprocess error" << qPrintable(std_error); + auto msgBox = QMessageBox(QMessageBox::Warning, tr("Update Check Error"), + tr("There was an error running the update check."), QMessageBox::Ok, priv->parent); + msgBox.setDetailedText(QString(std_error)); + msgBox.setMinimumWidth(460); + msgBox.adjustSize(); + msgBox.exec(); + } + break; + case 100: + // update available + { + auto [first_line, remainder1] = StringUtils::splitFirst(std_output, '\n'); + auto [second_line, remainder2] = StringUtils::splitFirst(remainder1, '\n'); + auto [third_line, release_notes] = StringUtils::splitFirst(remainder2, '\n'); + auto version_name = StringUtils::splitFirst(first_line, ": ").second.trimmed(); + auto version_tag = StringUtils::splitFirst(second_line, ": ").second.trimmed(); + auto release_timestamp = QDateTime::fromString(StringUtils::splitFirst(third_line, ": ").second.trimmed(), Qt::ISODate); + qDebug() << "Update available:" << version_name << version_tag << release_timestamp; + qDebug() << "Update release notes:" << release_notes; + + offerUpdate(version_name, version_tag, release_notes); + } + break; + default: + // unknown error code + { + qDebug() << "Updater exited with unknown code" << exit_code; + auto msgBox = + QMessageBox(QMessageBox::Information, tr("Unknown Update Error"), + tr("The updater exited with an unknown condition.\nExit Code: %1").arg(QString::number(exit_code)), + QMessageBox::Ok, priv->parent); + auto detail_txt = tr("StdOut: %1\nStdErr: %2").arg(QString(std_output)).arg(QString(std_error)); + msgBox.setDetailedText(detail_txt); + msgBox.setMinimumWidth(460); + msgBox.adjustSize(); + msgBox.exec(); + } + } + priv->lastCheck = QDateTime::currentDateTime(); + priv->settings->setValue("last_check", priv->lastCheck.toString(Qt::ISODate)); + priv->settings->sync(); + resetAutoCheckTimer(); +} + +bool PrismExternalUpdater::getAutomaticallyChecksForUpdates() +{ + return priv->autoCheck; +} + +double PrismExternalUpdater::getUpdateCheckInterval() +{ + return priv->updateInterval; +} + +bool PrismExternalUpdater::getBetaAllowed() +{ + return priv->allowBeta; +} + +void PrismExternalUpdater::setAutomaticallyChecksForUpdates(bool check) +{ + priv->autoCheck = check; + priv->settings->setValue("auto_check", check); + priv->settings->sync(); + resetAutoCheckTimer(); +} + +void PrismExternalUpdater::setUpdateCheckInterval(double seconds) +{ + priv->updateInterval = seconds; + priv->settings->setValue("update_interval", seconds); + priv->settings->sync(); + resetAutoCheckTimer(); +} + +void PrismExternalUpdater::setBetaAllowed(bool allowed) +{ + priv->allowBeta = allowed; + priv->settings->setValue("auto_beta", allowed); + priv->settings->sync(); +} + +void PrismExternalUpdater::resetAutoCheckTimer() +{ + if (priv->autoCheck) { + int timeoutDuration = 0; + auto now = QDateTime::currentDateTime(); + if (priv->lastCheck.isValid()) { + auto diff = priv->lastCheck.secsTo(now); + auto secs_left = priv->updateInterval - diff; + if (secs_left < 0) + secs_left = 0; + timeoutDuration = secs_left * 1000; // to msec + } + qDebug() << "Auto update timer starting," << timeoutDuration / 1000 << "seconds left"; + priv->updateTimer.start(timeoutDuration); + } else { + if (priv->updateTimer.isActive()) + priv->updateTimer.stop(); + } +} + +void PrismExternalUpdater::connectTimer() +{ + connect(&priv->updateTimer, &QTimer::timeout, this, &PrismExternalUpdater::autoCheckTimerFired); +} + +void PrismExternalUpdater::disconnectTimer() +{ + disconnect(&priv->updateTimer, &QTimer::timeout, this, &PrismExternalUpdater::autoCheckTimerFired); +} + +void PrismExternalUpdater::autoCheckTimerFired() +{ + qDebug() << "Auto update Timer fired"; + checkForUpdates(); +} + +void PrismExternalUpdater::offerUpdate(const QString& version_name, const QString& version_tag, const QString& release_notes) +{ + priv->settings->beginGroup("skip"); + auto should_skip = priv->settings->value(version_tag, false).toBool(); + priv->settings->endGroup(); + + if (should_skip) { + auto msgBox = QMessageBox(QMessageBox::Information, tr("No Update Available"), tr("There are no new updates available."), + QMessageBox::Ok, priv->parent); + msgBox.setMinimumWidth(460); + msgBox.adjustSize(); + msgBox.exec(); + return; + } + + UpdateAvailableDialog dlg(BuildConfig.printableVersionString(), version_name, release_notes); + + auto result = dlg.exec(); + qDebug() << "offer dlg result" << result; + switch (result) { + case UpdateAvailableDialog::Install: { + performUpdate(version_tag); + return; + } + case UpdateAvailableDialog::Skip: { + priv->settings->beginGroup("skip"); + priv->settings->setValue(version_tag, true); + priv->settings->endGroup(); + priv->settings->sync(); + return; + } + case UpdateAvailableDialog::DontInstall: { + return; + } + } +} + +void PrismExternalUpdater::performUpdate(const QString& version_tag) +{ + QProcess proc; + auto exe_name = QStringLiteral("%1_updater").arg(BuildConfig.LAUNCHER_APP_BINARY_NAME); +#if defined Q_OS_WIN32 + exe_name.append(".exe"); + + auto env = QProcessEnvironment::systemEnvironment(); + env.insert("__COMPAT_LAYER", "RUNASINVOKER"); + proc.setProcessEnvironment(env); +#else + exe_name = QString("bin/%1").arg(exe_name); +#endif + + QStringList args = { "--dir", priv->dataDir.absolutePath(), "--install-version", version_tag }; + if (priv->allowBeta) + args.append("--pre-release"); + + auto result = proc.startDetached(priv->appDir.absoluteFilePath(exe_name), args); + if (!result) { + qDebug() << "Failed to start updater:" << proc.error() << proc.errorString(); + } + QCoreApplication::exit(); +} diff --git a/launcher/updater/PrismExternalUpdater.h b/launcher/updater/PrismExternalUpdater.h new file mode 100644 index 000000000..bfe94c149 --- /dev/null +++ b/launcher/updater/PrismExternalUpdater.h @@ -0,0 +1,95 @@ +// SPDX-FileCopyrightText: 2023 Rachel Powers <508861+Ryex@users.noreply.github.com> +// +// 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 "ExternalUpdater.h" + +/*! + * An implementation for the updater on windows and linux that uses out external updater. + */ + +class PrismExternalUpdater : public ExternalUpdater { + Q_OBJECT + + public: + PrismExternalUpdater(QWidget* parent, const QString& appDir, const QString& dataDir); + ~PrismExternalUpdater() override; + + /*! + * Check for updates manually, showing the user a progress bar and an alert if no updates are found. + */ + void checkForUpdates() override; + + /*! + * Indicates whether or not to check for updates automatically. + */ + bool getAutomaticallyChecksForUpdates() override; + + /*! + * Indicates the current automatic update check interval in seconds. + */ + double getUpdateCheckInterval() override; + + /*! + * Indicates whether or not beta updates should be checked for in addition to regular releases. + */ + bool getBetaAllowed() override; + + /*! + * Set whether or not to check for updates automatically. + * + * The update schedule cycle will be reset in a short delay after the property’s new value is set. This is to allow + * reverting this property without kicking off a schedule change immediately." + */ + void setAutomaticallyChecksForUpdates(bool check) override; + + /*! + * Set the current automatic update check interval in seconds. + * + * The update schedule cycle will be reset in a short delay after the property’s new value is set. This is to allow + * reverting this property without kicking off a schedule change immediately." + */ + void setUpdateCheckInterval(double seconds) override; + + /*! + * Set whether or not beta updates should be checked for in addition to regular releases. + */ + void setBetaAllowed(bool allowed) override; + + void resetAutoCheckTimer(); + void disconnectTimer(); + void connectTimer(); + + void offerUpdate(const QString& version_name, const QString& version_tag, const QString& release_notes); + void performUpdate(const QString& version_tag); + + public slots: + void autoCheckTimerFired(); + + private: + class Private; + + Private* priv; +}; diff --git a/launcher/updater/prismupdater/GitHubRelease.cpp b/launcher/updater/prismupdater/GitHubRelease.cpp new file mode 100644 index 000000000..3beae31b1 --- /dev/null +++ b/launcher/updater/prismupdater/GitHubRelease.cpp @@ -0,0 +1,93 @@ +// SPDX-FileCopyrightText: 2023 Rachel Powers <508861+Ryex@users.noreply.github.com> +// +// 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 "GitHubRelease.h" + +QDebug operator<<(QDebug debug, const GitHubReleaseAsset& asset) +{ + QDebugStateSaver saver(debug); + debug.nospace() << "GitHubReleaseAsset( " + "id: " + << asset.id + << ", " + "name " + << asset.name + << ", " + "label: " + << asset.label + << ", " + "content_type: " + << asset.content_type + << ", " + "size: " + << asset.size + << ", " + "created_at: " + << asset.created_at + << ", " + "updated_at: " + << asset.updated_at + << ", " + "browser_download_url: " + << asset.browser_download_url + << " " + ")"; + return debug; +} + +QDebug operator<<(QDebug debug, const GitHubRelease& rls) +{ + QDebugStateSaver saver(debug); + debug.nospace() << "GitHubRelease( " + "id: " + << rls.id + << ", " + "name " + << rls.name + << ", " + "tag_name: " + << rls.tag_name + << ", " + "created_at: " + << rls.created_at + << ", " + "published_at: " + << rls.published_at + << ", " + "prerelease: " + << rls.prerelease + << ", " + "draft: " + << rls.draft + << ", " + "version" + << rls.version + << ", " + "body: " + << rls.body + << ", " + "assets: " + << rls.assets + << " " + ")"; + return debug; +} diff --git a/launcher/updater/prismupdater/GitHubRelease.h b/launcher/updater/prismupdater/GitHubRelease.h new file mode 100644 index 000000000..798c6b7ae --- /dev/null +++ b/launcher/updater/prismupdater/GitHubRelease.h @@ -0,0 +1,61 @@ +// SPDX-FileCopyrightText: 2023 Rachel Powers <508861+Ryex@users.noreply.github.com> +// +// 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 +#include + +#include + +#include "Version.h" + +struct GitHubReleaseAsset { + int id = -1; + QString name; + QString label; + QString content_type; + int size; + QDateTime created_at; + QDateTime updated_at; + QString browser_download_url; + + bool isValid() { return id > 0; } +}; + +struct GitHubRelease { + int id = -1; + QString name; + QString tag_name; + QDateTime created_at; + QDateTime published_at; + bool prerelease; + bool draft; + QString body; + QList assets; + Version version; + + bool isValid() const { return id > 0; } +}; + +QDebug operator<<(QDebug debug, const GitHubReleaseAsset& rls); +QDebug operator<<(QDebug debug, const GitHubRelease& rls); diff --git a/launcher/updater/prismupdater/PrismUpdater.cpp b/launcher/updater/prismupdater/PrismUpdater.cpp new file mode 100644 index 000000000..1c5aaf1e8 --- /dev/null +++ b/launcher/updater/prismupdater/PrismUpdater.cpp @@ -0,0 +1,1401 @@ +// SPDX-FileCopyrightText: 2022 Rachel Powers <508861+Ryex@users.noreply.github.com> +// +// 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 . + * + */ + +#include "PrismUpdater.h" +#include "BuildConfig.h" +#include "ui/dialogs/ProgressDialog.h" + +#include +#include + +#include + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include + +#if defined Q_OS_WIN32 +#ifndef WIN32_LEAN_AND_MEAN +#define WIN32_LEAN_AND_MEAN +#endif +#include +#include +#include +#include +#include +#endif + +// Snippet from https://github.com/gulrak/filesystem#using-it-as-single-file-header + +#ifdef __APPLE__ +#include // for deployment target to support pre-catalina targets without std::fs +#endif // __APPLE__ + +#if ((defined(_MSVC_LANG) && _MSVC_LANG >= 201703L) || (defined(__cplusplus) && __cplusplus >= 201703L)) && defined(__has_include) +#if __has_include() && (!defined(__MAC_OS_X_VERSION_MIN_REQUIRED) || __MAC_OS_X_VERSION_MIN_REQUIRED >= 101500) +#define GHC_USE_STD_FS +#include +namespace fs = std::filesystem; +#endif // MacOS min version check +#endif // Other OSes version check + +#ifndef GHC_USE_STD_FS +#include +namespace fs = ghc::filesystem; +#endif + +#include "DesktopServices.h" + +#include "updater/prismupdater/UpdaterDialogs.h" + +#include "FileSystem.h" +#include "Json.h" +#include "StringUtils.h" + +#include "net/Download.h" +#include "net/RawHeaderProxy.h" + +#include "MMCZip.h" + +/** output to the log file */ +void appDebugOutput(QtMsgType type, const QMessageLogContext& context, const QString& msg) +{ + static std::mutex loggerMutex; + const std::lock_guard lock(loggerMutex); // synchronized, QFile logFile is not thread-safe + + QString out = qFormatLogMessage(type, context, msg); + out += QChar::LineFeed; + + PrismUpdaterApp* app = static_cast(QCoreApplication::instance()); + app->logFile->write(out.toUtf8()); + app->logFile->flush(); + if (app->logToConsole) { + QTextStream(stderr) << out.toLocal8Bit(); + fflush(stderr); + } +} + +#if defined Q_OS_WIN32 + +// 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) { + HANDLE stdHandle = GetStdHandle(STD_INPUT_HANDLE); + if (stdHandle != INVALID_HANDLE_VALUE) { + int fileDescriptor = _open_osfhandle((intptr_t)stdHandle, _O_TEXT); + if (fileDescriptor != -1) { + FILE* file = _fdopen(fileDescriptor, "r"); + if (file != NULL) { + int dup2Result = _dup2(_fileno(file), _fileno(stdin)); + if (dup2Result == 0) { + setvbuf(stdin, NULL, _IONBF, 0); + } + } + } + } + } + + // Redirect unbuffered stdout to the current standard output handle + if (bindStdOut) { + HANDLE stdHandle = GetStdHandle(STD_OUTPUT_HANDLE); + if (stdHandle != INVALID_HANDLE_VALUE) { + int fileDescriptor = _open_osfhandle((intptr_t)stdHandle, _O_TEXT); + if (fileDescriptor != -1) { + FILE* file = _fdopen(fileDescriptor, "w"); + if (file != NULL) { + int dup2Result = _dup2(_fileno(file), _fileno(stdout)); + if (dup2Result == 0) { + setvbuf(stdout, NULL, _IONBF, 0); + } + } + } + } + } + + // Redirect unbuffered stderr to the current standard error handle + if (bindStdErr) { + HANDLE stdHandle = GetStdHandle(STD_ERROR_HANDLE); + if (stdHandle != INVALID_HANDLE_VALUE) { + int fileDescriptor = _open_osfhandle((intptr_t)stdHandle, _O_TEXT); + if (fileDescriptor != -1) { + FILE* file = _fdopen(fileDescriptor, "w"); + if (file != NULL) { + int dup2Result = _dup2(_fileno(file), _fileno(stderr)); + if (dup2Result == 0) { + setvbuf(stderr, NULL, _IONBF, 0); + } + } + } + } + } + + // 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(); + } +} +#endif + +PrismUpdaterApp::PrismUpdaterApp(int& argc, char** argv) : QApplication(argc, argv) +{ +#if defined Q_OS_WIN32 + // attach the parent console if stdout not already captured + auto stdout_type = GetFileType(GetStdHandle(STD_OUTPUT_HANDLE)); + if (stdout_type == FILE_TYPE_CHAR || stdout_type == FILE_TYPE_UNKNOWN) { + if (AttachConsole(ATTACH_PARENT_PROCESS)) { + BindCrtHandlesToStdHandles(true, true, true); + consoleAttached = true; + } + } +#endif + setOrganizationName(BuildConfig.LAUNCHER_NAME); + setOrganizationDomain(BuildConfig.LAUNCHER_DOMAIN); + setApplicationName(BuildConfig.LAUNCHER_NAME + "Updater"); + setApplicationVersion(BuildConfig.printableVersionString() + "\n" + BuildConfig.GIT_COMMIT); + + // Command line parsing + QCommandLineParser parser; + parser.setApplicationDescription(QObject::tr("An auto-updater for Prism Launcher")); + + parser.addOptions( + { { { "d", "dir" }, tr("Use a custom path as application root (use '.' for current directory)."), tr("directory") }, + { { "V", "prism-version" }, + tr("Use this version as the installed launcher version. (provided because stdout can not be reliably captured on windows)"), + tr("installed launcher version") }, + { { "I", "install-version" }, "Install a specific version.", tr("version name") }, + { { "U", "update-url" }, tr("Update from the specified repo."), tr("github repo url") }, + { { "c", "check-only" }, + tr("Only check if an update is needed. Exit status 100 if true, 0 if false (or non 0 if there was an error).") }, + { { "p", "pre-release" }, tr("Allow updating to pre-release releases") }, + { { "F", "force" }, tr("Force an update, even if one is not needed.") }, + { { "l", "list" }, tr("List available releases.") }, + { "debug", tr("Log debug to console.") }, + { { "S", "select-ui" }, tr("Select the version to install with a GUI.") }, + { { "D", "allow-downgrade" }, tr("Allow the updater to downgrade to previous versions.") } }); + + parser.addHelpOption(); + parser.addVersionOption(); + parser.process(arguments()); + + logToConsole = parser.isSet("debug"); + + auto updater_executable = QCoreApplication::applicationFilePath(); + + if (BuildConfig.BUILD_ARTIFACT.toLower() == "macos") + showFatalErrorMessage(tr("MacOS Not Supported"), tr("The updater does not support installations on MacOS")); + + if (updater_executable.startsWith("/tmp/.mount_")) { + m_isAppimage = true; + m_appimagePath = QProcessEnvironment::systemEnvironment().value(QStringLiteral("APPIMAGE")); + if (m_appimagePath.isEmpty()) { + showFatalErrorMessage(tr("Unsupported Installation"), + tr("Updater is running as misconfigured AppImage? ($APPIMAGE environment variable is missing)")); + } + } + + m_isFlatpak = DesktopServices::isFlatpak(); + + QString prism_executable = FS::PathCombine(applicationDirPath(), BuildConfig.LAUNCHER_APP_BINARY_NAME); +#if defined Q_OS_WIN32 + prism_executable.append(".exe"); +#endif + + if (!QFileInfo(prism_executable).isFile()) { + showFatalErrorMessage(tr("Unsupported Installation"), tr("The updater can not find the main executable.")); + } + + m_prismExecutable = prism_executable; + + auto prism_update_url = parser.value("update-url"); + if (prism_update_url.isEmpty()) + prism_update_url = BuildConfig.UPDATER_GITHUB_REPO; + + m_prismRepoUrl = QUrl::fromUserInput(prism_update_url); + + m_checkOnly = parser.isSet("check-only"); + m_forceUpdate = parser.isSet("force"); + m_printOnly = parser.isSet("list"); + auto user_version = parser.value("install-version"); + if (!user_version.isEmpty()) { + m_userSelectedVersion = Version(user_version); + } + m_selectUI = parser.isSet("select-ui"); + m_allowDowngrade = parser.isSet("allow-downgrade"); + + auto version = parser.value("prism-version"); + if (!version.isEmpty()) { + if (version.contains('-')) { + auto index = version.indexOf('-'); + m_prsimVersionChannel = version.mid(index + 1); + version = version.left(index); + } else { + m_prsimVersionChannel = "stable"; + } + auto version_parts = version.split('.'); + m_prismVersionMajor = version_parts.takeFirst().toInt(); + m_prismVersionMinor = version_parts.takeFirst().toInt(); + } + + m_allowPreRelease = parser.isSet("pre-release"); + + QString origCwdPath = QDir::currentPath(); + QString binPath = applicationDirPath(); + + { // find data director + // Root path is used for updates and portable data +#if defined(Q_OS_LINUX) || defined(Q_OS_FREEBSD) || defined(Q_OS_OPENBSD) + QDir foo(FS::PathCombine(binPath, "..")); // typically portable-root or /usr + m_rootPath = foo.absolutePath(); +#elif defined(Q_OS_WIN32) + m_rootPath = binPath; +#elif defined(Q_OS_MAC) + QDir foo(FS::PathCombine(binPath, "../..")); + m_rootPath = foo.absolutePath(); + // on macOS, touch the root to force Finder to reload the .app metadata (and fix any icon change issues) + FS::updateTimestamp(m_rootPath); +#endif + } + + QString adjustedBy; + // change folder + QString dirParam = parser.value("dir"); + if (!dirParam.isEmpty()) { + // the dir param. it makes prism launcher data path point to whatever the user specified + // on command line + adjustedBy = "Command line"; + m_dataPath = dirParam; +#ifndef Q_OS_MACOS + if (QFile::exists(FS::PathCombine(m_rootPath, "portable.txt"))) { + m_isPortable = true; + } +#endif + } else { + QDir foo(FS::PathCombine(QStandardPaths::writableLocation(QStandardPaths::AppDataLocation), "..")); + m_dataPath = foo.absolutePath(); + adjustedBy = "Persistent data path"; + +#ifndef Q_OS_MACOS + if (QFile::exists(FS::PathCombine(m_rootPath, "portable.txt"))) { + m_dataPath = m_rootPath; + adjustedBy = "Portable data path"; + m_isPortable = true; + } +#endif + } + + m_updateLogPath = FS::PathCombine(m_dataPath, "logs", "prism_launcher_update.log"); + + { // setup logging + FS::ensureFolderPathExists(FS::PathCombine(m_dataPath, "logs")); + static const QString baseLogFile = BuildConfig.LAUNCHER_NAME + "Updater" + (m_checkOnly ? "-CheckOnly" : "") + "-%0.log"; + static const QString logBase = FS::PathCombine(m_dataPath, "logs", baseLogFile); + auto moveFile = [](const QString& oldName, const QString& newName) { + QFile::remove(newName); + QFile::copy(oldName, newName); + QFile::remove(oldName); + }; + + if (FS::ensureFolderPathExists("logs")) { // enough history to track both launches of the updater during a portable install + moveFile(logBase.arg(1), logBase.arg(2)); + moveFile(logBase.arg(0), logBase.arg(1)); + } + + logFile = std::unique_ptr(new QFile(logBase.arg(0))); + if (!logFile->open(QIODevice::WriteOnly | QIODevice::Text | QIODevice::Truncate)) { + showFatalErrorMessage(tr("The launcher data folder is not writable!"), + tr("The updater couldn't create a log file - the data folder is not writable.\n" + "\n" + "Make sure you have write permissions to the data folder.\n" + "(%1)\n" + "\n" + "The updater cannot continue until you fix this problem.") + .arg(m_dataPath)); + return; + } + qInstallMessageHandler(appDebugOutput); + + qSetMessagePattern( + "%{time process}" + " " + "%{if-debug}D%{endif}" + "%{if-info}I%{endif}" + "%{if-warning}W%{endif}" + "%{if-critical}C%{endif}" + "%{if-fatal}F%{endif}" + " " + "|" + " " + "%{if-category}[%{category}]: %{endif}" + "%{message}"); + + bool foundLoggingRules = false; + + auto logRulesFile = QStringLiteral("qtlogging.ini"); + auto logRulesPath = FS::PathCombine(m_dataPath, logRulesFile); + + qDebug() << "Testing" << logRulesPath << "..."; + foundLoggingRules = QFile::exists(logRulesPath); + + // search the dataPath() + // seach app data standard path + if (!foundLoggingRules && !isPortable() && dirParam.isEmpty()) { + logRulesPath = QStandardPaths::locate(QStandardPaths::AppDataLocation, FS::PathCombine("..", logRulesFile)); + if (!logRulesPath.isEmpty()) { + qDebug() << "Found" << logRulesPath << "..."; + foundLoggingRules = true; + } + } + // seach root path + if (!foundLoggingRules) { + logRulesPath = FS::PathCombine(m_rootPath, logRulesFile); + qDebug() << "Testing" << logRulesPath << "..."; + foundLoggingRules = QFile::exists(logRulesPath); + } + + if (foundLoggingRules) { + // load and set logging rules + qDebug() << "Loading logging rules from:" << logRulesPath; + QSettings loggingRules(logRulesPath, QSettings::IniFormat); + loggingRules.beginGroup("Rules"); + QStringList rule_names = loggingRules.childKeys(); + QStringList rules; + qDebug() << "Setting log rules:"; + for (auto rule_name : rule_names) { + auto rule = QString("%1=%2").arg(rule_name).arg(loggingRules.value(rule_name).toString()); + rules.append(rule); + qDebug() << " " << rule; + } + auto rules_str = rules.join("\n"); + QLoggingCategory::setFilterRules(rules_str); + } + + qDebug() << "<> Log initialized."; + } + + { // log debug program info + qDebug() << qPrintable(BuildConfig.LAUNCHER_DISPLAYNAME) << "Updater" + << ", (c) 2022-2023 " << qPrintable(QString(BuildConfig.LAUNCHER_COPYRIGHT).replace("\n", ", ")); + qDebug() << "Version : " << BuildConfig.printableVersionString(); + qDebug() << "Git commit : " << BuildConfig.GIT_COMMIT; + qDebug() << "Git refspec : " << BuildConfig.GIT_REFSPEC; + qDebug() << "Compiled for : " << BuildConfig.systemID(); + qDebug() << "Compiled by : " << BuildConfig.compilerID(); + qDebug() << "Build Artifact : " << BuildConfig.BUILD_ARTIFACT; + if (adjustedBy.size()) { + qDebug() << "Data dir before adjustment : " << origCwdPath; + qDebug() << "Data dir after adjustment : " << m_dataPath; + qDebug() << "Adjusted by : " << adjustedBy; + } else { + qDebug() << "Data dir : " << QDir::currentPath(); + } + qDebug() << "Work dir : " << QDir::currentPath(); + qDebug() << "Binary path : " << binPath; + qDebug() << "Application root path : " << m_rootPath; + qDebug() << "Portable install : " << m_isPortable; + qDebug() << "<> Paths set."; + } + + { // network + m_network = makeShared(new QNetworkAccessManager()); + qDebug() << "Detecting proxy settings..."; + QNetworkProxy proxy = QNetworkProxy::applicationProxy(); + m_network->setProxy(proxy); + } + + auto marker_file_path = QDir(m_rootPath).absoluteFilePath(".prism_launcher_updater_unpack.marker"); + auto marker_file = QFileInfo(marker_file_path); + if (marker_file.exists()) { + auto target_dir = QString(FS::read(marker_file_path)).trimmed(); + if (target_dir.isEmpty()) { + qWarning() << "Empty updater marker file contains no install target. making best guess of parent dir"; + target_dir = QDir(m_rootPath).absoluteFilePath(".."); + } + + QMetaObject::invokeMethod( + this, [this, target_dir]() { moveAndFinishUpdate(target_dir); }, Qt::QueuedConnection); + + } else { + QMetaObject::invokeMethod(this, &PrismUpdaterApp::loadReleaseList, Qt::QueuedConnection); + } +} + +PrismUpdaterApp::~PrismUpdaterApp() +{ + qDebug() << "updater shutting down"; + // Shut down logger by setting the logger function to nothing + qInstallMessageHandler(nullptr); + +#if defined Q_OS_WIN32 + // Detach from Windows console + if (consoleAttached) { + fclose(stdout); + fclose(stdin); + fclose(stderr); + FreeConsole(); + } +#endif +} + +void PrismUpdaterApp::fail(const QString& reason) +{ + qCritical() << qPrintable(reason); + m_status = Failed; + exit(1); +} + +void PrismUpdaterApp::abort(const QString& reason) +{ + qCritical() << qPrintable(reason); + m_status = Aborted; + exit(2); +} + +void PrismUpdaterApp::showFatalErrorMessage(const QString& title, const QString& content) +{ + m_status = Failed; + auto msgBox = new QMessageBox(); + msgBox->setWindowTitle(title); + msgBox->setText(content); + msgBox->setStandardButtons(QMessageBox::Ok); + msgBox->setDefaultButton(QMessageBox::Ok); + msgBox->setTextInteractionFlags(Qt::TextSelectableByMouse | Qt::TextBrowserInteraction); + msgBox->setIcon(QMessageBox::Critical); + msgBox->setMinimumWidth(460); + msgBox->adjustSize(); + msgBox->exec(); + exit(1); +} + +void PrismUpdaterApp::run() +{ + qDebug() << "found" << m_releases.length() << "releases on github"; + qDebug() << "loading exe at " << m_prismExecutable; + + if (m_printOnly) { + printReleases(); + m_status = Succeeded; + return exit(0); + } + + if (!loadPrismVersionFromExe(m_prismExecutable)) { + m_prismVersion = BuildConfig.printableVersionString(); + m_prismVersionMajor = BuildConfig.VERSION_MAJOR; + m_prismVersionMinor = BuildConfig.VERSION_MINOR; + m_prsimVersionChannel = BuildConfig.VERSION_CHANNEL; + m_prismGitCommit = BuildConfig.GIT_COMMIT; + } + m_status = Succeeded; + + qDebug() << "Executable reports as:" << m_prismBinaryName << "version:" << m_prismVersion; + qDebug() << "Version major:" << m_prismVersionMajor; + qDebug() << "Version minor:" << m_prismVersionMinor; + qDebug() << "Version channel:" << m_prsimVersionChannel; + qDebug() << "Git Commit:" << m_prismGitCommit; + + auto latest = getLatestRelease(); + qDebug() << "Latest release" << latest.version; + auto need_update = needUpdate(latest); + + if (m_checkOnly) { + if (need_update) { + QTextStream stdOutStream(stdout); + stdOutStream << "Name: " << latest.name << "\n"; + stdOutStream << "Version: " << latest.tag_name << "\n"; + stdOutStream << "TimeStamp: " << latest.created_at.toString(Qt::ISODate) << "\n"; + stdOutStream << latest.body << "\n"; + stdOutStream.flush(); + + return exit(100); + } else { + return exit(0); + } + } + + if (m_isFlatpak) { + showFatalErrorMessage(tr("Updating flatpack not supported"), tr("Actions outside of checking if an update is available are not " + "supported when running the flatpak version of Prism Launcher.")); + return; + } + if (m_isAppimage) { + bool result = true; + if (need_update) + result = callAppImageUpdate(); + return exit(result ? 0 : 1); + } + + if (BuildConfig.BUILD_ARTIFACT.toLower() == "linux" && !m_isPortable) { + showFatalErrorMessage(tr("Updating Not Supported"), + tr("Updating non-portable linux installations is not supported. Please use your system package manager")); + return; + } + + if (need_update || m_forceUpdate || !m_userSelectedVersion.isEmpty()) { + GitHubRelease update_release = latest; + if (!m_userSelectedVersion.isEmpty()) { + bool found = false; + for (auto rls : m_releases) { + if (rls.version == m_userSelectedVersion) { + found = true; + update_release = rls; + break; + } + } + if (!found) { + showFatalErrorMessage( + "No release for version!", + QString("Can not find a github release for specified version %1").arg(m_userSelectedVersion.toString())); + return; + } + } else if (m_selectUI) { + update_release = selectRelease(); + if (!update_release.isValid()) { + showFatalErrorMessage("No version selected.", "No version was selected."); + return; + } + } + + performUpdate(update_release); + } + + exit(0); +} + +void PrismUpdaterApp::moveAndFinishUpdate(QDir target) +{ + logUpdate("Finishing update process"); + + logUpdate("Waiting 2 seconds for resources to free"); + this->thread()->sleep(2); + + auto manifest_path = FS::PathCombine(m_rootPath, "manifest.txt"); + QFileInfo manifest(manifest_path); + + auto app_dir = QDir(m_rootPath); + + QStringList file_list; + if (manifest.isFile()) { + // load manifest from file + logUpdate(tr("Reading manifest from %1").arg(manifest.absoluteFilePath())); + try { + auto contents = QString::fromUtf8(FS::read(manifest.absoluteFilePath())); + auto files = contents.split('\n'); + for (auto file : files) { + file_list.append(file.trimmed()); + } + } catch (FS::FileSystemException&) { + } + } + + if (file_list.isEmpty()) { + logUpdate(tr("Manifest empty, making best guess of the directory contents of %1").arg(m_rootPath)); + auto entries = target.entryInfoList(QDir::NoDotAndDotDot | QDir::Files | QDir::Dirs); + for (auto entry : entries) { + file_list.append(entry.fileName()); + } + } + logUpdate(tr("Installing the following to %1 :\n %2").arg(target.absolutePath()).arg(file_list.join(",\n "))); + + bool error = false; + + QProgressDialog progress(tr("Installing from %1").arg(m_rootPath), "", 0, file_list.length()); + progress.setCancelButton(nullptr); + progress.setMinimumWidth(400); + progress.adjustSize(); + progress.show(); + QCoreApplication::processEvents(); + + logUpdate(tr("Installing from %1").arg(m_rootPath)); + + auto copy = [this, app_dir, target](QString to_install_file) { + auto rel_path = app_dir.relativeFilePath(to_install_file); + auto install_path = FS::PathCombine(target.absolutePath(), rel_path); + logUpdate(tr("Installing %1 from %2").arg(install_path).arg(to_install_file)); + FS::ensureFilePathExists(install_path); + auto result = FS::copy(to_install_file, install_path).overwrite(true)(); + if (!result) { + logUpdate(tr("Failed copy %1 to %2").arg(to_install_file).arg(install_path)); + return true; + } + return false; + }; + + int i = 0; + for (auto glob : file_list) { + QDirIterator iter(m_rootPath, QStringList({ glob }), QDir::Files | QDir::Dirs | QDir::NoDotAndDotDot); + progress.setValue(i); + QCoreApplication::processEvents(); + if (!iter.hasNext() && !glob.isEmpty()) { + if (auto file_info = QFileInfo(FS::PathCombine(m_rootPath, glob)); file_info.exists()) { + error |= copy(file_info.absoluteFilePath()); + } else { + logUpdate(tr("File doesn't exist, ignoring: %1").arg(FS::PathCombine(m_rootPath, glob))); + } + } else { + while (iter.hasNext()) { + error |= copy(iter.next()); + } + } + i++; + } + progress.setValue(i); + QCoreApplication::processEvents(); + + if (error) { + logUpdate(tr("There were errors installing the update.")); + auto fail_marker = FS::PathCombine(m_dataPath, ".prism_launcher_update.fail"); + FS::copy(m_updateLogPath, fail_marker).overwrite(true)(); + } else { + logUpdate(tr("Update succeed.")); + auto success_marker = FS::PathCombine(m_dataPath, ".prism_launcher_update.success"); + FS::copy(m_updateLogPath, success_marker).overwrite(true)(); + } + auto update_lock_path = FS::PathCombine(m_dataPath, ".prism_launcher_update.lock"); + FS::deletePath(update_lock_path); + + QProcess proc; + auto app_exe_name = BuildConfig.LAUNCHER_APP_BINARY_NAME; +#if defined Q_OS_WIN32 + app_exe_name.append(".exe"); + + auto env = QProcessEnvironment::systemEnvironment(); + env.insert("__COMPAT_LAYER", "RUNASINVOKER"); + proc.setProcessEnvironment(env); +#else + app_exe_name.prepend("bin/"); +#endif + + auto app_exe_path = target.absoluteFilePath(app_exe_name); + proc.startDetached(app_exe_path); + + exit(error ? 1 : 0); +} + +void PrismUpdaterApp::printReleases() +{ + for (auto release : m_releases) { + std::cout << release.name.toStdString() << " Version: " << release.tag_name.toStdString() << std::endl; + } +} + +QList PrismUpdaterApp::nonDraftReleases() +{ + QList nonDraft; + for (auto rls : m_releases) { + if (rls.isValid() && !rls.draft) + nonDraft.append(rls); + } + return nonDraft; +} + +QList PrismUpdaterApp::newerReleases() +{ + QList newer; + for (auto rls : nonDraftReleases()) { + if (rls.version > m_prismVersion) + newer.append(rls); + } + return newer; +} + +GitHubRelease PrismUpdaterApp::selectRelease() +{ + QList releases; + + if (m_allowDowngrade) { + releases = nonDraftReleases(); + } else { + releases = newerReleases(); + } + + if (releases.isEmpty()) + return {}; + + SelectReleaseDialog dlg(Version(m_prismVersion), releases); + auto result = dlg.exec(); + + if (result == QDialog::Rejected) { + return {}; + } + GitHubRelease release = dlg.selectedRelease(); + + return release; +} + +QList PrismUpdaterApp::validReleaseArtifacts(const GitHubRelease& release) +{ + QList valid; + + qDebug() << "Selecting best asset from" << release.tag_name << "for platform" << BuildConfig.BUILD_ARTIFACT + << "portable:" << m_isPortable; + if (BuildConfig.BUILD_ARTIFACT.isEmpty()) + qWarning() << "Build platform is not set!"; + for (auto asset : release.assets) { + if (!m_isAppimage && asset.name.toLower().endsWith("appimage")) { + qDebug() << "Rejecting" << asset.name << "because it is an AppImage"; + continue; + } else if (m_isAppimage && !asset.name.toLower().endsWith("appimage")) { + qDebug() << "Rejecting" << asset.name << "because it is not an AppImage"; + continue; + } + auto asset_name = asset.name.toLower(); + auto [platform, platform_qt_ver] = StringUtils::splitFirst(BuildConfig.BUILD_ARTIFACT.toLower(), "-qt"); + auto system_is_arm = QSysInfo::buildCpuArchitecture().contains("arm64"); + auto asset_is_arm = asset_name.contains("arm64"); + auto asset_is_archive = asset_name.endsWith(".zip") || asset_name.endsWith(".tar.gz"); + + bool for_platform = !platform.isEmpty() && asset_name.contains(platform); + if (!for_platform) { + qDebug() << "Rejecting" << asset.name << "because platforms do not match"; + } + bool for_portable = asset_name.contains("portable"); + if (for_platform && asset_name.contains("legacy") && !platform.contains("legacy")) { + qDebug() << "Rejecting" << asset.name << "because platforms do not match"; + for_platform = false; + } + if (for_platform && ((asset_is_arm && !system_is_arm) || (!asset_is_arm && system_is_arm))) { + qDebug() << "Rejecting" << asset.name << "because architecture does not match"; + for_platform = false; + } + if (for_platform && platform.contains("windows") && !m_isPortable && asset_is_archive) { + qDebug() << "Rejecting" << asset.name << "because it is not an installer"; + for_platform = false; + } + + auto qt_pattern = QRegularExpression("-qt(\\d+)"); + auto qt_match = qt_pattern.match(asset_name); + if (for_platform && qt_match.hasMatch()) { + if (platform_qt_ver.isEmpty() || platform_qt_ver.toInt() != qt_match.captured(1).toInt()) { + qDebug() << "Rejecting" << asset.name << "because it is not for the correct qt version" << platform_qt_ver.toInt() << "vs" + << qt_match.captured(1).toInt(); + for_platform = false; + } + } + + if (((m_isPortable && for_portable) || (!m_isPortable && !for_portable)) && for_platform) { + qDebug() << "Accepting" << asset.name; + valid.append(asset); + } + } + return valid; +} + +GitHubReleaseAsset PrismUpdaterApp::selectAsset(const QList& assets) +{ + SelectReleaseAssetDialog dlg(assets); + auto result = dlg.exec(); + + if (result == QDialog::Rejected) { + return {}; + } + + GitHubReleaseAsset asset = dlg.selectedAsset(); + return asset; +} + +void PrismUpdaterApp::performUpdate(const GitHubRelease& release) +{ + m_install_release = release; + qDebug() << "Updating to" << release.tag_name; + auto valid_assets = validReleaseArtifacts(release); + qDebug() << "valid release assets:" << valid_assets; + + GitHubReleaseAsset selected_asset; + if (valid_assets.isEmpty()) { + return showFatalErrorMessage( + tr("No Valid Release Assets"), + tr("Github release %1 has no valid assets for this platform: %2") + .arg(release.tag_name) + .arg(tr("%1 portable: %2").arg(BuildConfig.BUILD_ARTIFACT).arg(m_isPortable ? tr("yes") : tr("no")))); + } else if (valid_assets.length() > 1) { + selected_asset = selectAsset(valid_assets); + } else { + selected_asset = valid_assets.takeFirst(); + } + + if (!selected_asset.isValid()) { + return showFatalErrorMessage(tr("No version selected."), tr("No version was selected.")); + } + + qDebug() << "will install" << selected_asset; + auto file = downloadAsset(selected_asset); + + if (!file.exists()) { + return showFatalErrorMessage(tr("Failed to Download"), tr("Failed to download the selected asset.")); + } + + performInstall(file); +} + +QFileInfo PrismUpdaterApp::downloadAsset(const GitHubReleaseAsset& asset) +{ + auto temp_dir = QDir::tempPath(); + auto file_url = QUrl(asset.browser_download_url); + auto out_file_path = FS::PathCombine(temp_dir, file_url.fileName()); + + qDebug() << "downloading" << file_url << "to" << out_file_path; + auto download = Net::Download::makeFile(file_url, out_file_path); + download->setNetwork(m_network); + auto progress_dialog = ProgressDialog(); + progress_dialog.adjustSize(); + + progress_dialog.execWithTask(download.get()); + + qDebug() << "download complete"; + + QFileInfo out_file(out_file_path); + return out_file; +} + +bool PrismUpdaterApp::callAppImageUpdate() +{ + auto appimage_path = QProcessEnvironment::systemEnvironment().value(QStringLiteral("APPIMAGE")); + QProcess proc = QProcess(); + qDebug() << "Calling: AppImageUpdate" << appimage_path; + proc.setProgram(FS::PathCombine(m_rootPath, "bin", "AppImageUpdate-x86_64.AppImage")); + proc.setArguments({ appimage_path }); + auto result = proc.startDetached(); + if (!result) + qDebug() << "Failed to start AppImageUpdate reason:" << proc.errorString(); + return result; +} + +void PrismUpdaterApp::clearUpdateLog() +{ + QFile::remove(m_updateLogPath); +} + +void PrismUpdaterApp::logUpdate(const QString& msg) +{ + qDebug() << qUtf8Printable(msg); + FS::append(m_updateLogPath, QStringLiteral("%1\n").arg(msg).toUtf8()); +} + +std::tuple read_lock_File(const QString& path) +{ + auto contents = QString(FS::read(path)); + auto lines = contents.split('\n'); + + QDateTime timestamp; + QString from, to, target, data_path; + for (auto line : lines) { + auto index = line.indexOf("="); + if (index < 0) + continue; + auto left = line.left(index); + auto right = line.mid(index + 1); + if (left.toLower() == "timestamp") { + timestamp = QDateTime::fromString(right, Qt::ISODate); + } else if (left.toLower() == "from") { + from = right; + } else if (left.toLower() == "to") { + to = right; + } else if (left.toLower() == "target") { + target = right; + } else if (left.toLower() == "data_path") { + data_path = right; + } + } + return std::make_tuple(timestamp, from, to, target, data_path); +} + +bool write_lock_file(const QString& path, QDateTime timestamp, QString from, QString to, QString target, QString data_path) +{ + try { + FS::write(path, QStringLiteral("TIMESTAMP=%1\nFROM=%2\nTO=%3\nTARGET=%4\nDATA_PATH=%5\n") + .arg(timestamp.toString(Qt::ISODate)) + .arg(from) + .arg(to) + .arg(target) + .arg(data_path) + .toUtf8()); + } catch (FS::FileSystemException& err) { + qWarning() << "Error writing lockfile:" << err.what() << "\n" << err.cause(); + return false; + } + return true; +} + +void PrismUpdaterApp::performInstall(QFileInfo file) +{ + qDebug() << "starting install"; + auto update_lock_path = FS::PathCombine(m_dataPath, ".prism_launcher_update.lock"); + QFileInfo update_lock(update_lock_path); + if (update_lock.exists()) { + auto [timestamp, from, to, target, data_path] = read_lock_File(update_lock_path); + auto msg = tr("Update already in progress\n"); + auto infoMsg = + tr("This installation has a update lock file present at: %1\n" + "\n" + "Timestamp: %2\n" + "Updating from version %3 to %4\n" + "Target install path: %5\n" + "Data Path: %6" + "\n" + "This likely means that a previous update attempt failed. Please ensure your installation is in working order before " + "proceeding.\n" + "Check the Prism Launcher updater log at: \n" + "%7\n" + "for details on the last update attempt.\n" + "\n" + "To overwrite this lock and proceed with this update anyway, select \"Ignore\" below.") + .arg(update_lock_path) + .arg(timestamp.toString(Qt::ISODate), from, to, target, data_path) + .arg(m_updateLogPath); + QMessageBox msgBox; + msgBox.setText(msg); + msgBox.setInformativeText(infoMsg); + msgBox.setStandardButtons(QMessageBox::Ignore | QMessageBox::Cancel); + msgBox.setDefaultButton(QMessageBox::Cancel); + msgBox.setMinimumWidth(460); + msgBox.adjustSize(); + switch (msgBox.exec()) { + case QMessageBox::AcceptRole: + break; + case QMessageBox::RejectRole: + [[fallthrough]]; + default: + return showFatalErrorMessage(tr("Update Aborted"), tr("The update attempt was aborted")); + } + } + clearUpdateLog(); + + auto changelog_path = FS::PathCombine(m_dataPath, ".prism_launcher_update.changelog"); + FS::write(changelog_path, m_install_release.body.toUtf8()); + + logUpdate(tr("Updating from %1 to %2").arg(m_prismVersion).arg(m_install_release.tag_name)); + if (m_isPortable || file.suffix().toLower() == "zip") { + write_lock_file(update_lock_path, QDateTime::currentDateTime(), m_prismVersion, m_install_release.tag_name, m_rootPath, m_dataPath); + logUpdate(tr("Updating portable install at %1").arg(m_rootPath)); + unpackAndInstall(file); + } else { + logUpdate(tr("Running installer file at %1").arg(file.absoluteFilePath())); + QProcess proc = QProcess(); +#if defined Q_OS_WIN + auto env = QProcessEnvironment::systemEnvironment(); + env.insert("__COMPAT_LAYER", "RUNASINVOKER"); + proc.setProcessEnvironment(env); +#endif + proc.setProgram(file.absoluteFilePath()); + bool result = proc.startDetached(); + logUpdate(tr("Process start result: %1").arg(result ? tr("yes") : tr("no"))); + exit(result ? 0 : 1); + } +} + +void PrismUpdaterApp::unpackAndInstall(QFileInfo archive) +{ + logUpdate(tr("Backing up install")); + backupAppDir(); + + if (auto loc = unpackArchive(archive)) { + auto marker_file_path = loc.value().absoluteFilePath(".prism_launcher_updater_unpack.marker"); + FS::write(marker_file_path, m_rootPath.toUtf8()); + + QProcess proc = QProcess(); + + auto exe_name = QStringLiteral("%1_updater").arg(BuildConfig.LAUNCHER_APP_BINARY_NAME); +#if defined Q_OS_WIN32 + exe_name.append(".exe"); + + auto env = QProcessEnvironment::systemEnvironment(); + env.insert("__COMPAT_LAYER", "RUNASINVOKER"); + proc.setProcessEnvironment(env); +#else + exe_name.prepend("bin/"); +#endif + + auto new_updater_path = loc.value().absoluteFilePath(exe_name); + logUpdate(tr("Starting new updater at '%1'").arg(new_updater_path)); + if (!proc.startDetached(new_updater_path, { "-d", m_dataPath }, loc.value().absolutePath())) { + logUpdate(tr("Failed to launch '%1' %2").arg(new_updater_path).arg(proc.errorString())); + return exit(10); + } + return exit(); // up to the new updater now + } + return exit(1); // unpack failure +} + +void PrismUpdaterApp::backupAppDir() +{ + auto manifest_path = FS::PathCombine(m_rootPath, "manifest.txt"); + QFileInfo manifest(manifest_path); + + QStringList file_list; + if (manifest.isFile()) { + // load manifest from file + + logUpdate(tr("Reading manifest from %1").arg(manifest.absoluteFilePath())); + try { + auto contents = QString::fromUtf8(FS::read(manifest.absoluteFilePath())); + auto files = contents.split('\n'); + for (auto file : files) { + file_list.append(file.trimmed()); + } + } catch (FS::FileSystemException&) { + } + } + + if (file_list.isEmpty()) { + // best guess + if (BuildConfig.BUILD_ARTIFACT.toLower() == "linux") { + file_list.append({ "PrismLauncher", "bin", "share", "lib" }); + } else { // windows by process of elimination + file_list.append({ + "jars", + "prismlauncher.exe", + "prismlauncher_filelink.exe", + "prismlauncher_updater.exe", + "qtlogging.ini", + "imageformats", + "iconengines", + "platforms", + "styles", + "tls", + "qt.conf", + "Qt*.dll", + }); + } + file_list.append("portable.txt"); + logUpdate("manifest.txt empty or missing. making best guess at files to back up."); + } + logUpdate(tr("Backing up:\n %1").arg(file_list.join(",\n "))); + auto app_dir = QDir(m_rootPath); + auto backup_dir = FS::PathCombine( + app_dir.absolutePath(), + QStringLiteral("backup_") + + QString(m_prismVersion).replace(QRegularExpression("[" + QRegularExpression::escape("\\/:*?\"<>|") + "]"), QString("_")) + "-" + + m_prismGitCommit); + FS::ensureFolderPathExists(backup_dir); + auto backup_marker_path = FS::PathCombine(m_dataPath, ".prism_launcher_update_backup_path.txt"); + FS::write(backup_marker_path, backup_dir.toUtf8()); + + QProgressDialog progress(tr("Backing up install at %1").arg(m_rootPath), "", 0, file_list.length()); + progress.setCancelButton(nullptr); + progress.setMinimumWidth(400); + progress.adjustSize(); + progress.show(); + QCoreApplication::processEvents(); + + logUpdate(tr("Backing up install at %1").arg(m_rootPath)); + + auto copy = [this, app_dir, backup_dir](QString to_bak_file) { + auto rel_path = app_dir.relativeFilePath(to_bak_file); + auto bak_path = FS::PathCombine(backup_dir, rel_path); + logUpdate(tr("Backing up and then removing %1").arg(to_bak_file)); + FS::ensureFilePathExists(bak_path); + auto result = FS::copy(to_bak_file, bak_path).overwrite(true)(); + if (!result) { + logUpdate(tr("Failed to backup %1 to %2").arg(to_bak_file).arg(bak_path)); + } else { + if (!FS::deletePath(to_bak_file)) + logUpdate(tr("Failed to remove %1").arg(to_bak_file)); + } + }; + + int i = 0; + for (auto glob : file_list) { + QDirIterator iter(app_dir.absolutePath(), QStringList({ glob }), QDir::Files | QDir::Dirs | QDir::NoDotAndDotDot); + progress.setValue(i); + QCoreApplication::processEvents(); + if (!iter.hasNext() && !glob.isEmpty()) { + if (auto file_info = QFileInfo(FS::PathCombine(app_dir.absolutePath(), glob)); file_info.exists()) { + copy(file_info.absoluteFilePath()); + } else { + logUpdate(tr("File doesn't exist, ignoring: %1").arg(FS::PathCombine(app_dir.absolutePath(), glob))); + } + } else { + while (iter.hasNext()) { + copy(iter.next()); + } + } + i++; + } + progress.setValue(i); + QCoreApplication::processEvents(); +} + +std::optional PrismUpdaterApp::unpackArchive(QFileInfo archive) +{ + auto temp_extract_path = FS::PathCombine(m_dataPath, "prism_launcher_update_release"); + FS::ensureFolderPathExists(temp_extract_path); + auto tmp_extract_dir = QDir(temp_extract_path); + + if (archive.fileName().endsWith(".zip")) { + auto result = MMCZip::extractDir(archive.absoluteFilePath(), tmp_extract_dir.absolutePath()); + if (result) { + logUpdate(tr("Extracted the following to \"%1\":\n %2").arg(tmp_extract_dir.absolutePath()).arg(result->join("\n "))); + } else { + logUpdate(tr("Failed to extract %1 to %2").arg(archive.absoluteFilePath()).arg(tmp_extract_dir.absolutePath())); + showFatalErrorMessage("Failed to extract archive", + tr("Failed to extract %1 to %2").arg(archive.absoluteFilePath()).arg(tmp_extract_dir.absolutePath())); + return std::nullopt; + } + + } else if (archive.fileName().endsWith(".tar.gz")) { + QString cmd = "tar"; + QStringList args = { "-xvf", archive.absoluteFilePath(), "-C", tmp_extract_dir.absolutePath() }; + logUpdate(tr("Running: `%1 %2`").arg(cmd).arg(args.join(" "))); + QProcess proc = QProcess(); + proc.start(cmd, args); + if (!proc.waitForStarted(5000)) { // wait 5 seconds to start + auto msg = tr("Failed to launcher child process \"%1 %2\".").arg(cmd).arg(args.join(" ")); + logUpdate(msg); + showFatalErrorMessage(tr("Failed extract archive"), msg); + return std::nullopt; + } + auto result = proc.waitForFinished(5000); + auto out = proc.readAll(); + logUpdate(out); + if (!result) { + auto msg = tr("Child process \"%1 %2\" failed.").arg(cmd).arg(args.join(" ")); + logUpdate(msg); + showFatalErrorMessage(tr("Failed to extract archive"), msg); + return std::nullopt; + } + + } else { + logUpdate(tr("Unknown archive format for %1").arg(archive.absoluteFilePath())); + showFatalErrorMessage("Can not extract", QStringLiteral("Unknown archive format %1").arg(archive.absoluteFilePath())); + return std::nullopt; + } + + return tmp_extract_dir; +} + +bool PrismUpdaterApp::loadPrismVersionFromExe(const QString& exe_path) +{ + QProcess proc = QProcess(); + proc.setProcessChannelMode(QProcess::MergedChannels); + proc.setReadChannel(QProcess::StandardOutput); + proc.start(exe_path, { "--version" }); + if (!proc.waitForStarted(5000)) { + showFatalErrorMessage(tr("Failed to Check Version"), tr("Failed to launcher child launcher process to read version.")); + return false; + } // wait 5 seconds to start + if (!proc.waitForFinished(5000)) { + showFatalErrorMessage(tr("Failed to Check Version"), tr("Child launcher process failed.")); + return false; + } + auto out = proc.readAllStandardOutput(); + auto lines = out.split('\n'); + lines.removeAll(""); + if (lines.length() < 2) + return false; + else if (lines.length() > 2) { + auto line1 = lines.takeLast(); + auto line2 = lines.takeLast(); + lines = { line2, line1 }; + } + auto first = lines.takeFirst(); + auto first_parts = first.split(' '); + if (first_parts.length() < 2) + return false; + m_prismBinaryName = first_parts.takeFirst(); + auto version = first_parts.takeFirst().trimmed(); + m_prismVersion = version; + if (version.contains('-')) { + auto index = version.indexOf('-'); + m_prsimVersionChannel = version.mid(index + 1); + version = version.left(index); + } else { + m_prsimVersionChannel = "stable"; + } + auto version_parts = version.split('.'); + if (version_parts.length() < 2) + return false; + m_prismVersionMajor = version_parts.takeFirst().toInt(); + m_prismVersionMinor = version_parts.takeFirst().toInt(); + m_prismGitCommit = lines.takeFirst().simplified(); + return true; +} + +void PrismUpdaterApp::loadReleaseList() +{ + auto github_repo = m_prismRepoUrl; + if (github_repo.host() != "github.com") + return fail("updating from a non github url is not supported"); + + auto path_parts = github_repo.path().split('/'); + path_parts.removeFirst(); // empty segment from leading / + auto repo_owner = path_parts.takeFirst(); + auto repo_name = path_parts.takeFirst(); + auto api_url = QString("https://api.github.com/repos/%1/%2/releases").arg(repo_owner, repo_name); + + qDebug() << "Fetching release list from" << api_url; + + downloadReleasePage(api_url, 1); +} + +void PrismUpdaterApp::downloadReleasePage(const QString& api_url, int page) +{ + int per_page = 30; + auto page_url = QString("%1?per_page=%2&page=%3").arg(api_url).arg(QString::number(per_page)).arg(QString::number(page)); + auto response = std::make_shared(); + auto download = Net::Download::makeByteArray(page_url, response); + download->setNetwork(m_network); + m_current_url = page_url; + + auto github_api_headers = new Net::RawHeaderProxy(); + github_api_headers->addHeaders({ + { "Accept", "application/vnd.github+json" }, + { "X-GitHub-Api-Version", "2022-11-28" }, + }); + download->addHeaderProxy(github_api_headers); + + connect(download.get(), &Net::Download::succeeded, this, [this, response, per_page, api_url, page]() { + int num_found = parseReleasePage(response.get()); + if (!(num_found < per_page)) { // there may be more, fetch next page + downloadReleasePage(api_url, page + 1); + } else { + run(); + } + }); + connect(download.get(), &Net::Download::failed, this, &PrismUpdaterApp::downloadError); + + m_current_task.reset(download); + connect(download.get(), &Net::Download::finished, this, [this]() { + qDebug() << "Download" << m_current_task->getUid().toString() << "finished"; + m_current_task.reset(); + m_current_url = ""; + }); + + QCoreApplication::processEvents(); + + QMetaObject::invokeMethod(download.get(), &Task::start, Qt::QueuedConnection); +} + +int PrismUpdaterApp::parseReleasePage(const QByteArray* response) +{ + if (response->isEmpty()) // empty page + return 0; + int num_releases = 0; + try { + auto doc = Json::requireDocument(*response); + auto release_list = Json::requireArray(doc); + for (auto release_json : release_list) { + auto release_obj = Json::requireObject(release_json); + + GitHubRelease release = {}; + release.id = Json::requireInteger(release_obj, "id"); + release.name = Json::ensureString(release_obj, "name"); + release.tag_name = Json::requireString(release_obj, "tag_name"); + release.created_at = QDateTime::fromString(Json::requireString(release_obj, "created_at"), Qt::ISODate); + release.published_at = QDateTime::fromString(Json::ensureString(release_obj, "published_at"), Qt::ISODate); + release.draft = Json::requireBoolean(release_obj, "draft"); + release.prerelease = Json::requireBoolean(release_obj, "prerelease"); + release.body = Json::ensureString(release_obj, "body"); + release.version = Version(release.tag_name); + + auto release_assets_obj = Json::requireArray(release_obj, "assets"); + for (auto asset_json : release_assets_obj) { + auto asset_obj = Json::requireObject(asset_json); + GitHubReleaseAsset asset = {}; + asset.id = Json::requireInteger(asset_obj, "id"); + asset.name = Json::requireString(asset_obj, "name"); + asset.label = Json::ensureString(asset_obj, "label"); + asset.content_type = Json::requireString(asset_obj, "content_type"); + asset.size = Json::requireInteger(asset_obj, "size"); + asset.created_at = QDateTime::fromString(Json::requireString(asset_obj, "created_at"), Qt::ISODate); + asset.updated_at = QDateTime::fromString(Json::requireString(asset_obj, "updated_at"), Qt::ISODate); + asset.browser_download_url = Json::requireString(asset_obj, "browser_download_url"); + release.assets.append(asset); + } + m_releases.append(release); + num_releases++; + } + } catch (Json::JsonException& e) { + auto err_msg = + QString("Failed to parse releases from github: %1\n%2").arg(e.what()).arg(QString::fromStdString(response->toStdString())); + fail(err_msg); + } + return num_releases; +} + +GitHubRelease PrismUpdaterApp::getLatestRelease() +{ + GitHubRelease latest; + for (auto release : m_releases) { + if (release.draft) + continue; + if (release.prerelease && !m_allowPreRelease) + continue; + if (!latest.isValid() || (release.version > latest.version)) { + latest = release; + } + } + return latest; +} + +bool PrismUpdaterApp::needUpdate(const GitHubRelease& release) +{ + auto current_ver = Version(QString("%1.%2").arg(QString::number(m_prismVersionMajor)).arg(QString::number(m_prismVersionMinor))); + return current_ver < release.version; +} + +void PrismUpdaterApp::downloadError(QString reason) +{ + fail(QString("Network request Failed: %1 with reason %2").arg(m_current_url).arg(reason)); +} diff --git a/launcher/updater/prismupdater/PrismUpdater.h b/launcher/updater/prismupdater/PrismUpdater.h new file mode 100644 index 000000000..f3dd6e062 --- /dev/null +++ b/launcher/updater/prismupdater/PrismUpdater.h @@ -0,0 +1,143 @@ +// SPDX-FileCopyrightText: 2023 Rachel Powers <508861+Ryex@users.noreply.github.com> +// +// 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 +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include "QObjectPtr.h" +#include "net/Download.h" + +#define PRISM_EXTERNAL_EXE +#include "FileSystem.h" + +#include "GitHubRelease.h" + +class PrismUpdaterApp : public QApplication { + // friends for the purpose of limiting access to deprecated stuff + Q_OBJECT + public: + enum Status { Starting, Failed, Succeeded, Initialized, Aborted }; + PrismUpdaterApp(int& argc, char** argv); + virtual ~PrismUpdaterApp(); + void loadReleaseList(); + void run(); + Status status() const { return m_status; } + + private: + void fail(const QString& reason); + void abort(const QString& reason); + void showFatalErrorMessage(const QString& title, const QString& content); + + bool loadPrismVersionFromExe(const QString& exe_path); + + void downloadReleasePage(const QString& api_url, int page); + int parseReleasePage(const QByteArray* response); + + bool needUpdate(const GitHubRelease& release); + + GitHubRelease getLatestRelease(); + GitHubRelease selectRelease(); + QList newerReleases(); + QList nonDraftReleases(); + + void printReleases(); + + QList validReleaseArtifacts(const GitHubRelease& release); + GitHubReleaseAsset selectAsset(const QList& assets); + void performUpdate(const GitHubRelease& release); + void performInstall(QFileInfo file); + void unpackAndInstall(QFileInfo file); + void backupAppDir(); + std::optional unpackArchive(QFileInfo file); + + QFileInfo downloadAsset(const GitHubReleaseAsset& asset); + bool callAppImageUpdate(); + + void moveAndFinishUpdate(QDir target); + + public slots: + void downloadError(QString reason); + + private: + const QString& root() { return m_rootPath; } + + bool isPortable() { return m_isPortable; } + + void clearUpdateLog(); + void logUpdate(const QString& msg); + + QString m_rootPath; + QString m_dataPath; + bool m_isPortable = false; + bool m_isAppimage = false; + bool m_isFlatpak = false; + QString m_appimagePath; + QString m_prismExecutable; + QUrl m_prismRepoUrl; + Version m_userSelectedVersion; + bool m_checkOnly; + bool m_forceUpdate; + bool m_printOnly; + bool m_selectUI; + bool m_allowDowngrade; + bool m_allowPreRelease; + + QString m_updateLogPath; + + QString m_prismBinaryName; + QString m_prismVersion; + int m_prismVersionMajor = -1; + int m_prismVersionMinor = -1; + QString m_prsimVersionChannel; + QString m_prismGitCommit; + + GitHubRelease m_install_release; + + Status m_status = Status::Starting; + shared_qobject_ptr m_network; + QString m_current_url; + Task::Ptr m_current_task; + QList m_releases; + + public: + std::unique_ptr logFile; + bool logToConsole = false; + +#if defined Q_OS_WIN32 + // used on Windows to attach the standard IO streams + bool consoleAttached = false; +#endif +}; diff --git a/launcher/updater/prismupdater/SelectReleaseDialog.ui b/launcher/updater/prismupdater/SelectReleaseDialog.ui new file mode 100644 index 000000000..a1aa38371 --- /dev/null +++ b/launcher/updater/prismupdater/SelectReleaseDialog.ui @@ -0,0 +1,89 @@ + + + SelectReleaseDialog + + + + 0 + 0 + 468 + 385 + + + + Select Release to Install + + + true + + + + + + Please select the release you wish to update to. + + + + + + + true + + + + 1 + + + + + + + + + + + Qt::Horizontal + + + QDialogButtonBox::Cancel|QDialogButtonBox::Ok + + + + + + + + + buttonBox + accepted() + SelectReleaseDialog + accept() + + + 248 + 254 + + + 157 + 274 + + + + + buttonBox + rejected() + SelectReleaseDialog + reject() + + + 316 + 260 + + + 286 + 274 + + + + + diff --git a/launcher/updater/prismupdater/UpdaterDialogs.cpp b/launcher/updater/prismupdater/UpdaterDialogs.cpp new file mode 100644 index 000000000..395b658db --- /dev/null +++ b/launcher/updater/prismupdater/UpdaterDialogs.cpp @@ -0,0 +1,168 @@ +// SPDX-FileCopyrightText: 2023 Rachel Powers <508861+Ryex@users.noreply.github.com> +// +// 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 "UpdaterDialogs.h" + +#include "ui_SelectReleaseDialog.h" + +#include +#include "Markdown.h" + +SelectReleaseDialog::SelectReleaseDialog(const Version& current_version, const QList& releases, QWidget* parent) + : QDialog(parent), m_releases(releases), m_currentVersion(current_version), ui(new Ui::SelectReleaseDialog) +{ + ui->setupUi(this); + + ui->changelogTextBrowser->setOpenExternalLinks(true); + ui->changelogTextBrowser->setLineWrapMode(QTextBrowser::LineWrapMode::WidgetWidth); + ui->changelogTextBrowser->setVerticalScrollBarPolicy(Qt::ScrollBarPolicy::ScrollBarAsNeeded); + + ui->versionsTree->setColumnCount(2); + + ui->versionsTree->header()->setSectionResizeMode(0, QHeaderView::Stretch); + ui->versionsTree->header()->setSectionResizeMode(1, QHeaderView::ResizeToContents); + ui->versionsTree->setHeaderLabels({ tr("Version"), tr("Published Date") }); + ui->versionsTree->header()->setStretchLastSection(false); + + ui->eplainLabel->setText(tr("Select a version to install.\n" + "\n" + "Currently installed version: %1") + .arg(m_currentVersion.toString())); + + loadReleases(); + + connect(ui->versionsTree, &QTreeWidget::currentItemChanged, this, &SelectReleaseDialog::selectionChanged); + + connect(ui->buttonBox, &QDialogButtonBox::accepted, this, &SelectReleaseDialog::accept); + connect(ui->buttonBox, &QDialogButtonBox::rejected, this, &SelectReleaseDialog::reject); +} + +SelectReleaseDialog::~SelectReleaseDialog() +{ + delete ui; +} + +void SelectReleaseDialog::loadReleases() +{ + for (auto rls : m_releases) { + appendRelease(rls); + } +} + +void SelectReleaseDialog::appendRelease(GitHubRelease const& release) +{ + auto rls_item = new QTreeWidgetItem(ui->versionsTree); + rls_item->setText(0, release.tag_name); + rls_item->setExpanded(true); + rls_item->setText(1, release.published_at.toString()); + rls_item->setData(0, Qt::UserRole, QVariant(release.id)); + + ui->versionsTree->addTopLevelItem(rls_item); +} + +GitHubRelease SelectReleaseDialog::getRelease(QTreeWidgetItem* item) +{ + int id = item->data(0, Qt::UserRole).toInt(); + GitHubRelease release; + for (auto rls : m_releases) { + if (rls.id == id) + release = rls; + } + return release; +} + +void SelectReleaseDialog::selectionChanged(QTreeWidgetItem* current, QTreeWidgetItem* previous) +{ + GitHubRelease release = getRelease(current); + QString body = markdownToHTML(release.body.toUtf8()); + m_selectedRelease = release; + + ui->changelogTextBrowser->setHtml(body); +} + +SelectReleaseAssetDialog::SelectReleaseAssetDialog(const QList& assets, QWidget* parent) + : QDialog(parent), m_assets(assets), ui(new Ui::SelectReleaseDialog) +{ + ui->setupUi(this); + + ui->changelogTextBrowser->setOpenExternalLinks(true); + ui->changelogTextBrowser->setLineWrapMode(QTextBrowser::LineWrapMode::WidgetWidth); + ui->changelogTextBrowser->setVerticalScrollBarPolicy(Qt::ScrollBarPolicy::ScrollBarAsNeeded); + + ui->versionsTree->setColumnCount(2); + + ui->versionsTree->header()->setSectionResizeMode(0, QHeaderView::Stretch); + ui->versionsTree->header()->setSectionResizeMode(1, QHeaderView::ResizeToContents); + ui->versionsTree->setHeaderLabels({ tr("Version"), tr("Published Date") }); + ui->versionsTree->header()->setStretchLastSection(false); + + ui->eplainLabel->setText(tr("Select a version to install.")); + + ui->changelogTextBrowser->setHidden(true); + + loadAssets(); + + connect(ui->versionsTree, &QTreeWidget::currentItemChanged, this, &SelectReleaseAssetDialog::selectionChanged); + + connect(ui->buttonBox, &QDialogButtonBox::accepted, this, &SelectReleaseAssetDialog::accept); + connect(ui->buttonBox, &QDialogButtonBox::rejected, this, &SelectReleaseAssetDialog::reject); +} + +SelectReleaseAssetDialog::~SelectReleaseAssetDialog() +{ + delete ui; +} + +void SelectReleaseAssetDialog::loadAssets() +{ + for (auto rls : m_assets) { + appendAsset(rls); + } +} + +void SelectReleaseAssetDialog::appendAsset(GitHubReleaseAsset const& asset) +{ + auto rls_item = new QTreeWidgetItem(ui->versionsTree); + rls_item->setText(0, asset.name); + rls_item->setExpanded(true); + rls_item->setText(1, asset.updated_at.toString()); + rls_item->setData(0, Qt::UserRole, QVariant(asset.id)); + + ui->versionsTree->addTopLevelItem(rls_item); +} + +GitHubReleaseAsset SelectReleaseAssetDialog::getAsset(QTreeWidgetItem* item) +{ + int id = item->data(0, Qt::UserRole).toInt(); + GitHubReleaseAsset selected_asset; + for (auto asset : m_assets) { + if (asset.id == id) + selected_asset = asset; + } + return selected_asset; +} + +void SelectReleaseAssetDialog::selectionChanged(QTreeWidgetItem* current, QTreeWidgetItem* previous) +{ + GitHubReleaseAsset asset = getAsset(current); + m_selectedAsset = asset; +} diff --git a/launcher/updater/prismupdater/UpdaterDialogs.h b/launcher/updater/prismupdater/UpdaterDialogs.h new file mode 100644 index 000000000..e336c0e2c --- /dev/null +++ b/launcher/updater/prismupdater/UpdaterDialogs.h @@ -0,0 +1,75 @@ +// SPDX-FileCopyrightText: 2023 Rachel Powers <508861+Ryex@users.noreply.github.com> +// +// 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 + +#include "GitHubRelease.h" +#include "Version.h" + +namespace Ui { +class SelectReleaseDialog; +} + +class SelectReleaseDialog : public QDialog { + Q_OBJECT + + public: + explicit SelectReleaseDialog(const Version& cur_version, const QList& releases, QWidget* parent = 0); + ~SelectReleaseDialog(); + + void loadReleases(); + void appendRelease(GitHubRelease const& release); + GitHubRelease selectedRelease() { return m_selectedRelease; } + private slots: + GitHubRelease getRelease(QTreeWidgetItem* item); + void selectionChanged(QTreeWidgetItem* current, QTreeWidgetItem* previous); + + protected: + QList m_releases; + GitHubRelease m_selectedRelease; + Version m_currentVersion; + + Ui::SelectReleaseDialog* ui; +}; + +class SelectReleaseAssetDialog : public QDialog { + Q_OBJECT + public: + explicit SelectReleaseAssetDialog(const QList& assets, QWidget* parent = 0); + ~SelectReleaseAssetDialog(); + + void loadAssets(); + void appendAsset(GitHubReleaseAsset const& asset); + GitHubReleaseAsset selectedAsset() { return m_selectedAsset; } + private slots: + GitHubReleaseAsset getAsset(QTreeWidgetItem* item); + void selectionChanged(QTreeWidgetItem* current, QTreeWidgetItem* previous); + + protected: + QList m_assets; + GitHubReleaseAsset m_selectedAsset; + + Ui::SelectReleaseDialog* ui; +}; diff --git a/launcher/updater/prismupdater/updater.exe.manifest b/launcher/updater/prismupdater/updater.exe.manifest new file mode 100644 index 000000000..2bce76b77 --- /dev/null +++ b/launcher/updater/prismupdater/updater.exe.manifest @@ -0,0 +1,26 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/launcher/updater/prismupdater/updater_main.cpp b/launcher/updater/prismupdater/updater_main.cpp new file mode 100644 index 000000000..89c1d1198 --- /dev/null +++ b/launcher/updater/prismupdater/updater_main.cpp @@ -0,0 +1,40 @@ +// SPDX-FileCopyrightText: 2022 Rachel Powers <508861+Ryex@users.noreply.github.com> +// +// 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 . + * + */ + +#include "PrismUpdater.h" +int main(int argc, char* argv[]) +{ + PrismUpdaterApp wUpApp(argc, argv); + + switch (wUpApp.status()) { + case PrismUpdaterApp::Starting: + case PrismUpdaterApp::Initialized: { + return wUpApp.exec(); + } + case PrismUpdaterApp::Failed: + return 1; + case PrismUpdaterApp::Succeeded: + return 0; + default: + return -1; + } +} diff --git a/libraries/launcher/CMakeLists.txt b/libraries/launcher/CMakeLists.txt index 55ed58756..7bf160760 100644 --- a/libraries/launcher/CMakeLists.txt +++ b/libraries/launcher/CMakeLists.txt @@ -11,15 +11,33 @@ set(SRC org/prismlauncher/launcher/Launcher.java org/prismlauncher/launcher/impl/AbstractLauncher.java org/prismlauncher/launcher/impl/StandardLauncher.java - org/prismlauncher/launcher/impl/legacy/LegacyLauncher.java - org/prismlauncher/launcher/impl/legacy/LegacyFrame.java org/prismlauncher/exception/ParameterNotFoundException.java org/prismlauncher/exception/ParseException.java org/prismlauncher/utils/Parameters.java org/prismlauncher/utils/ReflectionUtils.java org/prismlauncher/utils/logging/Level.java org/prismlauncher/utils/logging/Log.java - net/minecraft/Launcher.java + org/prismlauncher/legacy/LegacyProxy.java ) + +set(LEGACY_SRC + legacy/org/prismlauncher/legacy/LegacyFrame.java + legacy/org/prismlauncher/legacy/LegacyLauncher.java + legacy/org/prismlauncher/legacy/fix/online/Handler.java + legacy/org/prismlauncher/legacy/fix/online/OnlineFixes.java + legacy/org/prismlauncher/legacy/fix/online/SkinFix.java + legacy/org/prismlauncher/legacy/utils/Base64.java + legacy/org/prismlauncher/legacy/utils/api/MojangApi.java + legacy/org/prismlauncher/legacy/utils/api/Texture.java + legacy/org/prismlauncher/legacy/utils/json/JsonParseException.java + legacy/org/prismlauncher/legacy/utils/json/JsonParser.java + legacy/org/prismlauncher/legacy/utils/url/CustomUrlConnection.java + legacy/org/prismlauncher/legacy/utils/url/UrlUtils.java + legacy/net/minecraft/Launcher.java + legacy/org/prismlauncher/legacy/LegacyProxy.java +) + add_jar(NewLaunch ${SRC}) +add_jar(NewLaunchLegacy ${LEGACY_SRC} INCLUDE_JARS NewLaunch) install_jar(NewLaunch "${JARS_DEST_DIR}") +install_jar(NewLaunchLegacy "${JARS_DEST_DIR}") diff --git a/libraries/launcher/net/minecraft/Launcher.java b/libraries/launcher/legacy/net/minecraft/Launcher.java similarity index 98% rename from libraries/launcher/net/minecraft/Launcher.java rename to libraries/launcher/legacy/net/minecraft/Launcher.java index 10cfa2ac0..933a81433 100644 --- a/libraries/launcher/net/minecraft/Launcher.java +++ b/libraries/launcher/legacy/net/minecraft/Launcher.java @@ -92,12 +92,11 @@ public final class Launcher extends Applet implements AppletStub { try { if (documentBase == null) { - if (applet.getClass().getPackage().getName().startsWith("com.mojang.")) { + if (applet.getClass().getPackage().getName().startsWith("com.mojang")) // Special case only for Classic versions documentBase = new URL("http://www.minecraft.net:80/game/"); - } else { + else documentBase = new URL("http://www.minecraft.net/game/"); - } } } catch (MalformedURLException e) { throw new AssertionError(e); diff --git a/libraries/launcher/org/prismlauncher/launcher/impl/legacy/LegacyFrame.java b/libraries/launcher/legacy/org/prismlauncher/legacy/LegacyFrame.java similarity index 97% rename from libraries/launcher/org/prismlauncher/launcher/impl/legacy/LegacyFrame.java rename to libraries/launcher/legacy/org/prismlauncher/legacy/LegacyFrame.java index 6cfe35d86..8276c23ef 100644 --- a/libraries/launcher/org/prismlauncher/launcher/impl/legacy/LegacyFrame.java +++ b/libraries/launcher/legacy/org/prismlauncher/legacy/LegacyFrame.java @@ -52,7 +52,7 @@ * limitations under the License. */ -package org.prismlauncher.launcher.impl.legacy; +package org.prismlauncher.legacy; import org.prismlauncher.utils.logging.Log; @@ -74,7 +74,7 @@ import javax.swing.JFrame; import net.minecraft.Launcher; -public final class LegacyFrame extends JFrame { +final class LegacyFrame extends JFrame { private static final long serialVersionUID = 1L; private final Launcher launcher; @@ -130,7 +130,7 @@ public final class LegacyFrame extends JFrame { launcher.setParameter("username", user); launcher.setParameter("sessionid", session); - launcher.setParameter("stand-alone", true); // Show the quit button. TODO: why won't this work? + launcher.setParameter("stand-alone", true); // Show the quit button. This often doesn't seem to work. launcher.setParameter("haspaid", true); // Some old versions need this for world saves to work. launcher.setParameter("demo", demo); launcher.setParameter("fullscreen", false); diff --git a/libraries/launcher/org/prismlauncher/launcher/impl/legacy/LegacyLauncher.java b/libraries/launcher/legacy/org/prismlauncher/legacy/LegacyLauncher.java similarity index 75% rename from libraries/launcher/org/prismlauncher/launcher/impl/legacy/LegacyLauncher.java rename to libraries/launcher/legacy/org/prismlauncher/legacy/LegacyLauncher.java index 5c5f86d47..02f77e039 100644 --- a/libraries/launcher/org/prismlauncher/launcher/impl/legacy/LegacyLauncher.java +++ b/libraries/launcher/legacy/org/prismlauncher/legacy/LegacyLauncher.java @@ -53,23 +53,27 @@ * limitations under the License. */ -package org.prismlauncher.launcher.impl.legacy; +package org.prismlauncher.legacy; import org.prismlauncher.launcher.impl.AbstractLauncher; import org.prismlauncher.utils.Parameters; import org.prismlauncher.utils.ReflectionUtils; import org.prismlauncher.utils.logging.Log; +import java.applet.Applet; import java.io.File; import java.lang.invoke.MethodHandle; +import java.lang.invoke.MethodHandles; +import java.lang.invoke.MethodType; import java.lang.reflect.Field; +import java.lang.reflect.Modifier; import java.util.Collections; import java.util.List; /** - * Used to launch old versions that support applets. + * Used to launch old versions which support applets. */ -public final class LegacyLauncher extends AbstractLauncher { +final class LegacyLauncher extends AbstractLauncher { private final String user, session; private final String title; private final String appletClass; @@ -93,11 +97,9 @@ public final class LegacyLauncher extends AbstractLauncher { @Override public void launch() throws Throwable { Class main = ClassLoader.getSystemClassLoader().loadClass(mainClassName); - Field gameDirField = ReflectionUtils.findMinecraftGameDirField(main); + Field gameDirField = findMinecraftGameDirField(main); - if (gameDirField == null) - Log.warning("Could not find Minecraft folder field"); - else { + if (gameDirField != null) { gameDirField.setAccessible(true); gameDirField.set(null, new File(gameDir)); } @@ -106,7 +108,7 @@ public final class LegacyLauncher extends AbstractLauncher { System.setProperty("minecraft.applet.TargetDirectory", gameDir); try { - LegacyFrame window = new LegacyFrame(title, ReflectionUtils.createAppletClass(appletClass)); + LegacyFrame window = new LegacyFrame(title, createAppletClass(appletClass)); window.start(user, session, width, height, maximize, serverAddress, serverPort, gameArgs.contains("--demo")); return; @@ -115,9 +117,39 @@ public final class LegacyLauncher extends AbstractLauncher { } } - // find and invoke the main method, this time without size parameters - // in all versions that support applets, these are ignored + // find and invoke the main method, this time without size parameters - in all + // versions that support applets, these are ignored MethodHandle method = ReflectionUtils.findMainMethod(main); method.invokeExact(gameArgs.toArray(new String[0])); } + + private static Applet createAppletClass(String clazz) throws Throwable { + Class appletClass = ClassLoader.getSystemClassLoader().loadClass(clazz); + + MethodHandle appletConstructor = MethodHandles.lookup().findConstructor(appletClass, MethodType.methodType(void.class)); + return (Applet) appletConstructor.invoke(); + } + + private static Field findMinecraftGameDirField(Class clazz) { + // search for private static File + for (Field field : clazz.getDeclaredFields()) { + if (field.getType() != File.class) + continue; + + int fieldModifiers = field.getModifiers(); + + if (!Modifier.isStatic(fieldModifiers)) + continue; + + if (!Modifier.isPrivate(fieldModifiers)) + continue; + + if (Modifier.isFinal(fieldModifiers)) + continue; + + return field; + } + + return null; + } } diff --git a/libraries/launcher/legacy/org/prismlauncher/legacy/LegacyProxy.java b/libraries/launcher/legacy/org/prismlauncher/legacy/LegacyProxy.java new file mode 100644 index 000000000..4c5c28c52 --- /dev/null +++ b/libraries/launcher/legacy/org/prismlauncher/legacy/LegacyProxy.java @@ -0,0 +1,68 @@ +// SPDX-License-Identifier: GPL-3.0-only +/* + * Prism Launcher - Minecraft Launcher + * Copyright (C) 2022 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. + * + * Linking this library statically or dynamically with other modules is + * making a combined work based on this library. Thus, the terms and + * conditions of the GNU General Public License cover the whole + * combination. + * + * As a special exception, the copyright holders of this library give + * you permission to link this library with independent modules to + * produce an executable, regardless of the license terms of these + * independent modules, and to copy and distribute the resulting + * executable under terms of your choice, provided that you also meet, + * for each linked independent module, the terms and conditions of the + * license of that module. An independent module is a module which is + * not derived from or based on this library. If you modify this + * library, you may extend this exception to your version of the + * library, but you are not obliged to do so. If you do not wish to do + * so, delete this exception statement from your version. + * + * 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. + */ + +package org.prismlauncher.legacy; + +import org.prismlauncher.launcher.Launcher; +import org.prismlauncher.legacy.fix.online.OnlineFixes; +import org.prismlauncher.utils.Parameters; + +// implementation of LegacyProxy +public final class LegacyProxy { + public static Launcher createLauncher(Parameters params) { + return new LegacyLauncher(params); + } + + public static void applyOnlineFixes(Parameters parameters) { + OnlineFixes.apply(parameters); + } +} diff --git a/libraries/launcher/legacy/org/prismlauncher/legacy/fix/online/Handler.java b/libraries/launcher/legacy/org/prismlauncher/legacy/fix/online/Handler.java new file mode 100644 index 000000000..f85a8bc79 --- /dev/null +++ b/libraries/launcher/legacy/org/prismlauncher/legacy/fix/online/Handler.java @@ -0,0 +1,63 @@ +// SPDX-License-Identifier: GPL-3.0-only +/* + * Prism Launcher - Minecraft Launcher + * Copyright (C) 2022 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. + * + * Linking this library statically or dynamically with other modules is + * making a combined work based on this library. Thus, the terms and + * conditions of the GNU General Public License cover the whole + * combination. + * + * As a special exception, the copyright holders of this library give + * you permission to link this library with independent modules to + * produce an executable, regardless of the license terms of these + * independent modules, and to copy and distribute the resulting + * executable under terms of your choice, provided that you also meet, + * for each linked independent module, the terms and conditions of the + * license of that module. An independent module is a module which is + * not derived from or based on this library. If you modify this + * library, you may extend this exception to your version of the + * library, but you are not obliged to do so. If you do not wish to do + * so, delete this exception statement from your version. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +package org.prismlauncher.legacy.fix.online; + +import org.prismlauncher.legacy.utils.url.UrlUtils; + +import java.io.IOException; +import java.net.Proxy; +import java.net.URL; +import java.net.URLConnection; +import java.net.URLStreamHandler; + +final class Handler extends URLStreamHandler { + @Override + protected URLConnection openConnection(URL address) throws IOException { + return openConnection(address, null); + } + + @Override + protected URLConnection openConnection(URL address, Proxy proxy) throws IOException { + URLConnection result; + + // try skin fix + result = SkinFix.openConnection(address, proxy); + if (result != null) + return result; + + return UrlUtils.openConnection(address, proxy); + } +} diff --git a/libraries/launcher/legacy/org/prismlauncher/legacy/fix/online/OnlineFixes.java b/libraries/launcher/legacy/org/prismlauncher/legacy/fix/online/OnlineFixes.java new file mode 100644 index 000000000..88facff69 --- /dev/null +++ b/libraries/launcher/legacy/org/prismlauncher/legacy/fix/online/OnlineFixes.java @@ -0,0 +1,79 @@ +// SPDX-License-Identifier: GPL-3.0-only +/* + * Prism Launcher - Minecraft Launcher + * Copyright (C) 2022 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. + * + * Linking this library statically or dynamically with other modules is + * making a combined work based on this library. Thus, the terms and + * conditions of the GNU General Public License cover the whole + * combination. + * + * As a special exception, the copyright holders of this library give + * you permission to link this library with independent modules to + * produce an executable, regardless of the license terms of these + * independent modules, and to copy and distribute the resulting + * executable under terms of your choice, provided that you also meet, + * for each linked independent module, the terms and conditions of the + * license of that module. An independent module is a module which is + * not derived from or based on this library. If you modify this + * library, you may extend this exception to your version of the + * library, but you are not obliged to do so. If you do not wish to do + * so, delete this exception statement from your version. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +package org.prismlauncher.legacy.fix.online; + +import org.prismlauncher.legacy.utils.Base64; +import org.prismlauncher.legacy.utils.url.UrlUtils; +import org.prismlauncher.utils.Parameters; +import org.prismlauncher.utils.logging.Log; + +import java.net.URL; +import java.net.URLStreamHandler; +import java.net.URLStreamHandlerFactory; + +/** + * Fixes skins by redirecting to other URLs. + * + * @see {@link Handler} + * @see {@link UrlUtils} + */ +public final class OnlineFixes implements URLStreamHandlerFactory { + public static void apply(Parameters params) { + if (!"true".equals(params.getString("onlineFixes", null))) + return; + + if (!UrlUtils.isSupported() || !Base64.isSupported()) { + Log.warning("Cannot access the necessary Java internals for skin fix"); + Log.warning("Turning off online fixes in the settings will silence the warnings"); + return; + } + + try { + URL.setURLStreamHandlerFactory(new OnlineFixes()); + } catch (Error e) { + Log.warning("Cannot apply skin fix: URLStreamHandlerFactory is already set"); + Log.warning("Turning off online fixes in the settings will silence the warnings"); + } + } + + @Override + public URLStreamHandler createURLStreamHandler(String protocol) { + if ("http".equals(protocol)) + return new Handler(); + + return null; + } +} diff --git a/libraries/launcher/legacy/org/prismlauncher/legacy/fix/online/SkinFix.java b/libraries/launcher/legacy/org/prismlauncher/legacy/fix/online/SkinFix.java new file mode 100644 index 000000000..e734bdbc7 --- /dev/null +++ b/libraries/launcher/legacy/org/prismlauncher/legacy/fix/online/SkinFix.java @@ -0,0 +1,179 @@ +// 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. + * + * Linking this library statically or dynamically with other modules is + * making a combined work based on this library. Thus, the terms and + * conditions of the GNU General Public License cover the whole + * combination. + * + * As a special exception, the copyright holders of this library give + * you permission to link this library with independent modules to + * produce an executable, regardless of the license terms of these + * independent modules, and to copy and distribute the resulting + * executable under terms of your choice, provided that you also meet, + * for each linked independent module, the terms and conditions of the + * license of that module. An independent module is a module which is + * not derived from or based on this library. If you modify this + * library, you may extend this exception to your version of the + * library, but you are not obliged to do so. If you do not wish to do + * so, delete this exception statement from your version. + * + * 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. + */ + +package org.prismlauncher.legacy.fix.online; + +import org.prismlauncher.legacy.utils.api.MojangApi; +import org.prismlauncher.legacy.utils.api.Texture; +import org.prismlauncher.legacy.utils.url.CustomUrlConnection; +import org.prismlauncher.legacy.utils.url.UrlUtils; + +import java.awt.AlphaComposite; +import java.awt.Graphics2D; +import java.awt.image.BufferedImage; +import java.io.ByteArrayOutputStream; +import java.io.IOException; +import java.io.InputStream; +import java.net.Proxy; +import java.net.URL; +import java.net.URLConnection; + +import javax.imageio.ImageIO; + +final class SkinFix { + static URLConnection openConnection(URL address, Proxy proxy) throws IOException { + String skinOwner = findSkinOwner(address); + if (skinOwner != null) + // we need to correct the skin + return getSkinConnection(skinOwner, proxy); + + String capeOwner = findCapeOwner(address); + if (capeOwner != null) { + // since we do not need to process the image, open a direct connection bypassing + // Handler + Texture texture = MojangApi.getTexture(MojangApi.getUuid(capeOwner), "CAPE"); + if (texture == null) + return null; + + return UrlUtils.openConnection(texture.getUrl(), proxy); + } + + return null; + } + + private static URLConnection getSkinConnection(String owner, Proxy proxy) throws IOException { + Texture texture = MojangApi.getTexture(MojangApi.getUuid(owner), "SKIN"); + if (texture == null) + return null; + + URLConnection connection = UrlUtils.openConnection(texture.getUrl(), proxy); + try (InputStream in = connection.getInputStream()) { + // thank you craftycodie! + // this is heavily based on + // https://github.com/craftycodie/MineOnline/blob/4f4f86f9d051e0a6fd7ff0b95b2a05f7437683d7/src/main/java/gg/codie/mineonline/gui/textures/TextureHelper.java#L17 + BufferedImage image = ImageIO.read(in); + Graphics2D graphics = image.createGraphics(); + graphics.setComposite(AlphaComposite.getInstance(AlphaComposite.SRC_OVER)); + + BufferedImage subimage; + + if (image.getHeight() > 32) { + // flatten second layers + subimage = image.getSubimage(0, 32, 56, 16); + graphics.drawImage(subimage, 0, 16, null); + } + + if (texture.isSlim()) { + // convert slim to classic + subimage = image.getSubimage(45, 16, 9, 16); + graphics.drawImage(subimage, 46, 16, null); + + subimage = image.getSubimage(49, 16, 2, 4); + graphics.drawImage(subimage, 50, 16, null); + + subimage = image.getSubimage(53, 20, 2, 12); + graphics.drawImage(subimage, 54, 20, null); + } + + graphics.dispose(); + + // crop the image - old versions disregard all secondary layers besides the hat + ByteArrayOutputStream out = new ByteArrayOutputStream(); + image = image.getSubimage(0, 0, 64, 32); + ImageIO.write(image, "png", out); + + return new CustomUrlConnection(out.toByteArray()); + } + } + + private static String findSkinOwner(URL address) { + switch (address.getHost()) { + case "www.minecraft.net": + return stripIfPrefixed(address.getPath(), "/skin/"); + + case "s3.amazonaws.com": + case "skins.minecraft.net": + return stripIfPrefixed(address.getPath(), "/MinecraftSkins/"); + } + + return null; + } + + private static String findCapeOwner(URL address) { + switch (address.getHost()) { + case "www.minecraft.net": + if (!address.getPath().equals("/cloak/get.jsp")) + return null; + + return stripIfPrefixed(address.getQuery(), "user="); + + case "s3.amazonaws.com": + case "skins.minecraft.net": + return stripIfPrefixed(address.getPath(), "/MinecraftCloaks/"); + } + + return null; + } + + private static String stripIfPrefixed(String string, String prefix) { + if (string != null && string.startsWith(prefix)) { + string = string.substring(prefix.length()); + + if (string.endsWith(".png")) + string = string.substring(0, string.lastIndexOf('.')); + + return string; + } + + return null; + } +} diff --git a/libraries/launcher/legacy/org/prismlauncher/legacy/utils/Base64.java b/libraries/launcher/legacy/org/prismlauncher/legacy/utils/Base64.java new file mode 100644 index 000000000..a7076f21f --- /dev/null +++ b/libraries/launcher/legacy/org/prismlauncher/legacy/utils/Base64.java @@ -0,0 +1,92 @@ +// SPDX-License-Identifier: GPL-3.0-only +/* + * Prism Launcher - Minecraft Launcher + * Copyright (C) 2022 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. + * + * Linking this library statically or dynamically with other modules is + * making a combined work based on this library. Thus, the terms and + * conditions of the GNU General Public License cover the whole + * combination. + * + * As a special exception, the copyright holders of this library give + * you permission to link this library with independent modules to + * produce an executable, regardless of the license terms of these + * independent modules, and to copy and distribute the resulting + * executable under terms of your choice, provided that you also meet, + * for each linked independent module, the terms and conditions of the + * license of that module. An independent module is a module which is + * not derived from or based on this library. If you modify this + * library, you may extend this exception to your version of the + * library, but you are not obliged to do so. If you do not wish to do + * so, delete this exception statement from your version. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +package org.prismlauncher.legacy.utils; + +import org.prismlauncher.utils.logging.Log; + +import java.lang.invoke.MethodHandle; +import java.lang.invoke.MethodHandles; +import java.lang.invoke.MethodType; +import java.nio.charset.StandardCharsets; + +/** + * Uses Base64 with Java 8 or later, otherwise DatatypeConverter. In the latter + * case, reflection is used to allow using newer compilers. + */ +public final class Base64 { + private static boolean supported = true; + private static MethodHandle legacy; + + static { + try { + Class.forName("java.util.Base64"); + } catch (ClassNotFoundException e) { + try { + Class datatypeConverter = Class.forName("javax.xml.bind.DatatypeConverter"); + legacy = MethodHandles.lookup().findStatic( + datatypeConverter, "parseBase64Binary", MethodType.methodType(byte[].class, String.class)); + } catch (ClassNotFoundException | NoSuchMethodException | IllegalAccessException e1) { + Log.error("Base64 not supported", e1); + supported = false; + } + } + } + + /** + * Determines whether base64 is supported. + * + * @return true if base64 can be parsed + */ + public static boolean isSupported() { + return supported; + } + + public static byte[] decode(String input) { + if (!isSupported()) + throw new UnsupportedOperationException(); + + if (legacy == null) + return java.util.Base64.getDecoder().decode(input.getBytes(StandardCharsets.UTF_8)); + + try { + return (byte[]) legacy.invokeExact(input); + } catch (Error | RuntimeException e) { + throw e; + } catch (Throwable e) { + throw new Error(e); + } + } +} diff --git a/libraries/launcher/legacy/org/prismlauncher/legacy/utils/api/MojangApi.java b/libraries/launcher/legacy/org/prismlauncher/legacy/utils/api/MojangApi.java new file mode 100644 index 000000000..41f7f9114 --- /dev/null +++ b/libraries/launcher/legacy/org/prismlauncher/legacy/utils/api/MojangApi.java @@ -0,0 +1,98 @@ +// SPDX-License-Identifier: GPL-3.0-only +/* + * Prism Launcher - Minecraft Launcher + * Copyright (C) 2022 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. + * + * Linking this library statically or dynamically with other modules is + * making a combined work based on this library. Thus, the terms and + * conditions of the GNU General Public License cover the whole + * combination. + * + * As a special exception, the copyright holders of this library give + * you permission to link this library with independent modules to + * produce an executable, regardless of the license terms of these + * independent modules, and to copy and distribute the resulting + * executable under terms of your choice, provided that you also meet, + * for each linked independent module, the terms and conditions of the + * license of that module. An independent module is a module which is + * not derived from or based on this library. If you modify this + * library, you may extend this exception to your version of the + * library, but you are not obliged to do so. If you do not wish to do + * so, delete this exception statement from your version. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +package org.prismlauncher.legacy.utils.api; + +import org.prismlauncher.legacy.utils.Base64; +import org.prismlauncher.legacy.utils.json.JsonParser; + +import java.io.IOException; +import java.io.InputStream; +import java.net.URL; +import java.util.Map; + +/** + * Basic wrapper for Mojang's Minecraft API. + */ +@SuppressWarnings("unchecked") +public final class MojangApi { + public static String getUuid(String username) throws IOException { + try (InputStream in = new URL("https://api.mojang.com/users/profiles/minecraft/" + username).openStream()) { + Map map = (Map) JsonParser.parse(in); + return (String) map.get("id"); + } + } + + public static Texture getTexture(String player, String id) throws IOException { + Map map = getTextures(player); + + if (map != null) { + map = (Map) map.get(id); + if (map == null) + return null; + + URL url = new URL((String) map.get("url")); + boolean slim = false; + + if (id.equals("SKIN")) { + map = (Map) map.get("metadata"); + if (map != null && "slim".equals(map.get("model"))) + slim = true; + } + + return new Texture(url, slim); + } + + return null; + } + + public static Map getTextures(String player) throws IOException { + try (InputStream profileIn = new URL("https://sessionserver.mojang.com/session/minecraft/profile/" + player).openStream()) { + Map profile = (Map) JsonParser.parse(profileIn); + + for (Map property : (Iterable>) profile.get("properties")) { + if (property.get("name").equals("textures")) { + Map result = + (Map) JsonParser.parse(new String(Base64.decode((String) property.get("value")))); + result = (Map) result.get("textures"); + + return result; + } + } + + return null; + } + } +} diff --git a/libraries/launcher/legacy/org/prismlauncher/legacy/utils/api/Texture.java b/libraries/launcher/legacy/org/prismlauncher/legacy/utils/api/Texture.java new file mode 100644 index 000000000..094b08b98 --- /dev/null +++ b/libraries/launcher/legacy/org/prismlauncher/legacy/utils/api/Texture.java @@ -0,0 +1,59 @@ +// SPDX-License-Identifier: GPL-3.0-only +/* + * Prism Launcher - Minecraft Launcher + * Copyright (C) 2022 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. + * + * Linking this library statically or dynamically with other modules is + * making a combined work based on this library. Thus, the terms and + * conditions of the GNU General Public License cover the whole + * combination. + * + * As a special exception, the copyright holders of this library give + * you permission to link this library with independent modules to + * produce an executable, regardless of the license terms of these + * independent modules, and to copy and distribute the resulting + * executable under terms of your choice, provided that you also meet, + * for each linked independent module, the terms and conditions of the + * license of that module. An independent module is a module which is + * not derived from or based on this library. If you modify this + * library, you may extend this exception to your version of the + * library, but you are not obliged to do so. If you do not wish to do + * so, delete this exception statement from your version. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +package org.prismlauncher.legacy.utils.api; + +import java.net.URL; + +/** + * Represents a texture from the Mojang API. + */ +public final class Texture { + private final URL url; + private final boolean slim; + + public Texture(URL url, boolean slim) { + this.url = url; + this.slim = slim; + } + + public URL getUrl() { + return url; + } + + public boolean isSlim() { + return slim; + } +} diff --git a/libraries/launcher/legacy/org/prismlauncher/legacy/utils/json/JsonParseException.java b/libraries/launcher/legacy/org/prismlauncher/legacy/utils/json/JsonParseException.java new file mode 100644 index 000000000..a43876c55 --- /dev/null +++ b/libraries/launcher/legacy/org/prismlauncher/legacy/utils/json/JsonParseException.java @@ -0,0 +1,46 @@ +// SPDX-License-Identifier: GPL-3.0-only +/* + * Prism Launcher - Minecraft Launcher + * Copyright (C) 2022 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. + * + * Linking this library statically or dynamically with other modules is + * making a combined work based on this library. Thus, the terms and + * conditions of the GNU General Public License cover the whole + * combination. + * + * As a special exception, the copyright holders of this library give + * you permission to link this library with independent modules to + * produce an executable, regardless of the license terms of these + * independent modules, and to copy and distribute the resulting + * executable under terms of your choice, provided that you also meet, + * for each linked independent module, the terms and conditions of the + * license of that module. An independent module is a module which is + * not derived from or based on this library. If you modify this + * library, you may extend this exception to your version of the + * library, but you are not obliged to do so. If you do not wish to do + * so, delete this exception statement from your version. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +package org.prismlauncher.legacy.utils.json; + +import java.io.IOException; + +public final class JsonParseException extends IOException { + private static final long serialVersionUID = 1L; + + public JsonParseException(String message) { + super(message); + } +} diff --git a/libraries/launcher/legacy/org/prismlauncher/legacy/utils/json/JsonParser.java b/libraries/launcher/legacy/org/prismlauncher/legacy/utils/json/JsonParser.java new file mode 100644 index 000000000..9ce24de13 --- /dev/null +++ b/libraries/launcher/legacy/org/prismlauncher/legacy/utils/json/JsonParser.java @@ -0,0 +1,408 @@ +// SPDX-License-Identifier: GPL-3.0-only +/* + * Prism Launcher - Minecraft Launcher + * Copyright (C) 2022 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. + * + * Linking this library statically or dynamically with other modules is + * making a combined work based on this library. Thus, the terms and + * conditions of the GNU General Public License cover the whole + * combination. + * + * As a special exception, the copyright holders of this library give + * you permission to link this library with independent modules to + * produce an executable, regardless of the license terms of these + * independent modules, and to copy and distribute the resulting + * executable under terms of your choice, provided that you also meet, + * for each linked independent module, the terms and conditions of the + * license of that module. An independent module is a module which is + * not derived from or based on this library. If you modify this + * library, you may extend this exception to your version of the + * library, but you are not obliged to do so. If you do not wish to do + * so, delete this exception statement from your version. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +package org.prismlauncher.legacy.utils.json; + +import java.io.IOException; +import java.io.InputStream; +import java.io.InputStreamReader; +import java.io.Reader; +import java.io.StringReader; +import java.nio.charset.StandardCharsets; +import java.util.ArrayList; +import java.util.HashMap; +import java.util.List; +import java.util.Map; + +/** + * A lightweight portable JSON parser used instead of GSON since it is not + * available in a lot of versions. + */ +public final class JsonParser { + private final Reader in; + private char[] buffer; + private int pos, length; + + public static Object parse(String in) throws IOException { + return parse(new StringReader(in)); + } + + public static Object parse(InputStream in) throws IOException { + return parse(new InputStreamReader(in, StandardCharsets.UTF_8)); + } + + public static Object parse(Reader in) throws IOException { + return new JsonParser(in).readSingleValue(); + } + + private JsonParser(Reader in) throws IOException { + this.in = in; + pos = length = 0; + read(); + } + + private int character() { + if (length == -1) + return -1; + + return buffer[pos]; + } + + private int read() throws IOException { + if (length == -1) + return -1; + + if (buffer == null || pos++ == length - 1) { + pos = 0; + buffer = new char[8192]; + length = in.read(buffer); + } + + return character(); + } + + private void assertCharacter(char character) throws JsonParseException { + if (character() != character) + throw new JsonParseException( + "Expected '" + character + "' but got " + (character() != -1 ? ("'" + (char) character() + "'") : "EOF")); + } + + private void assertNoEOF(String expected) throws JsonParseException { + if (character() == -1) + throw new JsonParseException("Expected " + expected + " but got EOF"); + } + + private void skipWhitespace() throws IOException { + while (isWhitespace()) read(); + } + + private boolean isWhitespace() { + return character() == ' ' || character() == '\n' || character() == '\r' || character() == '\t'; + } + + private Object readSingleValue() throws IOException { + skipWhitespace(); + Object result = readValue(); + + if (!(result instanceof Double)) + read(); + + skipWhitespace(); + + if (character() != -1) + throw new JsonParseException("Found trailing non-whitespace characters"); + + return result; + } + + private Object readValue() throws IOException { + assertNoEOF("a value"); + + int character = character(); + + switch (character) { + case '{': + return readObject(); + + case '[': + return readArray(); + + case '"': + return readString(); + + case 't': + case 'f': + // probably boolean + Boolean bool = readBoolean(); + if (bool != null) + return bool; + + break; + + case 'n': + // probably null + if (readNull()) + return null; + + break; + } + + if (character == '-' || isDigit()) + // probably a number + return readNumber(); + + throw new JsonParseException("Expected a JSON value but got '" + (char) character + "'"); + } + + private Map readObject() throws IOException { + assertCharacter('{'); + Map obj = new HashMap<>(); + boolean comma = false; + + read(); + skipWhitespace(); + + while (character() != '}') { + if (comma) { + assertCharacter(','); + read(); + skipWhitespace(); + } + + String key = readString(); + read(); + skipWhitespace(); + assertCharacter(':'); + read(); + skipWhitespace(); + + Object value = readValue(); + obj.put(key, value); + + if (!(value instanceof Double)) + read(); + + skipWhitespace(); + comma = true; + } + + return obj; + } + + private List readArray() throws IOException { + assertCharacter('['); + List array = new ArrayList<>(); + boolean comma = false; + + read(); + skipWhitespace(); + + while (character() != ']') { + if (comma) { + assertCharacter(','); + read(); + skipWhitespace(); + } + + Object value = readValue(); + array.add(value); + + if (!(value instanceof Double)) + read(); + + skipWhitespace(); + comma = true; + } + + return array; + } + + private String readString() throws IOException { + assertCharacter('"'); + + StringBuilder result = new StringBuilder(); + + while (read() != '"') { + int character = character(); + + if (character >= '\u0000' && character <= '\u001F') + throw new JsonParseException("Found unescaped control character within string"); + + switch (character) { + case -1: + throw new JsonParseException("Expected '\"' but got EOF"); + + case 0x7F: + if (read() == '"') { + return result.toString(); + } + continue; + + case '\\': + int seq = read(); + + switch (seq) { + case -1: + throw new JsonParseException("Expected an escape sequence but got EOF"); + + case '\\': + break; + + case '/': + case '\"': + character = seq; + break; + + case 'b': + character = '\b'; + break; + + case 'f': + character = '\f'; + break; + + case 'n': + character = '\n'; + break; + + case 'r': + character = '\r'; + break; + + case 't': + character = '\t'; + break; + + case 'u': + // char array to allow allocation in advance. + char[] digits = new char[4]; + + for (int index = 0; index < digits.length; index++) { + character = read(); + if (index == 0 && character() == '-') { + throw new JsonParseException("Hex sequence may not be negative"); + } else if (character() == -1) { + throw new JsonParseException("Expected a hex sequence but got EOF"); + } + digits[index] = (char) character; + } + + String digitsString = new String(digits); + + try { + character = Integer.parseInt(digitsString, 16); + } catch (NumberFormatException e) { + throw new JsonParseException("Could not parse hex sequence \"" + digitsString + "\""); + } + + break; + default: + throw new JsonParseException("Invalid escape sequence: \\" + (char) seq); + } + break; + } + + result.append((char) character); + } + + return result.toString(); + } + + private boolean isDigit() { + return character() >= '0' && character() <= '9'; + } + + private Double readNumber() throws IOException { + StringBuilder result = new StringBuilder(); + + if (character() == '-') { + result.append((char) character()); + read(); + } + + if (character() == '0') { + result.append((char) character()); + read(); + + if (isDigit()) + throw new JsonParseException("Found superfluous leading zero"); + } else if (!isDigit()) + throw new JsonParseException("Expected digits"); + + while (character() != -1 && isDigit()) { + result.append((char) character()); + read(); + } + + if (character() == '.') { + result.append('.'); + + read(); + assertNoEOF("digits"); + + if (!isDigit()) + throw new JsonParseException("Expected digits after decimal point"); + + while (character() != -1 && isDigit()) { + result.append((char) character()); + read(); + } + } + + if (character() == 'e' || character() == 'E') { + result.append('E'); + + read(); + assertNoEOF("digits"); + + if (character() == '+' || character() == '-') { + result.append((char) character()); + read(); + } + + if (!(character() == '+' || character() == '-' || isDigit())) + throw new JsonParseException("Expected exponent digits"); + + while (character() != -1 && isDigit()) { + result.append((char) character()); + read(); + } + } + + String resultStr = result.toString(); + + try { + return Double.parseDouble(resultStr); + } catch (NumberFormatException e) { + throw new JsonParseException("Failed to parse number '" + resultStr + "'"); + } + } + + private Boolean readBoolean() throws IOException { + if (character() == 't') { + if (read() == 'r' && read() == 'u' && read() == 'e') { + return true; + } + } else if (character() == 'f' && read() == 'a' && read() == 'l' && read() == 's' && read() == 'e') { + return false; + } + + return null; + } + + private boolean readNull() throws IOException { + return character() == 'n' && read() == 'u' && read() == 'l' && read() == 'l'; + } +} diff --git a/libraries/launcher/legacy/org/prismlauncher/legacy/utils/url/CustomUrlConnection.java b/libraries/launcher/legacy/org/prismlauncher/legacy/utils/url/CustomUrlConnection.java new file mode 100644 index 000000000..71b0e68f2 --- /dev/null +++ b/libraries/launcher/legacy/org/prismlauncher/legacy/utils/url/CustomUrlConnection.java @@ -0,0 +1,77 @@ +// SPDX-License-Identifier: GPL-3.0-only +/* + * Prism Launcher - Minecraft Launcher + * Copyright (C) 2022 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. + * + * Linking this library statically or dynamically with other modules is + * making a combined work based on this library. Thus, the terms and + * conditions of the GNU General Public License cover the whole + * combination. + * + * As a special exception, the copyright holders of this library give + * you permission to link this library with independent modules to + * produce an executable, regardless of the license terms of these + * independent modules, and to copy and distribute the resulting + * executable under terms of your choice, provided that you also meet, + * for each linked independent module, the terms and conditions of the + * license of that module. An independent module is a module which is + * not derived from or based on this library. If you modify this + * library, you may extend this exception to your version of the + * library, but you are not obliged to do so. If you do not wish to do + * so, delete this exception statement from your version. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +package org.prismlauncher.legacy.utils.url; + +import java.io.ByteArrayInputStream; +import java.io.IOException; +import java.io.InputStream; +import java.net.HttpURLConnection; + +public final class CustomUrlConnection extends HttpURLConnection { + private final InputStream in; + + public CustomUrlConnection(byte[] data) { + this(new ByteArrayInputStream(data)); + } + + public CustomUrlConnection(InputStream in) { + super(null); + this.in = in; + } + + @Override + public void connect() throws IOException { + responseCode = 200; + } + + @Override + public void disconnect() { + try { + in.close(); + } catch (IOException e) { + } + } + + @Override + public InputStream getInputStream() throws IOException { + return in; + } + + @Override + public boolean usingProxy() { + return false; + } +} diff --git a/libraries/launcher/legacy/org/prismlauncher/legacy/utils/url/UrlUtils.java b/libraries/launcher/legacy/org/prismlauncher/legacy/utils/url/UrlUtils.java new file mode 100644 index 000000000..b0072485e --- /dev/null +++ b/libraries/launcher/legacy/org/prismlauncher/legacy/utils/url/UrlUtils.java @@ -0,0 +1,107 @@ +// SPDX-License-Identifier: GPL-3.0-only +/* + * Prism Launcher - Minecraft Launcher + * Copyright (C) 2022 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. + * + * Linking this library statically or dynamically with other modules is + * making a combined work based on this library. Thus, the terms and + * conditions of the GNU General Public License cover the whole + * combination. + * + * As a special exception, the copyright holders of this library give + * you permission to link this library with independent modules to + * produce an executable, regardless of the license terms of these + * independent modules, and to copy and distribute the resulting + * executable under terms of your choice, provided that you also meet, + * for each linked independent module, the terms and conditions of the + * license of that module. An independent module is a module which is + * not derived from or based on this library. If you modify this + * library, you may extend this exception to your version of the + * library, but you are not obliged to do so. If you do not wish to do + * so, delete this exception statement from your version. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +package org.prismlauncher.legacy.utils.url; + +import org.prismlauncher.utils.logging.Log; + +import java.io.IOException; +import java.lang.invoke.MethodHandle; +import java.lang.invoke.MethodHandles; +import java.lang.reflect.Method; +import java.net.Proxy; +import java.net.URL; +import java.net.URLConnection; +import java.net.URLStreamHandler; + +/** + * A utility class for URLs which uses reflection to access constructors for + * internal classes. + */ +public final class UrlUtils { + private static URLStreamHandler http; + private static MethodHandle openConnection; + + static { + try { + // we first obtain the stock URLStreamHandler for http as we overwrite it later + Method getURLStreamHandler = URL.class.getDeclaredMethod("getURLStreamHandler", String.class); + getURLStreamHandler.setAccessible(true); + http = (URLStreamHandler) getURLStreamHandler.invoke(null, "http"); + + // we next find the openConnection method + Method openConnectionReflect = URLStreamHandler.class.getDeclaredMethod("openConnection", URL.class, Proxy.class); + openConnectionReflect.setAccessible(true); + openConnection = MethodHandles.lookup().unreflect(openConnectionReflect); + } catch (Throwable e) { + Log.error("URL reflection failed - some features may not work", e); + } + } + + /** + * Determines whether all the features of this class are available. + * + * @return true if all features can be used + */ + public static boolean isSupported() { + return http != null && openConnection != null; + } + + public static URLConnection openConnection(URL url, Proxy proxy) throws IOException { + if (http == null) + throw new UnsupportedOperationException(); + + if (url.getProtocol().equals("http")) + return openConnection(http, url, proxy); + + // fall back to Java's default method + // at this point, this should not cause a StackOverflowError unless we've missed + // a protocol out from the if statements + return url.openConnection(); + } + + public static URLConnection openConnection(URLStreamHandler handler, URL url, Proxy proxy) throws IOException { + if (openConnection == null) + throw new UnsupportedOperationException(); + + try { + return (URLConnection) openConnection.invokeExact(handler, url, proxy); + } catch (IOException | Error | RuntimeException e) { + throw e; // rethrow if possible + } catch (Throwable e) { + throw new AssertionError(e); // oh dear! this isn't meant to happen + } + } +} diff --git a/libraries/launcher/org/prismlauncher/EntryPoint.java b/libraries/launcher/org/prismlauncher/EntryPoint.java index 4b59c1da6..3ef161b40 100644 --- a/libraries/launcher/org/prismlauncher/EntryPoint.java +++ b/libraries/launcher/org/prismlauncher/EntryPoint.java @@ -57,7 +57,7 @@ package org.prismlauncher; import org.prismlauncher.exception.ParseException; import org.prismlauncher.launcher.Launcher; import org.prismlauncher.launcher.impl.StandardLauncher; -import org.prismlauncher.launcher.impl.legacy.LegacyLauncher; +import org.prismlauncher.legacy.LegacyProxy; import org.prismlauncher.utils.Parameters; import org.prismlauncher.utils.logging.Log; @@ -106,6 +106,8 @@ public final class EntryPoint { } try { + LegacyProxy.applyOnlineFixes(params); + Launcher launcher; String type = params.getString("launcher"); @@ -115,7 +117,7 @@ public final class EntryPoint { break; case "legacy": - launcher = new LegacyLauncher(params); + launcher = LegacyProxy.createLauncher(params); break; default: diff --git a/libraries/launcher/org/prismlauncher/legacy/LegacyProxy.java b/libraries/launcher/org/prismlauncher/legacy/LegacyProxy.java new file mode 100644 index 000000000..133558c9a --- /dev/null +++ b/libraries/launcher/org/prismlauncher/legacy/LegacyProxy.java @@ -0,0 +1,66 @@ +// SPDX-License-Identifier: GPL-3.0-only +/* + * Prism Launcher - Minecraft Launcher + * Copyright (C) 2022 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. + * + * Linking this library statically or dynamically with other modules is + * making a combined work based on this library. Thus, the terms and + * conditions of the GNU General Public License cover the whole + * combination. + * + * As a special exception, the copyright holders of this library give + * you permission to link this library with independent modules to + * produce an executable, regardless of the license terms of these + * independent modules, and to copy and distribute the resulting + * executable under terms of your choice, provided that you also meet, + * for each linked independent module, the terms and conditions of the + * license of that module. An independent module is a module which is + * not derived from or based on this library. If you modify this + * library, you may extend this exception to your version of the + * library, but you are not obliged to do so. If you do not wish to do + * so, delete this exception statement from your version. + * + * 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. + */ + +package org.prismlauncher.legacy; + +import org.prismlauncher.launcher.Launcher; +import org.prismlauncher.utils.Parameters; + +// used as a fallback if NewLaunchLegacy is not on the classpath +// if it is, this class will be replaced +public final class LegacyProxy { + public static Launcher createLauncher(Parameters params) { + throw new AssertionError("NewLaunchLegacy is not loaded"); + } + + public static void applyOnlineFixes(Parameters params) {} +} diff --git a/libraries/launcher/org/prismlauncher/utils/ReflectionUtils.java b/libraries/launcher/org/prismlauncher/utils/ReflectionUtils.java index ad222bd2a..3b2bfd9bb 100644 --- a/libraries/launcher/org/prismlauncher/utils/ReflectionUtils.java +++ b/libraries/launcher/org/prismlauncher/utils/ReflectionUtils.java @@ -68,61 +68,6 @@ public final class ReflectionUtils { private static final MethodHandles.Lookup LOOKUP = MethodHandles.lookup(); private static final ClassLoader LOADER = ClassLoader.getSystemClassLoader(); - /** - * Construct a Java applet by its class name. - * - * @param clazz The class name - * @return The applet instance - * @throws Throwable - */ - public static Applet createAppletClass(String clazz) throws Throwable { - Class appletClass = LOADER.loadClass(clazz); - - MethodHandle appletConstructor = LOOKUP.findConstructor(appletClass, MethodType.methodType(void.class)); - return (Applet) appletConstructor.invoke(); - } - - /** - * Best guess of the game directory field within net.minecraft.client.Minecraft. - * Designed for legacy versions - newer versions do not use a static field. - * - * @param clazz The class - * @return The first field matching criteria - */ - public static Field findMinecraftGameDirField(Class clazz) { - Log.debug("Resolving minecraft game directory field"); - - // search for private static File - for (Field field : clazz.getDeclaredFields()) { - if (field.getType() != File.class) { - continue; - } - - int fieldModifiers = field.getModifiers(); - - if (!Modifier.isStatic(fieldModifiers)) { - Log.debug("Rejecting field " + field.getName() + " because it is not static"); - continue; - } - - if (!Modifier.isPrivate(fieldModifiers)) { - Log.debug("Rejecting field " + field.getName() + " because it is not private"); - continue; - } - - if (Modifier.isFinal(fieldModifiers)) { - Log.debug("Rejecting field " + field.getName() + " because it is final"); - continue; - } - - Log.debug("Identified field " + field.getName() + " to match conditions for game directory field"); - - return field; - } - - return null; - } - /** * Gets the main method within a class. * diff --git a/nix/pkg/wrapper.nix b/nix/pkg/wrapper.nix index 8bc255e71..cd356c8d7 100644 --- a/nix/pkg/wrapper.nix +++ b/nix/pkg/wrapper.nix @@ -4,6 +4,7 @@ symlinkJoin, prismlauncher-unwrapped, wrapQtAppsHook, + addOpenGLRunpath, qtbase, # needed for wrapQtAppsHook qtsvg, qtwayland, @@ -85,7 +86,7 @@ in 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}" + "--set LD_LIBRARY_PATH ${addOpenGLRunpath.driverLink}/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}" ]; diff --git a/program_info/win_install.nsi.in b/program_info/win_install.nsi.in index cb7b0935b..eda85821b 100644 --- a/program_info/win_install.nsi.in +++ b/program_info/win_install.nsi.in @@ -350,6 +350,7 @@ Section "@Launcher_DisplayName@" File "@Launcher_APP_BINARY_NAME@.exe" File "@Launcher_APP_BINARY_NAME@_filelink.exe" + File "@Launcher_APP_BINARY_NAME@_updater.exe" File "qt.conf" File "qtlogging.ini" File *.dll @@ -435,6 +436,7 @@ Section "Uninstall" Delete $INSTDIR\@Launcher_APP_BINARY_NAME@.exe Delete $INSTDIR\@Launcher_APP_BINARY_NAME@_filelink.exe + Delete $INSTDIR\@Launcher_APP_BINARY_NAME@_updater.exe Delete $INSTDIR\qt.conf Delete $INSTDIR\*.dll @@ -472,7 +474,6 @@ Function .onInit ${GetParameters} $R0 ${GetOptions} $R0 "/NoShortcuts" $R1 ${IfNot} ${Errors} -${OrIf} ${FileExists} "$InstDir\@Launcher_APP_BINARY_NAME@.exe" !insertmacro UnselectSection ${SM_SHORTCUTS} !insertmacro UnselectSection ${DESKTOP_SHORTCUTS} ${EndIf} diff --git a/tests/Packwiz_test.cpp b/tests/Packwiz_test.cpp index d1b274d12..e4abda9f9 100644 --- a/tests/Packwiz_test.cpp +++ b/tests/Packwiz_test.cpp @@ -42,7 +42,7 @@ class PackwizTest : public QObject { QCOMPARE(metadata.name, "Borderless Mining"); QCOMPARE(metadata.filename, "borderless-mining-1.1.1+1.18.jar"); - QCOMPARE(metadata.side, "client"); + QCOMPARE(metadata.side, Packwiz::V1::Side::ClientSide); QCOMPARE(metadata.url, QUrl("https://cdn.modrinth.com/data/kYq5qkSL/versions/1.1.1+1.18/borderless-mining-1.1.1+1.18.jar")); QCOMPARE(metadata.hash_format, "sha512"); @@ -72,7 +72,7 @@ class PackwizTest : public QObject { QCOMPARE(metadata.name, "Screenshot to Clipboard (Fabric)"); QCOMPARE(metadata.filename, "screenshot-to-clipboard-1.0.7-fabric.jar"); - QCOMPARE(metadata.side, "both"); + QCOMPARE(metadata.side, Packwiz::V1::Side::UniversalSide); QCOMPARE(metadata.url, QUrl("https://edge.forgecdn.net/files/3509/43/screenshot-to-clipboard-1.0.7-fabric.jar")); QCOMPARE(metadata.hash_format, "murmur2"); diff --git a/tests/Version_test.cpp b/tests/Version_test.cpp index d25bf7bb5..4c67cc544 100644 --- a/tests/Version_test.cpp +++ b/tests/Version_test.cpp @@ -55,6 +55,8 @@ class VersionTest : public QObject { << "2.2.0" << true << false; QTest::newRow("lessThan, two-digit") << "1.41" << "1.42" << true << false; + QTest::newRow("lessThan, snapshot") << "1.20.0-rc2" + << "1.20.1" << true << false; QTest::newRow("greaterThan, explicit 1") << "1.2.1" << "1.2.0" << false << false; @@ -72,6 +74,8 @@ class VersionTest : public QObject { << "1.2" << false << false; QTest::newRow("greaterThan, two-digit") << "1.42" << "1.41" << false << false; + QTest::newRow("greaterThan, snapshot") << "1.20.2-rc2" + << "1.20.1" << false << false; } private slots: