diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index cda301043..45ccd9a6a 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -7,10 +7,17 @@ on: description: Type of build (Debug, Release, RelWithDebInfo, MinSizeRel) type: string default: Debug + is_qt_cached: + description: Enable Qt caching or not + type: string + default: true secrets: SPARKLE_ED25519_KEY: description: Private key for signing Sparkle updates required: false + CACHIX_AUTH_TOKEN: + description: Private token for authenticating against Cachix cache + required: false jobs: build: @@ -31,17 +38,11 @@ jobs: qt_tools: '' - os: windows-2022 - name: "Windows-Legacy" - msystem: clang32 - qt_ver: 5 - - - os: windows-2022 - name: "Windows" + name: "Windows-MinGW-w64" msystem: clang64 - qt_ver: 6 - os: windows-2022 - name: "Windows-Legacy-MSVC" + name: "Windows-MSVC-Legacy" msystem: '' architecture: 'win32' vcvars_arch: 'amd64_x86' @@ -64,6 +65,18 @@ jobs: qt_modules: 'qt5compat qtimageformats' qt_tools: '' + - os: windows-2022 + name: "Windows-MSVC-arm64" + msystem: '' + architecture: 'arm64' + vcvars_arch: 'amd64_arm64' + qt_ver: 6 + qt_host: windows + qt_arch: 'win64_msvc2019_arm64' + qt_version: '6.4.0' + qt_modules: 'qt5compat qtimageformats' + qt_tools: '' + - os: macos-12 name: macOS macosx_deployment_target: 10.15 @@ -102,14 +115,6 @@ jobs: with: submodules: 'true' - - name: Initialize CodeQL - if: runner.os == 'Linux' && matrix.qt_ver == 6 - uses: github/codeql-action/init@v2 - with: - config-file: ./.github/codeql/codeql-config.yml - queries: security-and-quality - languages: cpp, java - - name: 'Setup MSYS2' if: runner.os == 'Windows' && matrix.msystem != '' uses: msys2/setup-msys2@v2 @@ -124,12 +129,12 @@ jobs: cmake:p extra-cmake-modules:p ninja:p - qt${{ matrix.qt_ver }}-base:p - qt${{ matrix.qt_ver }}-svg:p - qt${{ matrix.qt_ver }}-imageformats:p - quazip-qt${{ matrix.qt_ver }}:p + qt6-base:p + qt6-svg:p + qt6-imageformats:p + quazip-qt6:p ccache:p - ${{ matrix.qt_ver == 6 && 'qt6-5compat:p' || '' }} + qt6-5compat:p - name: Force newer ccache if: runner.os == 'Windows' && matrix.msystem == '' && inputs.build_type == 'Debug' @@ -140,7 +145,7 @@ jobs: if: (runner.os != 'Windows' || matrix.msystem == '') && inputs.build_type == 'Debug' uses: hendrikmuhs/ccache-action@v1.2.5 with: - key: ${{ matrix.os }}-qt${{ matrix.qt_ver }} + key: ${{ matrix.os }}-qt${{ matrix.qt_ver }}-${{ matrix.architecture }} - name: Setup ccache (Windows MinGW-w64) if: runner.os == 'Windows' && matrix.msystem != '' && inputs.build_type == 'Debug' @@ -163,9 +168,9 @@ jobs: uses: actions/cache@v3.0.11 with: path: '${{ github.workspace }}\.ccache' - key: ${{ matrix.os }}-qt${{ matrix.qt_ver }} + key: ${{ matrix.os }}-mingw-w64 restore-keys: | - ${{ matrix.os }}-qt${{ matrix.qt_ver }} + ${{ matrix.os }}-mingw-w64 - name: Set short version shell: bash @@ -189,8 +194,23 @@ jobs: if: runner.os == 'Linux' && matrix.qt_ver != 6 run: | sudo apt-get -y install qtbase5-dev qtchooser qt5-qmake qtbase5-dev-tools libqt5core5a libqt5network5 libqt5gui5 - - - name: Install Qt (macOS, AppImage & Windows MSVC) + + - name: Install host Qt (Windows MSVC arm64) + if: runner.os == 'Windows' && matrix.architecture == 'arm64' + uses: jurplel/install-qt-action@v3 + with: + 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: @@ -200,8 +220,14 @@ jobs: arch: ${{ matrix.qt_arch }} modules: ${{ matrix.qt_modules }} tools: ${{ matrix.qt_tools }} - cache: true - cache-key-prefix: ${{ matrix.qt_host }}-${{ matrix.qt_version }}-"${{ matrix.qt_modules }}"-qt_cache + cache: ${{ inputs.is_qt_cached }} + + - name: Install MSVC (Windows MSVC) + if: runner.os == 'Windows' && matrix.msystem == '' + uses: ilammy/msvc-dev-cmd@v1 + with: + vsversion: 2022 + arch: ${{ matrix.vcvars_arch }} - name: Prepare AppImage (Linux) if: runner.os == 'Linux' && matrix.qt_ver != 5 @@ -212,6 +238,11 @@ jobs: ${{ github.workspace }}/.github/scripts/prepare_JREs.sh + - name: Add QT_HOST_PATH var (Windows MSVC arm64) + if: runner.os == 'Windows' && matrix.architecture == 'arm64' + run: | + echo "QT_HOST_PATH=${{ github.workspace }}\HostQt\Qt\${{ matrix.qt_version }}\msvc2019_64" >> $env:GITHUB_ENV + ## # CONFIGURE ## @@ -230,12 +261,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=${{ matrix.name }} -DCMAKE_C_COMPILER_LAUNCHER=${{ env.CCACHE_VAR }} -DCMAKE_CXX_COMPILER_LAUNCHER=${{ env.CCACHE_VAR }} -DLauncher_QT_VERSION_MAJOR=${{ matrix.qt_ver }} -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=${{ matrix.name }} -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 - 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=${{ matrix.name }} -DLauncher_QT_VERSION_MAJOR=${{ matrix.qt_ver }} -DCMAKE_MSVC_RUNTIME_LIBRARY="MultiThreadedDLL" -A${{ matrix.architecture}} + cmake -S . -B ${{ env.BUILD_DIR }} -DCMAKE_INSTALL_PREFIX=${{ env.INSTALL_DIR }} -DCMAKE_BUILD_TYPE=${{ inputs.build_type }} -DENABLE_LTO=ON -DLauncher_BUILD_PLATFORM=${{ matrix.name }} -DLauncher_QT_VERSION_MAJOR=${{ matrix.qt_ver }} -DCMAKE_MSVC_RUNTIME_LIBRARY="MultiThreadedDLL" -A${{ matrix.architecture}} -DLauncher_FORCE_BUNDLED_LIBS=ON # https://github.com/ccache/ccache/wiki/MS-Visual-Studio (I coudn't figure out the compiler prefix) if ("${{ env.CCACHE_VAR }}") { @@ -288,18 +319,10 @@ jobs: ctest -E "^example64|example$" --test-dir build --output-on-failure - name: Test (Windows MSVC) - if: runner.os == 'Windows' && matrix.msystem == '' + if: runner.os == 'Windows' && matrix.msystem == '' && matrix.architecture != 'arm64' run: | ctest -E "^example64|example$" --test-dir build --output-on-failure -C ${{ inputs.build_type }} - ## - # CODE SCAN - ## - - - name: Perform CodeQL Analysis - if: runner.os == 'Linux' && matrix.qt_ver == 6 - uses: github/codeql-action/analyze@v2 - ## # PACKAGE BUILDS ## @@ -333,23 +356,12 @@ jobs: EOF fi - - name: Add VC Enviroment Variables - if: runner.os == 'Windows' && matrix.msystem == '' - uses: ilammy/msvc-dev-cmd@v1 - with: - arch: ${{ matrix.vcvars_arch }} - - name: Package (Windows MinGW-w64) if: runner.os == 'Windows' && matrix.msystem != '' shell: msys2 {0} run: | cmake --install ${{ env.BUILD_DIR }} - cd ${{ env.INSTALL_DIR }} - if [ "${{ matrix.qt_ver }}" == "5" ]; then - cp /clang32/bin/libcrypto-1_1.dll /clang32/bin/libssl-1_1.dll ./ - fi - - name: Package (Windows MSVC) if: runner.os == 'Windows' && matrix.msystem == '' run: | diff --git a/.github/workflows/codeql.yml b/.github/workflows/codeql.yml new file mode 100644 index 000000000..0cd1f6e40 --- /dev/null +++ b/.github/workflows/codeql.yml @@ -0,0 +1,35 @@ +name: "CodeQL Code Scanning" + +on: [ push, pull_request, workflow_dispatch ] + +jobs: + CodeQL: + runs-on: ubuntu-latest + + steps: + - name: Checkout repository + uses: actions/checkout@v3 + with: + submodules: 'true' + + - name: Initialize CodeQL + uses: github/codeql-action/init@v2 + with: + config-file: ./.github/codeql/codeql-config.yml + queries: security-and-quality + languages: cpp, java + + - name: Install Dependencies + run: + sudo apt-get -y update + + sudo apt-get -y install ninja-build extra-cmake-modules scdoc qtbase5-dev qtchooser qt5-qmake qtbase5-dev-tools libqt5core5a libqt5network5 libqt5gui5 + + - name: Configure and Build + run: | + cmake -S . -B build -DCMAKE_INSTALL_PREFIX=/usr -DLauncher_QT_VERSION_MAJOR=5 -G Ninja + + cmake --build build + + - name: Perform CodeQL Analysis + uses: github/codeql-action/analyze@v2 diff --git a/.github/workflows/trigger_builds.yml b/.github/workflows/trigger_builds.yml index a96635ff2..d9d711845 100644 --- a/.github/workflows/trigger_builds.yml +++ b/.github/workflows/trigger_builds.yml @@ -8,7 +8,6 @@ on: - '**.md' - '**/LICENSE' - 'flake.lock' - - '**.nix' - 'packages/**' - '.github/ISSUE_TEMPLATE/**' pull_request: @@ -16,7 +15,6 @@ on: - '**.md' - '**/LICENSE' - 'flake.lock' - - '**.nix' - 'packages/**' - '.github/ISSUE_TEMPLATE/**' workflow_dispatch: @@ -28,5 +26,7 @@ jobs: uses: ./.github/workflows/build.yml with: build_type: Debug + is_qt_cached: true secrets: SPARKLE_ED25519_KEY: ${{ secrets.SPARKLE_ED25519_KEY }} + CACHIX_AUTH_TOKEN: ${{ secrets.CACHIX_AUTH_TOKEN }} diff --git a/.github/workflows/trigger_release.yml b/.github/workflows/trigger_release.yml index 90addc3e3..2410eeefe 100644 --- a/.github/workflows/trigger_release.yml +++ b/.github/workflows/trigger_release.yml @@ -12,6 +12,7 @@ jobs: uses: ./.github/workflows/build.yml with: build_type: Release + is_qt_cached: false secrets: SPARKLE_ED25519_KEY: ${{ secrets.SPARKLE_ED25519_KEY }} @@ -36,7 +37,7 @@ jobs: run: | mv ${{ github.workspace }}/PollyMC-source PollyMC-${{ env.VERSION }} mv PollyMC-Linux-Qt6-Portable*/PollyMC-portable.tar.gz PollyMC-Linux-Qt6-Portable-${{ env.VERSION }}.tar.gz - mv PollyMC-Linux-Qt6*/PollyMC.tar.gz PollyMC-Linux-Qt6-${{ env.VERSION }}.tar.gz + mv PollyMC-Linux-Qt6*/PollyMC.tar.gz PollyMC-Linux-Qt6-${{ env.VERSION }}.tar.gz mv PollyMC-Linux-Portable*/PollyMC-portable.tar.gz PollyMC-Linux-Portable-${{ env.VERSION }}.tar.gz mv PollyMC-Linux*/PollyMC.tar.gz PollyMC-Linux-${{ env.VERSION }}.tar.gz mv PollyMC-*.AppImage/PollyMC-*.AppImage PollyMC-Linux-${{ env.VERSION }}-x86_64.AppImage @@ -45,15 +46,26 @@ jobs: tar -czf PollyMC-${{ env.VERSION }}.tar.gz PollyMC-${{ env.VERSION }} - for d in PollyMC-Windows-*; do + for d in PollyMC-Windows-MSVC*; do cd "${d}" || continue - MSVC="$(echo -n ${d} | grep -o MSVC || true)" LEGACY="$(echo -n ${d} | grep -o Legacy || true)" + ARM64="$(echo -n ${d} | grep -o arm64 || true)" INST="$(echo -n ${d} | grep -o Setup || true)" PORT="$(echo -n ${d} | grep -o Portable || true)" - NAME="PollyMC-Windows" - test -z "${MSVC}" && NAME="${NAME}-MinGW" || NAME="${NAME}-MSVC" + NAME="PollyMC-Windows-MSVC" test -z "${LEGACY}" || NAME="${NAME}-Legacy" + test -z "${ARM64}" || NAME="${NAME}-arm64" + test -z "${PORT}" || NAME="${NAME}-Portable" + test -z "${INST}" || mv PollyMC-*.exe ../${NAME}-Setup-${{ env.VERSION }}.exe + test -n "${INST}" || zip -r -9 "../${NAME}-${{ env.VERSION }}.zip" * + cd .. + done + + for d in PollyMC-Windows-MinGW-w64*; do + cd "${d}" || continue + INST="$(echo -n ${d} | grep -o Setup || true)" + PORT="$(echo -n ${d} | grep -o Portable || true)" + NAME="PollyMC-Windows-MinGW-w64" test -z "${PORT}" || NAME="${NAME}-Portable" test -z "${INST}" || mv PollyMC-*.exe ../${NAME}-Setup-${{ env.VERSION }}.exe test -n "${INST}" || zip -r -9 "../${NAME}-${{ env.VERSION }}.zip" * @@ -76,18 +88,19 @@ jobs: PollyMC-Linux-${{ env.VERSION }}-x86_64.AppImage PollyMC-Linux-Qt6-${{ env.VERSION }}.tar.gz PollyMC-Linux-Qt6-Portable-${{ env.VERSION }}.tar.gz - PollyMC-Windows-MinGW-Legacy-${{ env.VERSION }}.zip - PollyMC-Windows-MinGW-Legacy-Portable-${{ env.VERSION }}.zip - PollyMC-Windows-MinGW-Legacy-Setup-${{ env.VERSION }}.exe - PollyMC-Windows-MinGW-${{ env.VERSION }}.zip - PollyMC-Windows-MinGW-Portable-${{ env.VERSION }}.zip - PollyMC-Windows-MinGW-Setup-${{ env.VERSION }}.exe + PollyMC-Windows-MinGW-w64-${{ env.VERSION }}.zip + PollyMC-Windows-MinGW-w64-Portable-${{ env.VERSION }}.zip + PollyMC-Windows-MinGW-w64-Setup-${{ env.VERSION }}.exe PollyMC-Windows-MSVC-Legacy-${{ env.VERSION }}.zip PollyMC-Windows-MSVC-Legacy-Portable-${{ env.VERSION }}.zip PollyMC-Windows-MSVC-Legacy-Setup-${{ env.VERSION }}.exe + PollyMC-Windows-MSVC-arm64-${{ env.VERSION }}.zip + PollyMC-Windows-MSVC-arm64-Portable-${{ env.VERSION }}.zip + PollyMC-Windows-MSVC-arm64-Setup-${{ env.VERSION }}.exe PollyMC-Windows-MSVC-${{ env.VERSION }}.zip PollyMC-Windows-MSVC-Portable-${{ env.VERSION }}.zip PollyMC-Windows-MSVC-Setup-${{ env.VERSION }}.exe PollyMC-macOS-${{ env.VERSION }}.tar.gz PollyMC-macOS-Legacy-${{ env.VERSION }}.tar.gz PollyMC-${{ env.VERSION }}.tar.gz + diff --git a/.github/workflows/winget.yml b/.github/workflows/winget.yml index 5c34040f7..ef9561cf1 100644 --- a/.github/workflows/winget.yml +++ b/.github/workflows/winget.yml @@ -11,5 +11,5 @@ jobs: with: identifier: PrismLauncher.PrismLauncher version: ${{ github.event.release.tag_name }} - installers-regex: 'PrismLauncher-Windows-Setup-.+\.exe$' + installers-regex: 'PrismLauncher-Windows-MSVC(:?-arm64|-Legacy)?-Setup-.+\.exe$' token: ${{ secrets.WINGET_TOKEN }} diff --git a/CMakeLists.txt b/CMakeLists.txt index 7b628c9bc..eb4fd4e14 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -28,19 +28,28 @@ set(CMAKE_CXX_STANDARD 17) set(CMAKE_C_STANDARD 11) include(GenerateExportHeader) if(MSVC) - # Use /W4 as /Wall includes unnesserey warnings such as added padding to structs - # /permissive- specify standards-conforming compiler behavior, also enabled by Qt6, default on with std:c++20 # /GS Adds buffer security checks, default on but incuded anyway to mirror gcc's fstack-protector flag - set(CMAKE_CXX_FLAGS "/W4 /permissive- /GS ${CMAKE_CXX_FLAGS}") + # /permissive- specify standards-conforming compiler behavior, also enabled by Qt6, default on with std:c++20 + # Use /W4 as /Wall includes unnesserey warnings such as added padding to structs + set(CMAKE_CXX_FLAGS "/GS /permissive- /W4 ${CMAKE_CXX_FLAGS}") # LINK accepts /SUBSYSTEM whics sets if we are a WINDOWS (gui) or a CONSOLE programs # This implicitly selects an entrypoint specific to the subsystem selected # qtmain/QtEntryPointLib provides the correct entrypoint (wWinMain) for gui programs # Additinaly LINK autodetects we use a GUI so we can omit /SUBSYSTEM # This allows tests to still use have console without using seperate linker flags + # /LTCG allows for linking wholy optimizated programs # /MANIFEST:NO disables generating a manifest file, we instead provide our own # /STACK sets the stack reserve size, ATL's pack list needs 3-4 MiB as of November 2022, provide 8 MiB - set(CMAKE_EXE_LINKER_FLAGS "/MANIFEST:NO /STACK:8388608 ${CMAKE_EXE_LINKER_FLAGS}") + set(CMAKE_EXE_LINKER_FLAGS "/LTCG /MANIFEST:NO /STACK:8388608 ${CMAKE_EXE_LINKER_FLAGS}") + + # /GL enables whole program optimizations + # /Gw helps reduce binary size + # /Gy allows the compiler to package individual functions + # /guard:cf enables control flow guard + foreach(lang C CXX) + set("CMAKE_${lang}_FLAGS_RELEASE" "/GL /Gw /Gy /guard:cf") + endforeach() # See https://github.com/ccache/ccache/issues/1040 # Note, CMake 3.25 replaces this with CMAKE_MSVC_DEBUG_INFORMATION_FORMAT @@ -70,6 +79,9 @@ set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} -DQT_NO_DEPRECATED_WARNINGS=Y") set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} -DQT_DISABLE_DEPRECATED_BEFORE=0x050C00") +# Fix aarch64 build for toml++ +set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} -DTOML_ENABLE_FLOAT16=0") + # set CXXFLAGS for build targets set(CMAKE_CXX_FLAGS_RELEASE "-O2 -D_FORTIFY_SOURCE=2 ${CMAKE_CXX_FLAGS_RELEASE}") @@ -126,7 +138,7 @@ set(Launcher_NEWS_OPEN_URL "https://prismlauncher.org/news" CACHE STRING "URL th set(Launcher_HELP_URL "https://prismlauncher.org/wiki/help-pages/%1" CACHE STRING "URL (with arg %1 to be substituted with page-id) that gets opened when the user requests help") ######## Set version numbers ######## -set(Launcher_VERSION_MAJOR 6) +set(Launcher_VERSION_MAJOR 7) set(Launcher_VERSION_MINOR 0) set(Launcher_VERSION_NAME "${Launcher_VERSION_MAJOR}.${Launcher_VERSION_MINOR}") @@ -217,7 +229,7 @@ if(Launcher_QT_VERSION_MAJOR EQUAL 5) set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} -DUNICODE -D_UNICODE") elseif(Launcher_QT_VERSION_MAJOR EQUAL 6) set(QT_VERSION_MAJOR 6) - find_package(Qt6 REQUIRED COMPONENTS Core Widgets Concurrent Network Test Xml Core5Compat) + find_package(Qt6 REQUIRED COMPONENTS Core CoreTools Widgets Concurrent Network Test Xml Core5Compat) list(APPEND Launcher_QT_LIBS Qt6::Core5Compat) if(NOT Launcher_FORCE_BUNDLED_LIBS) @@ -231,12 +243,16 @@ else() message(FATAL_ERROR "Qt version ${Launcher_QT_VERSION_MAJOR} is not supported") endif() -include(ECMQueryQt) -ecm_query_qt(QT_PLUGINS_DIR QT_INSTALL_PLUGINS) -ecm_query_qt(QT_LIBS_DIR QT_INSTALL_LIBS) -ecm_query_qt(QT_LIBEXECS_DIR QT_INSTALL_LIBEXECS) -ecm_query_qt(QT_DATA_DIR QT_HOST_DATA) -set(QT_MKSPECS_DIR ${QT_DATA_DIR}/mkspecs) +if(Launcher_QT_VERSION_MAJOR EQUAL 5) + include(ECMQueryQt) + ecm_query_qt(QT_PLUGINS_DIR QT_INSTALL_PLUGINS) + ecm_query_qt(QT_LIBS_DIR QT_INSTALL_LIBS) + ecm_query_qt(QT_LIBEXECS_DIR QT_INSTALL_LIBEXECS) +else() + set(QT_PLUGINS_DIR ${QT${QT_VERSION_MAJOR}_INSTALL_PREFIX}/${QT${QT_VERSION_MAJOR}_INSTALL_PLUGINS}) + set(QT_LIBS_DIR ${QT${QT_VERSION_MAJOR}_INSTALL_PREFIX}/${QT${QT_VERSION_MAJOR}_INSTALL_LIBS}) + set(QT_LIBEXECS_DIR ${QT${QT_VERSION_MAJOR}_INSTALL_PREFIX}/${QT${QT_VERSION_MAJOR}_INSTALL_LIBEXECS}) +endif() # NOTE: Qt 6 already sets this by default if (Qt5_POSITION_INDEPENDENT_CODE) @@ -364,11 +380,11 @@ if(NOT ZLIB_FOUND) set(SKIP_INSTALL_ALL ON) add_subdirectory(libraries/zlib EXCLUDE_FROM_ALL) - set(ZLIB_INCLUDE_DIR "${CMAKE_CURRENT_SOURCE_DIR}/libraries/zlib" "${CMAKE_CURRENT_BINARY_DIR}/libraries/zlib") + set(ZLIB_INCLUDE_DIR "${CMAKE_CURRENT_SOURCE_DIR}/libraries/zlib" "${CMAKE_CURRENT_BINARY_DIR}/libraries/zlib" CACHE STRING "") set_target_properties(zlibstatic PROPERTIES INTERFACE_INCLUDE_DIRECTORIES "${ZLIB_INCLUDE_DIR}") add_library(ZLIB::ZLIB ALIAS zlibstatic) - set(ZLIB_LIBRARY ZLIB::ZLIB) - set(ZLIB_FOUND true) + set(ZLIB_LIBRARY ZLIB::ZLIB CACHE STRING "zlib library name") + find_package(ZLIB REQUIRED) else() message(STATUS "Using system zlib") diff --git a/COPYING.md b/COPYING.md index 3e35c579e..75a5c0eb3 100644 --- a/COPYING.md +++ b/COPYING.md @@ -398,3 +398,45 @@ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + +## Breeze icons + + Copyright (C) 2014 Uri Herrera and others + + This library is free software; you can redistribute it and/or + modify it under the terms of the GNU Lesser General Public + License as published by the Free Software Foundation; either + version 3 of the License, or (at your option) any later version. + + This library 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 + Lesser General Public License for more details. + + You should have received a copy of the GNU Lesser General Public + License along with this library. If not, see . + +## Oxygen Icons + + The Oxygen Icon Theme + Copyright (C) 2007 Nuno Pinheiro + Copyright (C) 2007 David Vignoni + Copyright (C) 2007 David Miller + Copyright (C) 2007 Johann Ollivier Lapeyre + Copyright (C) 2007 Kenneth Wimer + Copyright (C) 2007 Riccardo Iaconelli + + and others + + This library is free software; you can redistribute it and/or + modify it under the terms of the GNU Lesser General Public + License as published by the Free Software Foundation; either + version 3 of the License, or (at your option) any later version. + + This library 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 + Lesser General Public License for more details. + + You should have received a copy of the GNU Lesser General Public + License along with this library. If not, see . diff --git a/README.md b/README.md index d84e1f06d..7181cce3c 100644 --- a/README.md +++ b/README.md @@ -13,3 +13,4 @@ Binaries can be found in releases. Workaround for downloading from CurseForge and FTB not working [here](https://github.com/fn2006/PollyMC/wiki/CurseForge-Workaround). To build this yourself, follow the instructions on the Prism Launcher website but clone this repo instead. + diff --git a/buildconfig/BuildConfig.h b/buildconfig/BuildConfig.h index 4a3090736..a05d7a9eb 100644 --- a/buildconfig/BuildConfig.h +++ b/buildconfig/BuildConfig.h @@ -161,6 +161,8 @@ class Config { QString MODRINTH_STAGING_URL = "https://staging-api.modrinth.com/v2"; QString MODRINTH_PROD_URL = "https://api.modrinth.com/v2"; + QString FLAME_BASE_URL = "https://api.curseforge.com/v1"; + QString versionString() const; /** * \brief Converts the Version to a string. diff --git a/flake.lock b/flake.lock index 7c0bb2f8a..051e1664c 100644 --- a/flake.lock +++ b/flake.lock @@ -3,11 +3,11 @@ "flake-compat": { "flake": false, "locked": { - "lastModified": 1650374568, - "narHash": "sha256-Z+s0J8/r907g149rllvwhb4pKi8Wam5ij0st8PwAh+E=", + "lastModified": 1668681692, + "narHash": "sha256-Ht91NGdewz8IQLtWZ9LCeNXMSXHUss+9COoqu6JLmXU=", "owner": "edolstra", "repo": "flake-compat", - "rev": "b4a34015c698c7793d592d66adbab377907a2be8", + "rev": "009399224d5e398d03b22badca40a37ac85412a1", "type": "github" }, "original": { @@ -34,11 +34,11 @@ }, "nixpkgs": { "locked": { - "lastModified": 1666057921, - "narHash": "sha256-VpQqtXdj6G7cH//SvoprjR7XT3KS7p+tCVebGK1N6tE=", + "lastModified": 1671417167, + "narHash": "sha256-JkHam6WQOwZN1t2C2sbp1TqMv3TVRjzrdoejqfefwrM=", "owner": "nixos", "repo": "nixpkgs", - "rev": "88eab1e431cabd0ed621428d8b40d425a07af39f", + "rev": "bb31220cca6d044baa6dc2715b07497a2a7c4bc7", "type": "github" }, "original": { @@ -52,24 +52,7 @@ "inputs": { "flake-compat": "flake-compat", "libnbtplusplus": "libnbtplusplus", - "nixpkgs": "nixpkgs", - "tomlplusplus": "tomlplusplus" - } - }, - "tomlplusplus": { - "flake": false, - "locked": { - "lastModified": 1666091090, - "narHash": "sha256-djpMCFPvkJcfynV8WnsYdtwLq+J7jpV1iM4C6TojiyM=", - "owner": "marzer", - "repo": "tomlplusplus", - "rev": "1e4a3833d013aee08f58c5b31c69f709afc69f73", - "type": "github" - }, - "original": { - "owner": "marzer", - "repo": "tomlplusplus", - "type": "github" + "nixpkgs": "nixpkgs" } } }, diff --git a/flake.nix b/flake.nix index d4a253388..5615a758f 100644 --- a/flake.nix +++ b/flake.nix @@ -5,10 +5,9 @@ nixpkgs.url = "github:nixos/nixpkgs/nixpkgs-unstable"; flake-compat = { url = "github:edolstra/flake-compat"; flake = false; }; libnbtplusplus = { url = "github:PrismLauncher/libnbtplusplus"; flake = false; }; - tomlplusplus = { url = "github:marzer/tomlplusplus"; flake = false; }; }; - outputs = { self, nixpkgs, libnbtplusplus, tomlplusplus, ... }: + outputs = { self, nixpkgs, libnbtplusplus, ... }: let # User-friendly version number. version = builtins.substring 0 8 self.lastModifiedDate; @@ -23,8 +22,8 @@ pkgs = forAllSystems (system: nixpkgs.legacyPackages.${system}); packagesFn = pkgs: rec { - prismlauncher = pkgs.libsForQt5.callPackage ./nix { inherit version self libnbtplusplus tomlplusplus; }; - prismlauncher-qt6 = pkgs.qt6Packages.callPackage ./nix { inherit version self libnbtplusplus tomlplusplus; }; + prismlauncher-qt5 = pkgs.libsForQt5.callPackage ./nix { inherit version self libnbtplusplus; }; + prismlauncher = pkgs.qt6Packages.callPackage ./nix { inherit version self libnbtplusplus; }; }; in { diff --git a/flatpak/org.prismlauncher.PrismLauncher.yml b/flatpak/org.prismlauncher.PrismLauncher.yml new file mode 100644 index 000000000..fca306d78 --- /dev/null +++ b/flatpak/org.prismlauncher.PrismLauncher.yml @@ -0,0 +1,83 @@ +id: org.prismlauncher.PrismLauncher +runtime: org.kde.Platform +runtime-version: "5.15-22.08" +sdk: org.kde.Sdk +sdk-extensions: + - org.freedesktop.Sdk.Extension.openjdk17 + - org.freedesktop.Sdk.Extension.openjdk8 +add-extensions: + com.valvesoftware.Steam.Utility.gamescope: + version: stable + add-ld-path: lib + no-autodownload: true + autodelete: false + directory: utils/gamescope + +command: prismlauncher +finish-args: + - --share=ipc + - --socket=x11 + - --socket=wayland + - --device=all + - --share=network + - --socket=pulseaudio + # for Discord RPC mods + - --filesystem=xdg-run/app/com.discordapp.Discord:create + # Mod drag&drop + - --filesystem=xdg-download:ro + +modules: + - name: prismlauncher + buildsystem: cmake-ninja + config-opts: + - -DLauncher_BUILD_PLATFORM=flatpak + - -DCMAKE_BUILD_TYPE=Debug + build-options: + env: + JAVA_HOME: /usr/lib/sdk/openjdk17/jvm/openjdk-17 + JAVA_COMPILER: /usr/lib/sdk/openjdk17/jvm/openjdk-17/bin/javac + sources: + - type: dir + path: ../ + - name: openjdk + buildsystem: simple + build-commands: + - mkdir -p /app/jdk/ + - /usr/lib/sdk/openjdk17/install.sh + - mv /app/jre /app/jdk/17 + - /usr/lib/sdk/openjdk8/install.sh + - mv /app/jre /app/jdk/8 + cleanup: [/jre] + - name: xrandr + buildsystem: autotools + sources: + - type: archive + url: https://xorg.freedesktop.org/archive/individual/app/xrandr-1.5.1.tar.xz + sha256: 7bc76daf9d72f8aff885efad04ce06b90488a1a169d118dea8a2b661832e8762 + cleanup: [/share/man, /bin/xkeystone] + - name: gamemode + buildsystem: meson + config-opts: + - -Dwith-sd-bus-provider=no-daemon + - -Dwith-examples=false + post-install: + # gamemoderun is installed for users who want to use wrapper commands + # post-install is running inside the build dir, we need it from the source though + - install -Dm755 ../data/gamemoderun -t /app/bin + sources: + - type: git + url: https://github.com/FeralInteractive/gamemode + tag: "1.7" + commit: 4dc99dff76218718763a6b07fc1900fa6d1dafd9 + - name: enhance + buildsystem: simple + build-commands: + - mkdir -p /app/utils/gamescope + - install -Dm755 prime-run /app/bin/prime-run + - mv /app/bin/prismlauncher /app/bin/prismrun + - install -Dm755 prismlauncher /app/bin/prismlauncher + sources: + - type: file + path: ../flatpak/prime-run + - type: file + path: ../flatpak/prismlauncher diff --git a/flatpak/prime-run b/flatpak/prime-run new file mode 100644 index 000000000..946c28dd5 --- /dev/null +++ b/flatpak/prime-run @@ -0,0 +1,4 @@ +#!/bin/sh + +export __NV_PRIME_RENDER_OFFLOAD=1 __VK_LAYER_NV_optimus=NVIDIA_only __GLX_VENDOR_LIBRARY_NAME=nvidia +exec "$@" diff --git a/flatpak/prismlauncher b/flatpak/prismlauncher new file mode 100644 index 000000000..bb8767113 --- /dev/null +++ b/flatpak/prismlauncher @@ -0,0 +1,11 @@ +#!/bin/bash + +# discord RPC +for i in {0..9}; do + test -S "$XDG_RUNTIME_DIR"/discord-ipc-"$i" || ln -sf {app/com.discordapp.Discord,"$XDG_RUNTIME_DIR"}/discord-ipc-"$i"; +done + +export PATH="${PATH}${PATH:+:}/app/utils/gamescope/bin:/usr/lib/extensions/vulkan/MangoHud/bin" +export LD_LIBRARY_PATH="${LD_LIBRARY_PATH}${LD_LIBRARY_PATH:+:}/usr/lib/extensions/vulkan/MangoHud/\$LIB/" + +exec /app/bin/prismrun "$@" diff --git a/launcher/Application.cpp b/launcher/Application.cpp index 8cd87e5f5..b5ae6c86f 100644 --- a/launcher/Application.cpp +++ b/launcher/Application.cpp @@ -1,4 +1,7 @@ -// SPDX-License-Identifier: GPL-3.0-only +// SPDX-FileCopyrightText: 2022 Sefa Eyeoglu +// +// SPDX-License-Identifier: GPL-3.0-only AND Apache-2.0 + /* * Prism Launcher - Minecraft Launcher * Copyright (C) 2022 Sefa Eyeoglu @@ -38,10 +41,14 @@ #include "Application.h" #include "BuildConfig.h" +#include "DataMigrationTask.h" #include "net/PasteUpload.h" +#include "pathmatcher/MultiMatcher.h" +#include "pathmatcher/SimplePrefixMatcher.h" #include "ui/MainWindow.h" #include "ui/InstanceWindow.h" +#include "ui/dialogs/ProgressDialog.h" #include "ui/instanceview/AccessibleInstanceView.h" #include "ui/pages/BasePageProvider.h" @@ -90,6 +97,7 @@ #include #include "InstanceList.h" +#include "MTPixmapCache.h" #include #include "icons/IconList.h" @@ -118,6 +126,7 @@ #ifdef Q_OS_LINUX #include #include "gamemode_client.h" +#include "MangoHud.h" #endif @@ -134,6 +143,8 @@ static const QLatin1String liveCheckFile("live.check"); +PixmapCache* PixmapCache::s_instance = nullptr; + namespace { void appDebugOutput(QtMsgType type, const QMessageLogContext &context, const QString &msg) { @@ -226,7 +237,7 @@ Application::Application(int &argc, char **argv) : QApplication(argc, argv) setOrganizationDomain(BuildConfig.LAUNCHER_DOMAIN); setApplicationName(BuildConfig.LAUNCHER_NAME); setApplicationDisplayName(QString("%1 %2").arg(BuildConfig.LAUNCHER_DISPLAYNAME, BuildConfig.printableVersionString())); - setApplicationVersion(BuildConfig.printableVersionString()); + setApplicationVersion(BuildConfig.printableVersionString() + "\n" + BuildConfig.GIT_COMMIT); setDesktopFileName(BuildConfig.LAUNCHER_DESKTOPFILENAME); startTime = QDateTime::currentDateTime(); @@ -301,24 +312,6 @@ Application::Application(int &argc, char **argv) : QApplication(argc, argv) dataPath = foo.absolutePath(); adjustedBy = "Persistent data path"; - /* - QDir polymcData(FS::PathCombine(QStandardPaths::writableLocation(QStandardPaths::GenericDataLocation), "PolyMC")); - if (polymcData.exists()) { - dataPath = polymcData.absolutePath(); - adjustedBy = "PolyMC data path"; - } - */ - -#ifdef Q_OS_LINUX - // TODO: this should be removed in a future version - // TODO: provide a migration path similar to macOS migration - QDir bar(FS::PathCombine(QStandardPaths::writableLocation(QStandardPaths::GenericDataLocation), "polymc")); - if (bar.exists()) { - dataPath = bar.absolutePath(); - adjustedBy = "Legacy data path"; - } -#endif - #ifndef Q_OS_MACOS if (QFile::exists(FS::PathCombine(m_rootPath, "portable.txt"))) { dataPath = m_rootPath; @@ -441,6 +434,15 @@ Application::Application(int &argc, char **argv) : QApplication(argc, argv) qDebug() << "<> Log initialized."; } + { + bool migrated = false; + + if (!migrated) + migrated = handleDataMigration(dataPath, FS::PathCombine(QStandardPaths::writableLocation(QStandardPaths::AppDataLocation), "../../PolyMC"), "PolyMC", "polymc.cfg"); + if (!migrated) + migrated = handleDataMigration(dataPath, FS::PathCombine(QStandardPaths::writableLocation(QStandardPaths::AppDataLocation), "../../multimc"), "MultiMC", "multimc.cfg"); + } + { qDebug() << BuildConfig.LAUNCHER_DISPLAYNAME << ", (c) 2013-2021 " << BuildConfig.LAUNCHER_COPYRIGHT; @@ -695,6 +697,9 @@ Application::Application(int &argc, char **argv) : QApplication(argc, argv) m_globalSettingsProvider->addPage(); m_globalSettingsProvider->addPage(); } + + PixmapCache::setInstance(new PixmapCache(this)); + qDebug() << "<> Settings loaded."; } @@ -918,13 +923,13 @@ bool Application::createSetupWizard() return false; } -bool Application::event(QEvent* event) { +bool Application::event(QEvent* event) +{ #ifdef Q_OS_MACOS if (event->type() == QEvent::ApplicationStateChange) { auto ev = static_cast(event); - if (m_prevAppState == Qt::ApplicationActive - && ev->applicationState() == Qt::ApplicationActive) { + if (m_prevAppState == Qt::ApplicationActive && ev->applicationState() == Qt::ApplicationActive) { emit clickedOnDock(); } m_prevAppState = ev->applicationState(); @@ -1522,17 +1527,8 @@ void Application::updateCapabilities() if (gamemode_query_status() >= 0) m_capabilities |= SupportsGameMode; - { - void *dummy = dlopen("libMangoHud_dlsym.so", RTLD_LAZY); - // try normal variant as well - if (dummy == NULL) - dummy = dlopen("libMangoHud.so", RTLD_LAZY); - - if (dummy != NULL) { - dlclose(dummy); - m_capabilities |= SupportsMangoHud; - } - } + if (!MangoHud::getLibraryString().isEmpty()) + m_capabilities |= SupportsMangoHud; #endif } @@ -1608,3 +1604,89 @@ int Application::suitableMaxMem() return maxMemoryAlloc; } + +bool Application::handleDataMigration(const QString& currentData, + const QString& oldData, + const QString& name, + const QString& configFile) const +{ + QString nomigratePath = FS::PathCombine(currentData, name + "_nomigrate.txt"); + QStringList configPaths = { FS::PathCombine(oldData, configFile), FS::PathCombine(oldData, BuildConfig.LAUNCHER_CONFIGFILE) }; + + QLocale locale; + + // Is there a valid config at the old location? + bool configExists = false; + for (QString configPath : configPaths) { + configExists |= QFileInfo::exists(configPath); + } + + if (!configExists || QFileInfo::exists(nomigratePath)) { + qDebug() << "<> No migration needed from" << name; + return false; + } + + QString message; + bool currentExists = QFileInfo::exists(FS::PathCombine(currentData, BuildConfig.LAUNCHER_CONFIGFILE)); + + if (currentExists) { + message = tr("Old data from %1 was found, but you already have existing data for %2. Sadly you will need to migrate yourself. Do " + "you want to be reminded of the pending data migration next time you start %2?") + .arg(name, BuildConfig.LAUNCHER_DISPLAYNAME); + } else { + message = tr("It looks like you used %1 before. Do you want to migrate your data to the new location of %2?") + .arg(name, BuildConfig.LAUNCHER_DISPLAYNAME); + + QFileInfo logInfo(FS::PathCombine(oldData, name + "-0.log")); + if (logInfo.exists()) { + QString lastModified = logInfo.lastModified().toString(locale.dateFormat()); + message = tr("It looks like you used %1 on %2 before. Do you want to migrate your data to the new location of %3?") + .arg(name, lastModified, BuildConfig.LAUNCHER_DISPLAYNAME); + } + } + + QMessageBox::StandardButton askMoveDialogue = + QMessageBox::question(nullptr, BuildConfig.LAUNCHER_DISPLAYNAME, message, QMessageBox::Yes | QMessageBox::No, QMessageBox::Yes); + + auto setDoNotMigrate = [&nomigratePath] { + QFile file(nomigratePath); + file.open(QIODevice::WriteOnly); + }; + + // create no-migrate file if user doesn't want to migrate + if (askMoveDialogue != QMessageBox::Yes) { + qDebug() << "<> Migration declined for" << name; + setDoNotMigrate(); + return currentExists; // cancel further migrations, if we already have a data directory + } + + if (!currentExists) { + // Migrate! + auto matcher = std::make_shared(); + matcher->add(std::make_shared(configFile)); + matcher->add(std::make_shared( + BuildConfig.LAUNCHER_CONFIGFILE)); // it's possible that we already used that directory before + matcher->add(std::make_shared("accounts.json")); + matcher->add(std::make_shared("accounts/")); + matcher->add(std::make_shared("assets/")); + matcher->add(std::make_shared("icons/")); + matcher->add(std::make_shared("instances/")); + matcher->add(std::make_shared("libraries/")); + matcher->add(std::make_shared("mods/")); + matcher->add(std::make_shared("themes/")); + + ProgressDialog diag; + DataMigrationTask task(nullptr, oldData, currentData, matcher); + if (diag.execWithTask(&task)) { + qDebug() << "<> Migration succeeded"; + setDoNotMigrate(); + } else { + QString reason = task.failReason(); + QMessageBox::critical(nullptr, BuildConfig.LAUNCHER_DISPLAYNAME, tr("Migration failed! Reason: %1").arg(reason)); + } + } else { + qWarning() << "<> Migration was skipped, due to existing data"; + } + return true; +} + diff --git a/launcher/Application.h b/launcher/Application.h index 4c2f62d4c..7884227a1 100644 --- a/launcher/Application.h +++ b/launcher/Application.h @@ -231,6 +231,7 @@ private slots: void setupWizardFinished(int status); private: + bool handleDataMigration(const QString & currentData, const QString & oldData, const QString & name, const QString & configFile) const; bool createSetupWizard(); void performMainStartupAction(); diff --git a/launcher/ApplicationMessage.cpp b/launcher/ApplicationMessage.cpp index ca276b89c..700e43ced 100644 --- a/launcher/ApplicationMessage.cpp +++ b/launcher/ApplicationMessage.cpp @@ -47,8 +47,8 @@ void ApplicationMessage::parse(const QByteArray & input) { args.clear(); auto parsedArgs = root.value("args").toObject(); - for(auto iter = parsedArgs.begin(); iter != parsedArgs.end(); iter++) { - args[iter.key()] = iter.value().toString(); + for(auto iter = parsedArgs.constBegin(); iter != parsedArgs.constEnd(); iter++) { + args.insert(iter.key(), iter.value().toString()); } } @@ -56,8 +56,8 @@ QByteArray ApplicationMessage::serialize() { QJsonObject root; root.insert("command", command); QJsonObject outArgs; - for (auto iter = args.begin(); iter != args.end(); iter++) { - outArgs[iter.key()] = iter.value(); + for (auto iter = args.constBegin(); iter != args.constEnd(); iter++) { + outArgs.insert(iter.key(), iter.value()); } root.insert("args", outArgs); diff --git a/launcher/ApplicationMessage.h b/launcher/ApplicationMessage.h index 745bdeadc..d66456ebd 100644 --- a/launcher/ApplicationMessage.h +++ b/launcher/ApplicationMessage.h @@ -1,12 +1,12 @@ #pragma once #include -#include +#include #include struct ApplicationMessage { QString command; - QMap args; + QHash args; QByteArray serialize(); void parse(const QByteArray & input); diff --git a/launcher/BaseInstance.h b/launcher/BaseInstance.h index 307240e0d..a2a4f8246 100644 --- a/launcher/BaseInstance.h +++ b/launcher/BaseInstance.h @@ -151,7 +151,7 @@ public: void copyManagedPack(BaseInstance& other); /// guess log level from a line of game log - virtual MessageLevel::Enum guessLevel(const QString &line, MessageLevel::Enum level) + virtual MessageLevel::Enum guessLevel([[maybe_unused]] const QString &line, MessageLevel::Enum level) { return level; }; diff --git a/launcher/BaseVersionList.cpp b/launcher/BaseVersionList.cpp index 4ed826123..dc95e7ea5 100644 --- a/launcher/BaseVersionList.cpp +++ b/launcher/BaseVersionList.cpp @@ -95,12 +95,12 @@ BaseVersionList::RoleList BaseVersionList::providesRoles() const int BaseVersionList::rowCount(const QModelIndex &parent) const { // Return count - return count(); + return parent.isValid() ? 0 : count(); } int BaseVersionList::columnCount(const QModelIndex &parent) const { - return 1; + return parent.isValid() ? 0 : 1; } QHash BaseVersionList::roleNames() const diff --git a/launcher/CMakeLists.txt b/launcher/CMakeLists.txt index 25dc066b5..e99b60d46 100644 --- a/launcher/CMakeLists.txt +++ b/launcher/CMakeLists.txt @@ -89,7 +89,18 @@ set(CORE_SOURCES # Time MMCTime.h MMCTime.cpp + + MTPixmapCache.h ) +if (UNIX AND NOT CYGWIN AND NOT APPLE) +set(CORE_SOURCES + ${CORE_SOURCES} + + # MangoHud + MangoHud.h + MangoHud.cpp + ) +endif() set(PATHMATCHER_SOURCES # Path matchers @@ -97,6 +108,7 @@ set(PATHMATCHER_SOURCES pathmatcher/IPathMatcher.h pathmatcher/MultiMatcher.h pathmatcher/RegexpMatcher.h + pathmatcher/SimplePrefixMatcher.h ) set(NET_SOURCES @@ -583,6 +595,8 @@ SET(LAUNCHER_SOURCES # Application base Application.h Application.cpp + DataMigrationTask.h + DataMigrationTask.cpp UpdateController.cpp UpdateController.h ApplicationMessage.h @@ -606,6 +620,8 @@ SET(LAUNCHER_SOURCES resources/pe_light/pe_light.qrc resources/pe_colored/pe_colored.qrc resources/pe_blue/pe_blue.qrc + resources/breeze_dark/breeze_dark.qrc + resources/breeze_light/breeze_light.qrc resources/OSX/OSX.qrc resources/iOS/iOS.qrc resources/flat/flat.qrc @@ -683,6 +699,8 @@ SET(LAUNCHER_SOURCES ui/pages/instance/GameOptionsPage.h ui/pages/instance/VersionPage.cpp ui/pages/instance/VersionPage.h + ui/pages/instance/ManagedPackPage.cpp + ui/pages/instance/ManagedPackPage.h ui/pages/instance/TexturePackPage.h ui/pages/instance/ResourcePackPage.h ui/pages/instance/ShaderPackPage.h @@ -801,6 +819,8 @@ SET(LAUNCHER_SOURCES ui/dialogs/ExportInstanceDialog.h ui/dialogs/IconPickerDialog.cpp ui/dialogs/IconPickerDialog.h + ui/dialogs/ImportResourcePackDialog.cpp + ui/dialogs/ImportResourcePackDialog.h ui/dialogs/LoginDialog.cpp ui/dialogs/LoginDialog.h ui/dialogs/MSALoginDialog.cpp @@ -922,6 +942,7 @@ qt_wrap_ui(LAUNCHER_UI ui/pages/instance/OtherLogsPage.ui ui/pages/instance/InstanceSettingsPage.ui ui/pages/instance/VersionPage.ui + ui/pages/instance/ManagedPackPage.ui ui/pages/instance/WorldListPage.ui ui/pages/instance/ScreenshotsPage.ui ui/pages/modplatform/atlauncher/AtlOptionalModDialog.ui @@ -949,6 +970,7 @@ qt_wrap_ui(LAUNCHER_UI ui/dialogs/SkinUploadDialog.ui ui/dialogs/ExportInstanceDialog.ui ui/dialogs/IconPickerDialog.ui + ui/dialogs/ImportResourcePackDialog.ui ui/dialogs/MSALoginDialog.ui ui/dialogs/OfflineLoginDialog.ui ui/dialogs/ElybyLoginDialog.ui @@ -968,6 +990,8 @@ qt_add_resources(LAUNCHER_RESOURCES resources/pe_light/pe_light.qrc resources/pe_colored/pe_colored.qrc resources/pe_blue/pe_blue.qrc + resources/breeze_dark/breeze_dark.qrc + resources/breeze_light/breeze_light.qrc resources/OSX/OSX.qrc resources/iOS/iOS.qrc resources/flat/flat.qrc @@ -1153,6 +1177,8 @@ if(INSTALL_BUNDLE STREQUAL "full") CONFIGURATIONS Debug RelWithDebInfo "" DESTINATION ${PLUGIN_DEST_DIR} COMPONENT Runtime + PATTERN "*qopensslbackend*" EXCLUDE + PATTERN "*qcertonlybackend*" EXCLUDE ) install( DIRECTORY "${QT_PLUGINS_DIR}/tls" @@ -1162,6 +1188,8 @@ if(INSTALL_BUNDLE STREQUAL "full") REGEX "dd\\." EXCLUDE REGEX "_debug\\." EXCLUDE REGEX "\\.dSYM" EXCLUDE + PATTERN "*qopensslbackend*" EXCLUDE + PATTERN "*qcertonlybackend*" EXCLUDE ) endif() configure_file( diff --git a/launcher/DataMigrationTask.cpp b/launcher/DataMigrationTask.cpp new file mode 100644 index 000000000..27ce5f01b --- /dev/null +++ b/launcher/DataMigrationTask.cpp @@ -0,0 +1,96 @@ +// SPDX-FileCopyrightText: 2022 Sefa Eyeoglu +// +// SPDX-License-Identifier: GPL-3.0-only + +#include "DataMigrationTask.h" + +#include "FileSystem.h" + +#include +#include +#include + +#include + +DataMigrationTask::DataMigrationTask(QObject* parent, + const QString& sourcePath, + const QString& targetPath, + const IPathMatcher::Ptr pathMatcher) + : Task(parent), m_sourcePath(sourcePath), m_targetPath(targetPath), m_pathMatcher(pathMatcher), m_copy(sourcePath, targetPath) +{ + m_copy.matcher(m_pathMatcher.get()).whitelist(true); +} + +void DataMigrationTask::executeTask() +{ + setStatus(tr("Scanning files...")); + + // 1. Scan + // Check how many files we gotta copy + m_copyFuture = QtConcurrent::run(QThreadPool::globalInstance(), [&] { + return m_copy(true); // dry run to collect amount of files + }); + connect(&m_copyFutureWatcher, &QFutureWatcher::finished, this, &DataMigrationTask::dryRunFinished); + connect(&m_copyFutureWatcher, &QFutureWatcher::canceled, this, &DataMigrationTask::dryRunAborted); + m_copyFutureWatcher.setFuture(m_copyFuture); +} + +void DataMigrationTask::dryRunFinished() +{ + disconnect(&m_copyFutureWatcher, &QFutureWatcher::finished, this, &DataMigrationTask::dryRunFinished); + disconnect(&m_copyFutureWatcher, &QFutureWatcher::canceled, this, &DataMigrationTask::dryRunAborted); + +#if (QT_VERSION >= QT_VERSION_CHECK(6, 0, 0)) + if (!m_copyFuture.isValid() || !m_copyFuture.result()) { +#else + if (!m_copyFuture.result()) { +#endif + emitFailed(tr("Failed to scan source path.")); + return; + } + + // 2. Copy + // Actually copy all files now. + m_toCopy = m_copy.totalCopied(); + connect(&m_copy, &FS::copy::fileCopied, [&, this](const QString& relativeName) { + QString shortenedName = relativeName; + // shorten the filename to hopefully fit into one line + if (shortenedName.length() > 50) + shortenedName = relativeName.left(20) + "…" + relativeName.right(29); + setProgress(m_copy.totalCopied(), m_toCopy); + setStatus(tr("Copying %1…").arg(shortenedName)); + }); + m_copyFuture = QtConcurrent::run(QThreadPool::globalInstance(), [&] { + return m_copy(false); // actually copy now + }); + connect(&m_copyFutureWatcher, &QFutureWatcher::finished, this, &DataMigrationTask::copyFinished); + connect(&m_copyFutureWatcher, &QFutureWatcher::canceled, this, &DataMigrationTask::copyAborted); + m_copyFutureWatcher.setFuture(m_copyFuture); +} + +void DataMigrationTask::dryRunAborted() +{ + emitFailed(tr("Aborted")); +} + +void DataMigrationTask::copyFinished() +{ + disconnect(&m_copyFutureWatcher, &QFutureWatcher::finished, this, &DataMigrationTask::copyFinished); + disconnect(&m_copyFutureWatcher, &QFutureWatcher::canceled, this, &DataMigrationTask::copyAborted); + +#if (QT_VERSION >= QT_VERSION_CHECK(6, 0, 0)) + if (!m_copyFuture.isValid() || !m_copyFuture.result()) { +#else + if (!m_copyFuture.result()) { +#endif + emitFailed(tr("Some paths could not be copied!")); + return; + } + + emitSucceeded(); +} + +void DataMigrationTask::copyAborted() +{ + emitFailed(tr("Aborted")); +} diff --git a/launcher/DataMigrationTask.h b/launcher/DataMigrationTask.h new file mode 100644 index 000000000..6cc23b1a8 --- /dev/null +++ b/launcher/DataMigrationTask.h @@ -0,0 +1,42 @@ +// SPDX-FileCopyrightText: 2022 Sefa Eyeoglu +// +// SPDX-License-Identifier: GPL-3.0-only + +#pragma once + +#include "FileSystem.h" +#include "pathmatcher/IPathMatcher.h" +#include "tasks/Task.h" + +#include +#include + +/* + * Migrate existing data from other MMC-like launchers. + */ + +class DataMigrationTask : public Task { + Q_OBJECT + public: + explicit DataMigrationTask(QObject* parent, const QString& sourcePath, const QString& targetPath, const IPathMatcher::Ptr pathmatcher); + ~DataMigrationTask() override = default; + + protected: + virtual void executeTask() override; + + protected slots: + void dryRunFinished(); + void dryRunAborted(); + void copyFinished(); + void copyAborted(); + + private: + const QString& m_sourcePath; + const QString& m_targetPath; + const IPathMatcher::Ptr m_pathMatcher; + + FS::copy m_copy; + int m_toCopy = 0; + QFuture m_copyFuture; + QFutureWatcher m_copyFutureWatcher; +}; diff --git a/launcher/FileSystem.cpp b/launcher/FileSystem.cpp index 4a8f4bd3d..3e8e10a51 100644 --- a/launcher/FileSystem.cpp +++ b/launcher/FileSystem.cpp @@ -49,6 +49,7 @@ #include "StringUtils.h" #if defined Q_OS_WIN32 +#define WIN32_LEAN_AND_MEAN #include #include #include @@ -152,9 +153,10 @@ bool ensureFolderPathExists(QString foldernamepath) /// @brief Copies a directory and it's contents from src to dest /// @param offset subdirectory form src to copy to dest /// @return if there was an error during the filecopy -bool copy::operator()(const QString& offset) +bool copy::operator()(const QString& offset, bool dryRun) { using copy_opts = fs::copy_options; + m_copied = 0; // reset counter // NOTE always deep copy on windows. the alternatives are too messy. #if defined Q_OS_WIN32 @@ -174,18 +176,21 @@ bool copy::operator()(const QString& offset) // Function that'll do the actual copying auto copy_file = [&](QString src_path, QString relative_dst_path) { - if (m_blacklist && m_blacklist->matches(relative_dst_path)) + if (m_matcher && (m_matcher->matches(relative_dst_path) != m_whitelist)) return; auto dst_path = PathCombine(dst, relative_dst_path); - ensureFilePathExists(dst_path); - - fs::copy(StringUtils::toStdString(src_path), StringUtils::toStdString(dst_path), opt, err); + if (!dryRun) { + ensureFilePathExists(dst_path); + fs::copy(StringUtils::toStdString(src_path), StringUtils::toStdString(dst_path), opt, err); + } if (err) { qWarning() << "Failed to copy files:" << QString::fromStdString(err.message()); qDebug() << "Source file:" << src_path; qDebug() << "Destination file:" << dst_path; } + m_copied++; + emit fileCopied(relative_dst_path); }; // We can't use copy_opts::recursive because we need to take into account the @@ -339,12 +344,35 @@ QString getDesktopDir() } // Cross-platform Shortcut creation -bool createShortCut(QString location, QString dest, QStringList args, QString name, QString icon) +bool createShortcut(QString destination, QString target, QStringList args, QString name, QString icon) { -#if defined(Q_OS_LINUX) || defined(Q_OS_FREEBSD) - location = PathCombine(location, name + ".desktop"); +#if defined(Q_OS_MACOS) + destination += ".command"; - QFile f(location); + QFile f(destination); + f.open(QIODevice::WriteOnly | QIODevice::Text); + QTextStream stream(&f); + + QString argstring; + if (!args.empty()) + argstring = " \"" + args.join("\" \"") + "\""; + + stream << "#!/bin/bash" + << "\n"; + stream << "\"" + << target + << "\" " + << argstring + << "\n"; + + stream.flush(); + f.close(); + + f.setPermissions(f.permissions() | QFileDevice::ExeOwner | QFileDevice::ExeGroup | QFileDevice::ExeOther); + + return true; +#elif defined(Q_OS_LINUX) || defined(Q_OS_FREEBSD) || defined(Q_OS_OPENBSD) + QFile f(destination); f.open(QIODevice::WriteOnly | QIODevice::Text); QTextStream stream(&f); @@ -356,10 +384,12 @@ bool createShortCut(QString location, QString dest, QStringList args, QString na << "\n"; stream << "Type=Application" << "\n"; - stream << "TryExec=" << dest.toLocal8Bit() << "\n"; - stream << "Exec=" << dest.toLocal8Bit() << argstring.toLocal8Bit() << "\n"; + stream << "Exec=\"" << target.toLocal8Bit() << "\"" << argstring.toLocal8Bit() << "\n"; stream << "Name=" << name.toLocal8Bit() << "\n"; - stream << "Icon=" << icon.toLocal8Bit() << "\n"; + if (!icon.isEmpty()) + { + stream << "Icon=" << icon.toLocal8Bit() << "\n"; + } stream.flush(); f.close(); @@ -367,25 +397,132 @@ bool createShortCut(QString location, QString dest, QStringList args, QString na f.setPermissions(f.permissions() | QFileDevice::ExeOwner | QFileDevice::ExeGroup | QFileDevice::ExeOther); return true; -#elif defined Q_OS_WIN - // TODO: Fix - // QFile file(PathCombine(location, name + ".lnk")); - // WCHAR *file_w; - // WCHAR *dest_w; - // WCHAR *args_w; - // file.fileName().toWCharArray(file_w); - // dest.toWCharArray(dest_w); +#elif defined(Q_OS_WIN) + QFileInfo targetInfo(target); - // QString argStr; - // for (int i = 0; i < args.count(); i++) - // { - // argStr.append(args[i]); - // argStr.append(" "); - // } - // argStr.toWCharArray(args_w); + if (!targetInfo.exists()) + { + qWarning() << "Target file does not exist!"; + return false; + } - // return SUCCEEDED(CreateLink(file_w, dest_w, args_w)); - return false; + target = targetInfo.absoluteFilePath(); + + if (target.length() >= MAX_PATH) + { + qWarning() << "Target file path is too long!"; + return false; + } + + if (!icon.isEmpty() && icon.length() >= MAX_PATH) + { + qWarning() << "Icon path is too long!"; + return false; + } + + destination += ".lnk"; + + if (destination.length() >= MAX_PATH) + { + qWarning() << "Destination path is too long!"; + return false; + } + + QString argStr; + int argCount = args.count(); + for (int i = 0; i < argCount; i++) + { + if (args[i].contains(' ')) + { + argStr.append('"').append(args[i]).append('"'); + } + else + { + argStr.append(args[i]); + } + + if (i < argCount - 1) + { + argStr.append(" "); + } + } + + if (argStr.length() >= MAX_PATH) + { + qWarning() << "Arguments string is too long!"; + return false; + } + + HRESULT hres; + + // ...yes, you need to initialize the entire COM stack just to make a shortcut + hres = CoInitialize(nullptr); + if (FAILED(hres)) + { + qWarning() << "Failed to initialize COM!"; + return false; + } + + WCHAR wsz[MAX_PATH]; + + IShellLink* psl; + + // create an IShellLink instance - this stores the shortcut's attributes + hres = CoCreateInstance(CLSID_ShellLink, NULL, CLSCTX_INPROC_SERVER, IID_IShellLink, (LPVOID*)&psl); + if (SUCCEEDED(hres)) + { + wmemset(wsz, 0, MAX_PATH); + target.toWCharArray(wsz); + psl->SetPath(wsz); + + wmemset(wsz, 0, MAX_PATH); + argStr.toWCharArray(wsz); + psl->SetArguments(wsz); + + wmemset(wsz, 0, MAX_PATH); + targetInfo.absolutePath().toWCharArray(wsz); + psl->SetWorkingDirectory(wsz); // "Starts in" attribute + + if (!icon.isEmpty()) + { + wmemset(wsz, 0, MAX_PATH); + icon.toWCharArray(wsz); + psl->SetIconLocation(wsz, 0); + } + + // query an IPersistFile interface from our IShellLink instance + // this is the interface that will actually let us save the shortcut to disk! + IPersistFile* ppf; + hres = psl->QueryInterface(IID_IPersistFile, (LPVOID*)&ppf); + if (SUCCEEDED(hres)) + { + wmemset(wsz, 0, MAX_PATH); + destination.toWCharArray(wsz); + hres = ppf->Save(wsz, TRUE); + if (FAILED(hres)) + { + qWarning() << "IPresistFile->Save() failed"; + qWarning() << "hres = " << hres; + } + ppf->Release(); + } + else + { + qWarning() << "Failed to query IPersistFile interface from IShellLink instance"; + qWarning() << "hres = " << hres; + } + psl->Release(); + } + else + { + qWarning() << "Failed to create IShellLink instance"; + qWarning() << "hres = " << hres; + } + + // go away COM, nobody likes you + CoUninitialize(); + + return SUCCEEDED(hres); #else qWarning("Desktop Shortcuts not supported on your platform!"); return false; diff --git a/launcher/FileSystem.h b/launcher/FileSystem.h index b7e175fdf..ac8937258 100644 --- a/launcher/FileSystem.h +++ b/launcher/FileSystem.h @@ -40,6 +40,7 @@ #include #include +#include namespace FS { @@ -76,9 +77,10 @@ bool ensureFilePathExists(QString filenamepath); bool ensureFolderPathExists(QString filenamepath); /// @brief Copies a directory and it's contents from src to dest -class copy { +class copy : public QObject { + Q_OBJECT public: - copy(const QString& src, const QString& dst) + copy(const QString& src, const QString& dst, QObject* parent = nullptr) : QObject(parent) { m_src.setPath(src); m_dst.setPath(dst); @@ -88,21 +90,35 @@ class copy { m_followSymlinks = follow; return *this; } - copy& blacklist(const IPathMatcher* filter) + copy& matcher(const IPathMatcher* filter) { - m_blacklist = filter; + m_matcher = filter; return *this; } - bool operator()() { return operator()(QString()); } + copy& whitelist(bool whitelist) + { + m_whitelist = whitelist; + return *this; + } + + bool operator()(bool dryRun = false) { return operator()(QString(), dryRun); } + + int totalCopied() { return m_copied; } + + signals: + void fileCopied(const QString& relativeName); + // TODO: maybe add a "shouldCopy" signal in the future? private: - bool operator()(const QString& offset); + bool operator()(const QString& offset, bool dryRun = false); private: bool m_followSymlinks = true; - const IPathMatcher* m_blacklist = nullptr; + const IPathMatcher* m_matcher = nullptr; + bool m_whitelist = false; QDir m_src; QDir m_dst; + int m_copied; }; /** @@ -156,4 +172,9 @@ QString getDesktopDir(); // Overrides one folder with the contents of another, preserving items exclusive to the first folder // Equivalent to doing QDir::rename, but allowing for overrides bool overrideFolder(QString overwritten_path, QString override_path); + +/** + * Creates a shortcut to the specified target file at the specified destination path. + */ +bool createShortcut(QString destination, QString target, QStringList args, QString name, QString icon); } diff --git a/launcher/InstanceCopyTask.cpp b/launcher/InstanceCopyTask.cpp index a4ea947d2..188d163b6 100644 --- a/launcher/InstanceCopyTask.cpp +++ b/launcher/InstanceCopyTask.cpp @@ -25,10 +25,12 @@ void InstanceCopyTask::executeTask() { setStatus(tr("Copying instance %1").arg(m_origInstance->name())); - FS::copy folderCopy(m_origInstance->instanceRoot(), m_stagingPath); - folderCopy.followSymlinks(false).blacklist(m_matcher.get()); + m_copyFuture = QtConcurrent::run(QThreadPool::globalInstance(), [this]{ + FS::copy folderCopy(m_origInstance->instanceRoot(), m_stagingPath); + folderCopy.followSymlinks(false).matcher(m_matcher.get()); - m_copyFuture = QtConcurrent::run(QThreadPool::globalInstance(), folderCopy); + return folderCopy(); + }); connect(&m_copyFutureWatcher, &QFutureWatcher::finished, this, &InstanceCopyTask::copyFinished); connect(&m_copyFutureWatcher, &QFutureWatcher::canceled, this, &InstanceCopyTask::copyAborted); m_copyFutureWatcher.setFuture(m_copyFuture); diff --git a/launcher/InstanceCreationTask.cpp b/launcher/InstanceCreationTask.cpp index 3971effaf..73dc17891 100644 --- a/launcher/InstanceCreationTask.cpp +++ b/launcher/InstanceCreationTask.cpp @@ -25,9 +25,13 @@ void InstanceCreationTask::executeTask() return; qWarning() << "Instance creation failed!"; - if (!m_error_message.isEmpty()) + if (!m_error_message.isEmpty()) { qWarning() << "Reason: " << m_error_message; - emitFailed(tr("Error while creating new instance.")); + emitFailed(tr("Error while creating new instance:\n%1").arg(m_error_message)); + } else { + emitFailed(tr("Error while creating new instance.")); + } + return; } diff --git a/launcher/InstanceImportTask.cpp b/launcher/InstanceImportTask.cpp index b490620d5..6b3fd296f 100644 --- a/launcher/InstanceImportTask.cpp +++ b/launcher/InstanceImportTask.cpp @@ -55,11 +55,9 @@ #include -InstanceImportTask::InstanceImportTask(const QUrl sourceUrl, QWidget* parent) -{ - m_sourceUrl = sourceUrl; - m_parent = parent; -} +InstanceImportTask::InstanceImportTask(const QUrl sourceUrl, QWidget* parent, QMap&& extra_info) + : m_sourceUrl(sourceUrl), m_extra_info(extra_info), m_parent(parent) +{} bool InstanceImportTask::abort() { @@ -164,18 +162,14 @@ void InstanceImportTask::processZipPack() } else { - QString mmcRoot = MMCZip::findFolderOfFileInZip(m_packZip.get(), "instance.cfg"); - QString flameRoot = MMCZip::findFolderOfFileInZip(m_packZip.get(), "manifest.json"); + QStringList paths_to_ignore { "overrides/" }; - if (!mmcRoot.isNull()) - { + if (QString mmcRoot = MMCZip::findFolderOfFileInZip(m_packZip.get(), "instance.cfg", paths_to_ignore); !mmcRoot.isNull()) { // process as MultiMC instance/pack qDebug() << "MultiMC:" << mmcRoot; root = mmcRoot; m_modpackType = ModpackType::MultiMC; - } - else if(!flameRoot.isNull()) - { + } else if (QString flameRoot = MMCZip::findFolderOfFileInZip(m_packZip.get(), "manifest.json", paths_to_ignore); !flameRoot.isNull()) { // process as Flame pack qDebug() << "Flame:" << flameRoot; root = flameRoot; @@ -263,14 +257,34 @@ void InstanceImportTask::extractAborted() void InstanceImportTask::processFlame() { - auto* inst_creation_task = new FlameCreationTask(m_stagingPath, m_globalSettings, m_parent); + FlameCreationTask* inst_creation_task = nullptr; + if (!m_extra_info.isEmpty()) { + auto pack_id_it = m_extra_info.constFind("pack_id"); + Q_ASSERT(pack_id_it != m_extra_info.constEnd()); + auto pack_id = pack_id_it.value(); + + auto pack_version_id_it = m_extra_info.constFind("pack_version_id"); + Q_ASSERT(pack_version_id_it != m_extra_info.constEnd()); + auto pack_version_id = pack_version_id_it.value(); + + QString original_instance_id; + auto original_instance_id_it = m_extra_info.constFind("original_instance_id"); + if (original_instance_id_it != m_extra_info.constEnd()) + original_instance_id = original_instance_id_it.value(); + + inst_creation_task = new FlameCreationTask(m_stagingPath, m_globalSettings, m_parent, pack_id, pack_version_id, original_instance_id); + } else { + // FIXME: Find a way to get IDs in directly imported ZIPs + inst_creation_task = new FlameCreationTask(m_stagingPath, m_globalSettings, m_parent, {}, {}); + } inst_creation_task->setName(*this); inst_creation_task->setIcon(m_instIcon); inst_creation_task->setGroup(m_instGroup); + inst_creation_task->setConfirmUpdate(shouldConfirmUpdate()); connect(inst_creation_task, &Task::succeeded, this, [this, inst_creation_task] { - setOverride(inst_creation_task->shouldOverride()); + setOverride(inst_creation_task->shouldOverride(), inst_creation_task->originalInstanceID()); emitSucceeded(); }); connect(inst_creation_task, &Task::failed, this, &InstanceImportTask::emitFailed); @@ -327,14 +341,41 @@ void InstanceImportTask::processMultiMC() void InstanceImportTask::processModrinth() { - auto* inst_creation_task = new ModrinthCreationTask(m_stagingPath, m_globalSettings, m_parent, m_sourceUrl.toString()); + ModrinthCreationTask* inst_creation_task = nullptr; + if (!m_extra_info.isEmpty()) { + auto pack_id_it = m_extra_info.constFind("pack_id"); + Q_ASSERT(pack_id_it != m_extra_info.constEnd()); + auto pack_id = pack_id_it.value(); + + QString pack_version_id; + auto pack_version_id_it = m_extra_info.constFind("pack_version_id"); + if (pack_version_id_it != m_extra_info.constEnd()) + pack_version_id = pack_version_id_it.value(); + + QString original_instance_id; + auto original_instance_id_it = m_extra_info.constFind("original_instance_id"); + if (original_instance_id_it != m_extra_info.constEnd()) + original_instance_id = original_instance_id_it.value(); + + inst_creation_task = new ModrinthCreationTask(m_stagingPath, m_globalSettings, m_parent, pack_id, pack_version_id, original_instance_id); + } else { + QString pack_id; + if (!m_sourceUrl.isEmpty()) { + QRegularExpression regex(R"(data\/(.*)\/versions)"); + pack_id = regex.match(m_sourceUrl.toString()).captured(1); + } + + // FIXME: Find a way to get the ID in directly imported ZIPs + inst_creation_task = new ModrinthCreationTask(m_stagingPath, m_globalSettings, m_parent, pack_id); + } inst_creation_task->setName(*this); inst_creation_task->setIcon(m_instIcon); inst_creation_task->setGroup(m_instGroup); + inst_creation_task->setConfirmUpdate(shouldConfirmUpdate()); connect(inst_creation_task, &Task::succeeded, this, [this, inst_creation_task] { - setOverride(inst_creation_task->shouldOverride()); + setOverride(inst_creation_task->shouldOverride(), inst_creation_task->originalInstanceID()); emitSucceeded(); }); connect(inst_creation_task, &Task::failed, this, &InstanceImportTask::emitFailed); diff --git a/launcher/InstanceImportTask.h b/launcher/InstanceImportTask.h index ef70c819f..6b8ac966c 100644 --- a/launcher/InstanceImportTask.h +++ b/launcher/InstanceImportTask.h @@ -56,7 +56,7 @@ class InstanceImportTask : public InstanceTask { Q_OBJECT public: - explicit InstanceImportTask(const QUrl sourceUrl, QWidget* parent = nullptr); + explicit InstanceImportTask(const QUrl sourceUrl, QWidget* parent = nullptr, QMap&& extra_info = {}); bool abort() override; const QVector &getBlockedFiles() const @@ -101,6 +101,10 @@ private: /* data */ Modrinth, } m_modpackType = ModpackType::Unknown; + // Extra info we might need, that's available before, but can't be derived from + // the source URL / the resource it points to alone. + QMap m_extra_info; + //FIXME: nuke QWidget* m_parent; }; diff --git a/launcher/InstanceList.cpp b/launcher/InstanceList.cpp index cebd70d7f..68e3e92cb 100644 --- a/launcher/InstanceList.cpp +++ b/launcher/InstanceList.cpp @@ -816,7 +816,7 @@ class InstanceStaging : public Task { void childSucceded() { unsigned sleepTime = backoff(); - if (m_parent->commitStagedInstance(m_stagingPath, m_instance_name, m_groupName, m_child->shouldOverride())) + if (m_parent->commitStagedInstance(m_stagingPath, m_instance_name, m_groupName, *m_child.get())) { emitSucceeded(); return; @@ -880,25 +880,22 @@ QString InstanceList::getStagedInstancePath() return path; } -bool InstanceList::commitStagedInstance(const QString& path, InstanceName const& instanceName, const QString& groupName, bool should_override) +bool InstanceList::commitStagedInstance(const QString& path, InstanceName const& instanceName, const QString& groupName, InstanceTask const& commiting) { QDir dir; QString instID; InstancePtr inst; + auto should_override = commiting.shouldOverride(); + if (should_override) { - // This is to avoid problems when the instance folder gets manually renamed - if ((inst = getInstanceByManagedName(instanceName.originalName()))) { - instID = QFileInfo(inst->instanceRoot()).fileName(); - } else if ((inst = getInstanceByManagedName(instanceName.modifiedName()))) { - instID = QFileInfo(inst->instanceRoot()).fileName(); - } else { - instID = FS::RemoveInvalidFilenameChars(instanceName.modifiedName(), '-'); - } + instID = commiting.originalInstanceID(); } else { instID = FS::DirNameFromString(instanceName.modifiedName(), m_instDir); } + Q_ASSERT(!instID.isEmpty()); + { WatchLock lock(m_watcher, m_instDir); QString destination = FS::PathCombine(m_instDir, instID); diff --git a/launcher/InstanceList.h b/launcher/InstanceList.h index 3673298f2..edacba3cd 100644 --- a/launcher/InstanceList.h +++ b/launcher/InstanceList.h @@ -133,7 +133,7 @@ public: * should_override is used when another similar instance already exists, and we want to override it * - for instance, when updating it. */ - bool commitStagedInstance(const QString& keyPath, const InstanceName& instanceName, const QString& groupName, bool should_override); + bool commitStagedInstance(const QString& keyPath, const InstanceName& instanceName, const QString& groupName, const InstanceTask&); /** * Destroy a previously created staging area given by @keyPath - used when creation fails. diff --git a/launcher/InstancePageProvider.h b/launcher/InstancePageProvider.h index bf29377dd..5d8beca93 100644 --- a/launcher/InstancePageProvider.h +++ b/launcher/InstancePageProvider.h @@ -5,6 +5,7 @@ #include "ui/pages/BasePageProvider.h" #include "ui/pages/instance/LogPage.h" #include "ui/pages/instance/VersionPage.h" +#include "ui/pages/instance/ManagedPackPage.h" #include "ui/pages/instance/ModFolderPage.h" #include "ui/pages/instance/ResourcePackPage.h" #include "ui/pages/instance/TexturePackPage.h" @@ -33,6 +34,7 @@ public: values.append(new LogPage(inst)); std::shared_ptr onesix = std::dynamic_pointer_cast(inst); values.append(new VersionPage(onesix.get())); + values.append(ManagedPackPage::createPage(onesix.get())); auto modsPage = new ModFolderPage(onesix.get(), onesix->loaderModList()); modsPage->setFilter("%1 (*.zip *.jar *.litemod)"); values.append(modsPage); diff --git a/launcher/InstanceTask.cpp b/launcher/InstanceTask.cpp index 55a44fd3b..066827829 100644 --- a/launcher/InstanceTask.cpp +++ b/launcher/InstanceTask.cpp @@ -18,6 +18,29 @@ InstanceNameChange askForChangingInstanceName(QWidget* parent, const QString& ol return InstanceNameChange::ShouldKeep; } +ShouldUpdate askIfShouldUpdate(QWidget *parent, QString original_version_name) +{ + auto info = CustomMessageBox::selectable( + parent, QObject::tr("Similar modpack was found!"), + QObject::tr("One or more of your instances are from this same modpack%1. Do you want to create a " + "separate instance, or update the existing one?\n\nNOTE: Make sure you made a backup of your important instance data before " + "updating, as worlds can be corrupted and some configuration may be lost (due to pack overrides).") + .arg(original_version_name), + QMessageBox::Information, QMessageBox::Ok | QMessageBox::Reset | QMessageBox::Abort); + info->setButtonText(QMessageBox::Ok, QObject::tr("Update existing instance")); + info->setButtonText(QMessageBox::Abort, QObject::tr("Create new instance")); + info->setButtonText(QMessageBox::Reset, QObject::tr("Cancel")); + + info->exec(); + + if (info->clickedButton() == info->button(QMessageBox::Ok)) + return ShouldUpdate::Update; + if (info->clickedButton() == info->button(QMessageBox::Abort)) + return ShouldUpdate::SkipUpdating; + return ShouldUpdate::Cancel; + +} + QString InstanceName::name() const { if (!m_modified_name.isEmpty()) diff --git a/launcher/InstanceTask.h b/launcher/InstanceTask.h index e35533fc4..7c02160a7 100644 --- a/launcher/InstanceTask.h +++ b/launcher/InstanceTask.h @@ -6,6 +6,8 @@ /* Helpers */ enum class InstanceNameChange { ShouldChange, ShouldKeep }; [[nodiscard]] InstanceNameChange askForChangingInstanceName(QWidget* parent, const QString& old_name, const QString& new_name); +enum class ShouldUpdate { Update, SkipUpdating, Cancel }; +[[nodiscard]] ShouldUpdate askIfShouldUpdate(QWidget* parent, QString original_version_name); struct InstanceName { public: @@ -42,10 +44,20 @@ class InstanceTask : public Task, public InstanceName { void setGroup(const QString& group) { m_instGroup = group; } QString group() const { return m_instGroup; } + [[nodiscard]] bool shouldConfirmUpdate() const { return m_confirm_update; } + void setConfirmUpdate(bool confirm) { m_confirm_update = confirm; } + bool shouldOverride() const { return m_override_existing; } + [[nodiscard]] QString originalInstanceID() const { return m_original_instance_id; }; + protected: - void setOverride(bool override) { m_override_existing = override; } + void setOverride(bool override, QString instance_id_to_override = {}) + { + m_override_existing = override; + if (!instance_id_to_override.isEmpty()) + m_original_instance_id = instance_id_to_override; + } protected: /* data */ SettingsObjectPtr m_globalSettings; @@ -54,4 +66,7 @@ class InstanceTask : public Task, public InstanceName { QString m_stagingPath; bool m_override_existing = false; + bool m_confirm_update = true; + + QString m_original_instance_id; }; diff --git a/launcher/MMCTime.cpp b/launcher/MMCTime.cpp index 4d7f424de..70bc4135f 100644 --- a/launcher/MMCTime.cpp +++ b/launcher/MMCTime.cpp @@ -28,11 +28,11 @@ QString Time::prettifyDuration(int64_t duration) { int days = (int) (duration / 24); if((hours == 0)&&(days == 0)) { - return QObject::tr("%1m %2s").arg(minutes).arg(seconds); + return QObject::tr("%1min %2s").arg(minutes).arg(seconds); } if (days == 0) { - return QObject::tr("%1h %2m").arg(hours).arg(minutes); + return QObject::tr("%1h %2min").arg(hours).arg(minutes); } - return QObject::tr("%1d %2h %3m").arg(days).arg(hours).arg(minutes); + return QObject::tr("%1d %2h %3min").arg(days).arg(hours).arg(minutes); } diff --git a/launcher/MMCZip.cpp b/launcher/MMCZip.cpp index 9f4e968f7..f66003432 100644 --- a/launcher/MMCZip.cpp +++ b/launcher/MMCZip.cpp @@ -39,6 +39,7 @@ #include "MMCZip.h" #include "FileSystem.h" +#include #include // ours @@ -228,23 +229,27 @@ bool MMCZip::createModdedJar(QString sourceJarPath, QString targetJarPath, const } // ours -QString MMCZip::findFolderOfFileInZip(QuaZip * zip, const QString & what, const QString &root) +QString MMCZip::findFolderOfFileInZip(QuaZip* zip, const QString& what, const QStringList& ignore_paths, const QString& root) { QuaZipDir rootDir(zip, root); - for(auto fileName: rootDir.entryList(QDir::Files)) - { - if(fileName == what) + for (auto&& fileName : rootDir.entryList(QDir::Files)) { + if (fileName == what) return root; + + QCoreApplication::processEvents(); } - for(auto fileName: rootDir.entryList(QDir::Dirs)) - { - QString result = findFolderOfFileInZip(zip, what, root + fileName); - if(!result.isEmpty()) - { + + // Recurse the search to non-ignored subfolders + for (auto&& fileName : rootDir.entryList(QDir::Dirs)) { + if (ignore_paths.contains(fileName)) + continue; + + QString result = findFolderOfFileInZip(zip, what, ignore_paths, root + fileName); + if (!result.isEmpty()) return result; - } } - return QString(); + + return {}; } // ours diff --git a/launcher/MMCZip.h b/launcher/MMCZip.h index ce9775bdb..81f9cb908 100644 --- a/launcher/MMCZip.h +++ b/launcher/MMCZip.h @@ -80,9 +80,11 @@ namespace MMCZip /** * Find a single file in archive by file name (not path) * + * \param ignore_paths paths to skip when recursing the search + * * \return the path prefix where the file is */ - QString findFolderOfFileInZip(QuaZip * zip, const QString & what, const QString &root = QString("")); + QString findFolderOfFileInZip(QuaZip * zip, const QString & what, const QStringList& ignore_paths = {}, const QString &root = QString("")); /** * Find a multiple files of the same name in archive by file name diff --git a/launcher/MTPixmapCache.h b/launcher/MTPixmapCache.h new file mode 100644 index 000000000..57847a0e1 --- /dev/null +++ b/launcher/MTPixmapCache.h @@ -0,0 +1,95 @@ +#pragma once + +#include +#include +#include + +#define GET_TYPE() \ + Qt::ConnectionType type; \ + if (QThread::currentThread() != QCoreApplication::instance()->thread()) \ + type = Qt::BlockingQueuedConnection; \ + else \ + type = Qt::DirectConnection; + +#define DEFINE_FUNC_NO_PARAM(NAME, RET_TYPE) \ + static RET_TYPE NAME() \ + { \ + RET_TYPE ret; \ + GET_TYPE() \ + QMetaObject::invokeMethod(s_instance, "_" #NAME, type, Q_RETURN_ARG(RET_TYPE, ret)); \ + return ret; \ + } +#define DEFINE_FUNC_ONE_PARAM(NAME, RET_TYPE, PARAM_1_TYPE) \ + static RET_TYPE NAME(PARAM_1_TYPE p1) \ + { \ + RET_TYPE ret; \ + GET_TYPE() \ + QMetaObject::invokeMethod(s_instance, "_" #NAME, type, Q_RETURN_ARG(RET_TYPE, ret), Q_ARG(PARAM_1_TYPE, p1)); \ + return ret; \ + } +#define DEFINE_FUNC_TWO_PARAM(NAME, RET_TYPE, PARAM_1_TYPE, PARAM_2_TYPE) \ + static RET_TYPE NAME(PARAM_1_TYPE p1, PARAM_2_TYPE p2) \ + { \ + RET_TYPE ret; \ + GET_TYPE() \ + QMetaObject::invokeMethod(s_instance, "_" #NAME, type, Q_RETURN_ARG(RET_TYPE, ret), Q_ARG(PARAM_1_TYPE, p1), \ + Q_ARG(PARAM_2_TYPE, p2)); \ + return ret; \ + } + +/** A wrapper around QPixmapCache with thread affinity with the main thread. + */ +class PixmapCache final : public QObject { + Q_OBJECT + + public: + PixmapCache(QObject* parent) : QObject(parent) {} + ~PixmapCache() override = default; + + static PixmapCache& instance() { return *s_instance; } + static void setInstance(PixmapCache* i) { s_instance = i; } + + public: + DEFINE_FUNC_NO_PARAM(cacheLimit, int) + DEFINE_FUNC_NO_PARAM(clear, bool) + DEFINE_FUNC_TWO_PARAM(find, bool, const QString&, QPixmap*) + DEFINE_FUNC_TWO_PARAM(find, bool, const QPixmapCache::Key&, QPixmap*) + DEFINE_FUNC_TWO_PARAM(insert, bool, const QString&, const QPixmap&) + DEFINE_FUNC_ONE_PARAM(insert, QPixmapCache::Key, const QPixmap&) + DEFINE_FUNC_ONE_PARAM(remove, bool, const QString&) + DEFINE_FUNC_ONE_PARAM(remove, bool, const QPixmapCache::Key&) + DEFINE_FUNC_TWO_PARAM(replace, bool, const QPixmapCache::Key&, const QPixmap&) + DEFINE_FUNC_ONE_PARAM(setCacheLimit, bool, int) + + // NOTE: Every function returns something non-void to simplify the macros. + private slots: + int _cacheLimit() { return QPixmapCache::cacheLimit(); } + bool _clear() + { + QPixmapCache::clear(); + return true; + } + bool _find(const QString& key, QPixmap* pixmap) { return QPixmapCache::find(key, pixmap); } + bool _find(const QPixmapCache::Key& key, QPixmap* pixmap) { return QPixmapCache::find(key, pixmap); } + bool _insert(const QString& key, const QPixmap& pixmap) { return QPixmapCache::insert(key, pixmap); } + QPixmapCache::Key _insert(const QPixmap& pixmap) { return QPixmapCache::insert(pixmap); } + bool _remove(const QString& key) + { + QPixmapCache::remove(key); + return true; + } + bool _remove(const QPixmapCache::Key& key) + { + QPixmapCache::remove(key); + return true; + } + bool _replace(const QPixmapCache::Key& key, const QPixmap& pixmap) { return QPixmapCache::replace(key, pixmap); } + bool _setCacheLimit(int n) + { + QPixmapCache::setCacheLimit(n); + return true; + } + + private: + static PixmapCache* s_instance; +}; diff --git a/launcher/MangoHud.cpp b/launcher/MangoHud.cpp new file mode 100644 index 000000000..d635518e3 --- /dev/null +++ b/launcher/MangoHud.cpp @@ -0,0 +1,90 @@ +// SPDX-License-Identifier: GPL-3.0-only +/* + * PrismLauncher - Minecraft Launcher + * Copyright (C) 2022 Jan Drögehoff + * + * 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 +#include +#include +#include + +#include "MangoHud.h" +#include "FileSystem.h" +#include "Json.h" + +namespace MangoHud { + +QString getLibraryString() +{ + /* + * Check for vulkan layers in this order: + * + * $VK_LAYER_PATH + * $XDG_DATA_DIRS (/usr/local/share/:/usr/share/) + * $XDG_DATA_HOME (~/.local/share) + * /etc + * $XDG_CONFIG_DIRS (/etc/xdg) + * $XDG_CONFIG_HOME (~/.config) + */ + + QStringList vkLayerList; + { + QString home = QDir::homePath(); + + QString vkLayerPath = qEnvironmentVariable("VK_LAYER_PATH"); + if (!vkLayerPath.isEmpty()) { + vkLayerList << vkLayerPath; + } + + QStringList xdgDataDirs = qEnvironmentVariable("XDG_DATA_DIRS", "/usr/local/share/:/usr/share/").split(QLatin1String(":")); + for (QString dir : xdgDataDirs) { + vkLayerList << FS::PathCombine(dir, "vulkan", "implicit_layer.d"); + } + + QString xdgDataHome = qEnvironmentVariable("XDG_DATA_HOME"); + if (xdgDataHome.isEmpty()) { + xdgDataHome = FS::PathCombine(home, ".local", "share"); + } + vkLayerList << FS::PathCombine(xdgDataHome, "vulkan", "implicit_layer.d"); + + vkLayerList << "/etc"; + + QStringList xdgConfigDirs = qEnvironmentVariable("XDG_CONFIG_DIRS", "/etc/xdg").split(QLatin1String(":")); + for (QString dir : xdgConfigDirs) { + vkLayerList << FS::PathCombine(dir, "vulkan", "implicit_layer.d"); + } + + QString xdgConfigHome = qEnvironmentVariable("XDG_CONFIG_HOME"); + if (xdgConfigHome.isEmpty()) { + xdgConfigHome = FS::PathCombine(home, ".config"); + } + vkLayerList << FS::PathCombine(xdgConfigHome, "vulkan", "implicit_layer.d"); + } + + for (QString vkLayer : vkLayerList) { + QString filePath = FS::PathCombine(vkLayer, "MangoHud.json"); + if (!QFile::exists(filePath)) + continue; + + auto conf = Json::requireDocument(filePath, vkLayer); + auto confObject = Json::requireObject(conf, vkLayer); + auto layer = Json::ensureObject(confObject, "layer"); + return Json::ensureString(layer, "library_path"); + } + + return QString(); +} +} // namespace MangoHud diff --git a/launcher/MangoHud.h b/launcher/MangoHud.h new file mode 100644 index 000000000..7b7c2849c --- /dev/null +++ b/launcher/MangoHud.h @@ -0,0 +1,27 @@ +// SPDX-License-Identifier: GPL-3.0-only +/* + * PrismLauncher - Minecraft Launcher + * Copyright (C) 2022 Jan Drögehoff + * + * 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 MangoHud { + +QString getLibraryString(); +} diff --git a/launcher/VersionProxyModel.cpp b/launcher/VersionProxyModel.cpp index 032f21f94..6aba268d8 100644 --- a/launcher/VersionProxyModel.cpp +++ b/launcher/VersionProxyModel.cpp @@ -311,14 +311,14 @@ QModelIndex VersionProxyModel::index(int row, int column, const QModelIndex &par int VersionProxyModel::columnCount(const QModelIndex &parent) const { - return m_columns.size(); + return parent.isValid() ? 0 : m_columns.size(); } int VersionProxyModel::rowCount(const QModelIndex &parent) const { if(sourceModel()) { - return sourceModel()->rowCount(); + return sourceModel()->rowCount(parent); } return 0; } diff --git a/launcher/icons/IconList.cpp b/launcher/icons/IconList.cpp index 3a223d1b6..1dfc64324 100644 --- a/launcher/icons/IconList.cpp +++ b/launcher/icons/IconList.cpp @@ -242,7 +242,7 @@ Qt::DropActions IconList::supportedDropActions() const return Qt::CopyAction; } -bool IconList::dropMimeData(const QMimeData *data, Qt::DropAction action, int row, int column, const QModelIndex &parent) +bool IconList::dropMimeData(const QMimeData *data, Qt::DropAction action, [[maybe_unused]] int row, [[maybe_unused]] int column, [[maybe_unused]] const QModelIndex &parent) { if (action == Qt::IgnoreAction) return true; @@ -302,7 +302,7 @@ QVariant IconList::data(const QModelIndex &index, int role) const int IconList::rowCount(const QModelIndex &parent) const { - return icons.size(); + return parent.isValid() ? 0 : icons.size(); } void IconList::installIcons(const QStringList &iconFiles) @@ -354,15 +354,18 @@ const MMCIcon *IconList::icon(const QString &key) const bool IconList::deleteIcon(const QString &key) { - int iconIdx = getIconIndex(key); - if (iconIdx == -1) + if (!iconFileExists(key)) return false; - auto &iconEntry = icons[iconIdx]; - if (iconEntry.has(IconType::FileBased)) - { - return QFile::remove(iconEntry.m_images[IconType::FileBased].filename); - } - return false; + + return QFile::remove(icon(key)->getFilePath()); +} + +bool IconList::trashIcon(const QString &key) +{ + if (!iconFileExists(key)) + return false; + + return FS::trash(icon(key)->getFilePath(), nullptr); } bool IconList::addThemeIcon(const QString& key) diff --git a/launcher/icons/IconList.h b/launcher/icons/IconList.h index f9f49e7f7..97141e4ae 100644 --- a/launcher/icons/IconList.h +++ b/launcher/icons/IconList.h @@ -52,6 +52,7 @@ public: bool addIcon(const QString &key, const QString &name, const QString &path, const IconType type); void saveIcon(const QString &key, const QString &path, const char * format) const; bool deleteIcon(const QString &key); + bool trashIcon(const QString &key); bool iconFileExists(const QString &key) const; void installIcons(const QStringList &iconFiles); diff --git a/launcher/main.cpp b/launcher/main.cpp index df5964493..b63f8bfd0 100644 --- a/launcher/main.cpp +++ b/launcher/main.cpp @@ -81,6 +81,8 @@ int main(int argc, char *argv[]) Q_INIT_RESOURCE(pe_light); Q_INIT_RESOURCE(pe_blue); Q_INIT_RESOURCE(pe_colored); + Q_INIT_RESOURCE(breeze_dark); + Q_INIT_RESOURCE(breeze_light); Q_INIT_RESOURCE(OSX); Q_INIT_RESOURCE(iOS); Q_INIT_RESOURCE(flat); diff --git a/launcher/meta/Index.cpp b/launcher/meta/Index.cpp index eec1b329b..242aad9f9 100644 --- a/launcher/meta/Index.cpp +++ b/launcher/meta/Index.cpp @@ -58,11 +58,11 @@ QVariant Index::data(const QModelIndex &index, int role) const } int Index::rowCount(const QModelIndex &parent) const { - return m_lists.size(); + return parent.isValid() ? 0 : m_lists.size(); } int Index::columnCount(const QModelIndex &parent) const { - return 1; + return parent.isValid() ? 0 : 1; } QVariant Index::headerData(int section, Qt::Orientation orientation, int role) const { diff --git a/launcher/meta/JsonFormat.h b/launcher/meta/JsonFormat.h index 93217b7e0..63128a4e6 100644 --- a/launcher/meta/JsonFormat.h +++ b/launcher/meta/JsonFormat.h @@ -60,11 +60,6 @@ struct Require QString suggests; }; -inline Q_DECL_PURE_FUNCTION uint qHash(const Require &key, uint seed = 0) Q_DECL_NOTHROW -{ - return qHash(key.uid, seed); -} - using RequireSet = std::set; void parseIndex(const QJsonObject &obj, Index *ptr); diff --git a/launcher/minecraft/Agent.h b/launcher/minecraft/Agent.h index 01109dafa..374e6e94e 100644 --- a/launcher/minecraft/Agent.h +++ b/launcher/minecraft/Agent.h @@ -10,7 +10,7 @@ typedef std::shared_ptr AgentPtr; class Agent { public: - Agent(LibraryPtr library, QString &argument) + Agent(LibraryPtr library, const QString &argument) { m_library = library; m_argument = argument; diff --git a/launcher/minecraft/MinecraftInstance.cpp b/launcher/minecraft/MinecraftInstance.cpp index 4d164b1dd..71bac4db3 100644 --- a/launcher/minecraft/MinecraftInstance.cpp +++ b/launcher/minecraft/MinecraftInstance.cpp @@ -1,8 +1,9 @@ // SPDX-License-Identifier: GPL-3.0-only /* - * PolyMC - Minecraft Launcher + * Prism Launcher - Minecraft Launcher * Copyright (C) 2022 Sefa Eyeoglu * Copyright (C) 2022 Jamie Mansfield + * 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 @@ -87,6 +88,10 @@ #include "minecraft/gameoptions/GameOptions.h" #include "minecraft/update/FoldersTask.h" +#ifdef Q_OS_LINUX +#include "MangoHud.h" +#endif + #define IBUS "@im=ibus" // all of this because keeping things compatible with deprecated old settings @@ -444,6 +449,17 @@ QStringList MinecraftInstance::javaArguments() 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")) + return "legacy"; + + return "standard"; +} + QMap MinecraftInstance::getVariables() { QMap out; @@ -478,9 +494,22 @@ QProcessEnvironment MinecraftInstance::createLaunchEnvironment() #ifdef Q_OS_LINUX if (settings()->get("EnableMangoHud").toBool() && APPLICATION->capabilities() & Application::SupportsMangoHud) { - auto preload = env.value("LD_PRELOAD", "") + ":libMangoHud_dlsym.so:libMangoHud.so"; - env.insert("LD_PRELOAD", preload); + auto preloadList = env.value("LD_PRELOAD").split(QLatin1String(":")); + auto libPaths = env.value("LD_LIBRARY_PATH").split(QLatin1String(":")); + + auto mangoHudLibString = MangoHud::getLibraryString(); + if (!mangoHudLibString.isEmpty()) + { + QFileInfo mangoHudLib(mangoHudLibString); + + // dlsym variant is only needed for OpenGL and not included in the vulkan layer + preloadList << "libMangoHud_dlsym.so" << mangoHudLib.fileName(); + libPaths << mangoHudLib.absolutePath(); + } + + env.insert("LD_PRELOAD", preloadList.join(QLatin1String(":"))); + env.insert("LD_LIBRARY_PATH", libPaths.join(QLatin1String(":"))); env.insert("MANGOHUD", "1"); } @@ -635,26 +664,13 @@ QString MinecraftInstance::createLaunchScript(AuthSessionPtr session, MinecraftS launchScript += "sessionId " + session->session + "\n"; } - // libraries and class path. - { - QStringList jars, nativeJars; - profile->getLibraryFiles(runtimeContext(), jars, nativeJars, getLocalLibraryPath(), binRoot()); - for(auto file: jars) - { - launchScript += "cp " + file + "\n"; - } - for(auto file: nativeJars) - { - launchScript += "ext " + file + "\n"; - } - launchScript += "natives " + getNativePath() + "\n"; - } - for (auto trait : profile->getTraits()) { launchScript += "traits " + trait + "\n"; } - launchScript += "launcher onesix\n"; + + launchScript += "launcher " + getLauncher() + "\n"; + // qDebug() << "Generated launch script:" << launchScript; return launchScript; } @@ -790,6 +806,8 @@ QStringList MinecraftInstance::verboseDescription(AuthSessionPtr session, Minecr out << "Window size: " + QString::number(width) + " x " + QString::number(height); } out << ""; + out << "Launcher: " + getLauncher(); + out << ""; return out; } @@ -1124,8 +1142,6 @@ std::shared_ptr MinecraftInstance::resourcePackList() c if (!m_resource_pack_list) { m_resource_pack_list.reset(new ResourcePackFolderModel(resourcePacksDir())); - m_resource_pack_list->enableInteraction(!isRunning()); - connect(this, &BaseInstance::runningStatusChanged, m_resource_pack_list.get(), &ResourcePackFolderModel::disableInteraction); } return m_resource_pack_list; } @@ -1135,8 +1151,6 @@ std::shared_ptr MinecraftInstance::texturePackList() con if (!m_texture_pack_list) { m_texture_pack_list.reset(new TexturePackFolderModel(texturePacksDir())); - m_texture_pack_list->disableInteraction(isRunning()); - connect(this, &BaseInstance::runningStatusChanged, m_texture_pack_list.get(), &ModFolderModel::disableInteraction); } return m_texture_pack_list; } @@ -1146,8 +1160,6 @@ std::shared_ptr MinecraftInstance::shaderPackList() const if (!m_shader_pack_list) { m_shader_pack_list.reset(new ShaderPackFolderModel(shaderPacksDir())); - m_shader_pack_list->disableInteraction(isRunning()); - connect(this, &BaseInstance::runningStatusChanged, m_shader_pack_list.get(), &ModFolderModel::disableInteraction); } return m_shader_pack_list; } diff --git a/launcher/minecraft/MinecraftInstance.h b/launcher/minecraft/MinecraftInstance.h index e4f68508f..a92510667 100644 --- a/launcher/minecraft/MinecraftInstance.h +++ b/launcher/minecraft/MinecraftInstance.h @@ -1,7 +1,8 @@ // SPDX-License-Identifier: GPL-3.0-only /* - * PolyMC - Minecraft Launcher + * Prism Launcher - Minecraft Launcher * 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 @@ -131,6 +132,7 @@ public: QString createLaunchScript(AuthSessionPtr session, MinecraftServerTargetPtr serverToJoin); /// get arguments passed to java QStringList javaArguments(); + QString getLauncher(); /// get variables for launch command variable substitution/environment QMap getVariables() override; diff --git a/launcher/minecraft/MojangVersionFormat.cpp b/launcher/minecraft/MojangVersionFormat.cpp index 9bbb4adab..623dcdfa6 100644 --- a/launcher/minecraft/MojangVersionFormat.cpp +++ b/launcher/minecraft/MojangVersionFormat.cpp @@ -135,7 +135,7 @@ QJsonObject libDownloadInfoToJson(MojangLibraryDownloadInfo::Ptr libinfo) { out.insert("artifact", downloadInfoToJson(libinfo->artifact)); } - if(libinfo->classifiers.size()) + if(!libinfo->classifiers.isEmpty()) { QJsonObject classifiersOut; for(auto iter = libinfo->classifiers.begin(); iter != libinfo->classifiers.end(); iter++) @@ -297,7 +297,7 @@ void MojangVersionFormat::writeVersionProperties(const VersionFile* in, QJsonObj { out.insert("assetIndex", assetIndexToJson(in->mojangAssetIndex)); } - if(in->mojangDownloads.size()) + if(!in->mojangDownloads.isEmpty()) { QJsonObject downloadsOut; for(auto iter = in->mojangDownloads.begin(); iter != in->mojangDownloads.end(); iter++) @@ -306,6 +306,15 @@ void MojangVersionFormat::writeVersionProperties(const VersionFile* in, QJsonObj } out.insert("downloads", downloadsOut); } + if(!in->compatibleJavaMajors.isEmpty()) + { + QJsonArray compatibleJavaMajorsOut; + for(auto compatibleJavaMajor : in->compatibleJavaMajors) + { + compatibleJavaMajorsOut.append(compatibleJavaMajor); + } + out.insert("compatibleJavaMajors", compatibleJavaMajorsOut); + } } QJsonDocument MojangVersionFormat::versionFileToJson(const VersionFilePtr &patch) @@ -396,7 +405,7 @@ QJsonObject MojangVersionFormat::libraryToJson(Library *library) iter++; } libRoot.insert("natives", nativeList); - if (library->m_extractExcludes.size()) + if (!library->m_extractExcludes.isEmpty()) { QJsonArray excludes; QJsonObject extract; @@ -408,7 +417,7 @@ QJsonObject MojangVersionFormat::libraryToJson(Library *library) libRoot.insert("extract", extract); } } - if (library->m_rules.size()) + if (!library->m_rules.isEmpty()) { QJsonArray allRules; for (auto &rule : library->m_rules) diff --git a/launcher/minecraft/OneSixVersionFormat.cpp b/launcher/minecraft/OneSixVersionFormat.cpp index cec4a55bb..280f6b269 100644 --- a/launcher/minecraft/OneSixVersionFormat.cpp +++ b/launcher/minecraft/OneSixVersionFormat.cpp @@ -63,13 +63,13 @@ LibraryPtr OneSixVersionFormat::libraryFromJson(ProblemContainer & problems, con QJsonObject OneSixVersionFormat::libraryToJson(Library *library) { QJsonObject libRoot = MojangVersionFormat::libraryToJson(library); - if (library->m_absoluteURL.size()) + if (!library->m_absoluteURL.isEmpty()) libRoot.insert("MMC-absoluteUrl", library->m_absoluteURL); - if (library->m_hint.size()) + if (!library->m_hint.isEmpty()) libRoot.insert("MMC-hint", library->m_hint); - if (library->m_filename.size()) + if (!library->m_filename.isEmpty()) libRoot.insert("MMC-filename", library->m_filename); - if (library->m_displayname.size()) + if (!library->m_displayname.isEmpty()) libRoot.insert("MMC-displayname", library->m_displayname); return libRoot; } @@ -225,11 +225,10 @@ VersionFilePtr OneSixVersionFormat::versionFileFromJson(const QJsonDocument &doc { QJsonObject agentObj = requireObject(agentVal); auto lib = libraryFromJson(*out, agentObj, filename); + QString arg = ""; - if (agentObj.contains("argument")) - { - readString(agentObj, "argument", arg); - } + readString(agentObj, "argument", arg); + AgentPtr agent(new Agent(lib, arg)); out->agents.append(agent); } @@ -332,6 +331,20 @@ QJsonDocument OneSixVersionFormat::versionFileToJson(const VersionFilePtr &patch writeString(root, "appletClass", patch->appletClass); writeStringList(root, "+tweakers", patch->addTweakers); writeStringList(root, "+traits", patch->traits.values()); + writeStringList(root, "+jvmArgs", patch->addnJvmArguments); + if (!patch->agents.isEmpty()) + { + QJsonArray array; + for (auto value: patch->agents) + { + QJsonObject agentOut = OneSixVersionFormat::libraryToJson(value->library().get()); + if (!value->argument().isEmpty()) + agentOut.insert("argument", value->argument()); + + array.append(agentOut); + } + root.insert("+agents", array); + } if (!patch->libraries.isEmpty()) { QJsonArray array; diff --git a/launcher/minecraft/PackProfile.cpp b/launcher/minecraft/PackProfile.cpp index 1618458f5..43fa3f8d6 100644 --- a/launcher/minecraft/PackProfile.cpp +++ b/launcher/minecraft/PackProfile.cpp @@ -1,7 +1,8 @@ // SPDX-License-Identifier: GPL-3.0-only /* - * PolyMC - Minecraft Launcher + * Prism Launcher - Minecraft Launcher * 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 @@ -47,7 +48,6 @@ #include "Exception.h" #include "minecraft/OneSixVersionFormat.h" #include "FileSystem.h" -#include "meta/Index.h" #include "minecraft/MinecraftInstance.h" #include "Json.h" @@ -55,7 +55,6 @@ #include "PackProfile_p.h" #include "ComponentUpdateTask.h" -#include "Application.h" #include "modplatform/ModAPI.h" static const QMap modloaderMapping{ @@ -613,7 +612,7 @@ QVariant PackProfile::data(const QModelIndex &index, int role) const bool PackProfile::setData(const QModelIndex& index, const QVariant& value, int role) { - if (!index.isValid() || index.row() < 0 || index.row() >= rowCount(index)) + if (!index.isValid() || index.row() < 0 || index.row() >= rowCount(index.parent())) { return false; } @@ -675,12 +674,12 @@ Qt::ItemFlags PackProfile::flags(const QModelIndex &index) const int PackProfile::rowCount(const QModelIndex &parent) const { - return d->components.size(); + return parent.isValid() ? 0 : d->components.size(); } int PackProfile::columnCount(const QModelIndex &parent) const { - return NUM_COLUMNS; + return parent.isValid() ? 0 : NUM_COLUMNS; } void PackProfile::move(const int index, const MoveDirection direction) @@ -738,6 +737,11 @@ void PackProfile::installCustomJar(QString selectedFile) installCustomJar_internal(selectedFile); } +void PackProfile::installAgents(QStringList selectedFiles) +{ + installAgents_internal(selectedFiles); +} + bool PackProfile::installEmpty(const QString& uid, const QString& name) { QString patchDir = FS::PathCombine(d->m_instance->instanceRoot(), "patches"); @@ -832,18 +836,14 @@ bool PackProfile::installJarMods_internal(QStringList filepaths) for(auto filepath:filepaths) { QFileInfo sourceInfo(filepath); - auto uuid = QUuid::createUuid(); - QString id = uuid.toString().remove('{').remove('}'); + QString id = QUuid::createUuid().toString(QUuid::WithoutBraces); QString target_filename = id + ".jar"; - QString target_id = "org.multimc.jarmod." + id; + QString target_id = "custom.jarmod." + id; QString target_name = sourceInfo.completeBaseName() + " (jar mod)"; QString finalPath = FS::PathCombine(d->m_instance->jarModsDir(), target_filename); QFileInfo targetInfo(finalPath); - if(targetInfo.exists()) - { - return false; - } + Q_ASSERT(!targetInfo.exists()); if (!QFile::copy(sourceInfo.absoluteFilePath(),QFileInfo(finalPath).absoluteFilePath())) { @@ -852,7 +852,7 @@ bool PackProfile::installJarMods_internal(QStringList filepaths) auto f = std::make_shared(); auto jarMod = std::make_shared(); - jarMod->setRawName(GradleSpecifier("org.multimc.jarmods:" + id + ":1")); + jarMod->setRawName(GradleSpecifier("custom.jarmods:" + id + ":1")); jarMod->setFilename(target_filename); jarMod->setDisplayName(sourceInfo.completeBaseName()); jarMod->setHint("local"); @@ -892,7 +892,7 @@ bool PackProfile::installCustomJar_internal(QString filepath) return false; } - auto specifier = GradleSpecifier("org.multimc:customjar:1"); + auto specifier = GradleSpecifier("custom:customjar:1"); QFileInfo sourceInfo(filepath); QString target_filename = specifier.getFileName(); QString target_id = specifier.artifactId(); @@ -939,6 +939,64 @@ bool PackProfile::installCustomJar_internal(QString filepath) return true; } +bool PackProfile::installAgents_internal(QStringList filepaths) +{ + // FIXME code duplication + const QString patchDir = FS::PathCombine(d->m_instance->instanceRoot(), "patches"); + if (!FS::ensureFolderPathExists(patchDir)) + return false; + + const QString libDir = d->m_instance->getLocalLibraryPath(); + if (!FS::ensureFolderPathExists(libDir)) + return false; + + for (const QString& source : filepaths) { + const QFileInfo sourceInfo(source); + const QString id = QUuid::createUuid().toString(QUuid::WithoutBraces); + const QString targetBaseName = id + ".jar"; + const QString targetId = "custom.agent." + id; + const QString targetName = sourceInfo.completeBaseName() + " (agent)"; + const QString target = FS::PathCombine(d->m_instance->getLocalLibraryPath(), targetBaseName); + + const QFileInfo targetInfo(target); + Q_ASSERT(!targetInfo.exists()); + + if (!QFile::copy(source, target)) + return false; + + auto versionFile = std::make_shared(); + + auto agent = std::make_shared(); + + agent->setRawName("custom.agents:" + id + ":1"); + agent->setFilename(targetBaseName); + agent->setDisplayName(sourceInfo.completeBaseName()); + agent->setHint("local"); + + versionFile->agents.append(std::make_shared(agent, QString())); + + versionFile->name = targetName; + versionFile->uid = targetId; + + QFile patchFile(FS::PathCombine(patchDir, targetId + ".json")); + + if (!patchFile.open(QFile::WriteOnly)) { + qCritical() << "Error opening" << patchFile.fileName() << "for reading:" << patchFile.errorString(); + return false; + } + + patchFile.write(OneSixVersionFormat::versionFileToJson(versionFile).toJson()); + patchFile.close(); + + appendComponent(new Component(this, versionFile->uid, versionFile)); + } + + scheduleSave(); + invalidateLaunchProfile(); + + return true; +} + std::shared_ptr PackProfile::getProfile() const { if(!d->m_profile) diff --git a/launcher/minecraft/PackProfile.h b/launcher/minecraft/PackProfile.h index 807511a26..2330cca1c 100644 --- a/launcher/minecraft/PackProfile.h +++ b/launcher/minecraft/PackProfile.h @@ -1,7 +1,8 @@ // SPDX-License-Identifier: GPL-3.0-only /* - * PolyMC - Minecraft Launcher + * Prism Launcher - Minecraft Launcher * 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 @@ -85,6 +86,9 @@ public: /// install a jar/zip as a replacement for the main jar void installCustomJar(QString selectedFile); + /// install Java agent files + void installAgents(QStringList selectedFiles); + enum MoveDirection { MoveUp, MoveDown }; /// move component file # up or down the list void move(const int index, const MoveDirection direction); @@ -167,6 +171,7 @@ private: bool load(); bool installJarMods_internal(QStringList filepaths); bool installCustomJar_internal(QString filepath); + bool installAgents_internal(QStringList filepaths); bool removeComponent_internal(ComponentPtr patch); private: /* data */ diff --git a/launcher/minecraft/Rule.h b/launcher/minecraft/Rule.h index 236f9a878..846e8e428 100644 --- a/launcher/minecraft/Rule.h +++ b/launcher/minecraft/Rule.h @@ -104,7 +104,7 @@ public: class ImplicitRule : public Rule { protected: - virtual bool applies(const Library *, const RuntimeContext & runtimeContext) + virtual bool applies(const Library *, [[maybe_unused]] const RuntimeContext & runtimeContext) { return true; } diff --git a/launcher/minecraft/WorldList.cpp b/launcher/minecraft/WorldList.cpp index aee7be358..ae29a972f 100644 --- a/launcher/minecraft/WorldList.cpp +++ b/launcher/minecraft/WorldList.cpp @@ -173,7 +173,7 @@ bool WorldList::resetIcon(int row) int WorldList::columnCount(const QModelIndex &parent) const { - return 4; + return parent.isValid()? 0 : 4; } QVariant WorldList::data(const QModelIndex &index, int role) const @@ -398,8 +398,8 @@ void WorldList::installWorld(QFileInfo filename) w.install(m_dir.absolutePath()); } -bool WorldList::dropMimeData(const QMimeData *data, Qt::DropAction action, int row, int column, - const QModelIndex &parent) +bool WorldList::dropMimeData(const QMimeData *data, Qt::DropAction action, [[maybe_unused]] int row, [[maybe_unused]] int column, + [[maybe_unused]] const QModelIndex &parent) { if (action == Qt::IgnoreAction) return true; diff --git a/launcher/minecraft/WorldList.h b/launcher/minecraft/WorldList.h index 5138e5837..082947556 100644 --- a/launcher/minecraft/WorldList.h +++ b/launcher/minecraft/WorldList.h @@ -54,7 +54,7 @@ public: virtual int rowCount(const QModelIndex &parent = QModelIndex()) const { - return size(); + return parent.isValid() ? 0 : static_cast(size()); }; virtual QVariant headerData(int section, Qt::Orientation orientation, int role = Qt::DisplayRole) const; diff --git a/launcher/minecraft/auth/AccountList.cpp b/launcher/minecraft/auth/AccountList.cpp index 4f7f0a128..d90e3513e 100644 --- a/launcher/minecraft/auth/AccountList.cpp +++ b/launcher/minecraft/auth/AccountList.cpp @@ -408,20 +408,20 @@ QVariant AccountList::headerData(int section, Qt::Orientation orientation, int r } } -int AccountList::rowCount(const QModelIndex &) const +int AccountList::rowCount(const QModelIndex &parent) const { // Return count - return count(); + return parent.isValid() ? 0 : count(); } -int AccountList::columnCount(const QModelIndex &) const +int AccountList::columnCount(const QModelIndex &parent) const { - return NUM_COLUMNS; + return parent.isValid() ? 0 : NUM_COLUMNS; } Qt::ItemFlags AccountList::flags(const QModelIndex &index) const { - if (index.row() < 0 || index.row() >= rowCount(index) || !index.isValid()) + if (index.row() < 0 || index.row() >= rowCount(index.parent()) || !index.isValid()) { return Qt::NoItemFlags; } diff --git a/launcher/minecraft/mod/ModFolderModel.cpp b/launcher/minecraft/mod/ModFolderModel.cpp index 66e80f4a5..4ccc5d4d5 100644 --- a/launcher/minecraft/mod/ModFolderModel.cpp +++ b/launcher/minecraft/mod/ModFolderModel.cpp @@ -144,7 +144,7 @@ QVariant ModFolderModel::headerData(int section, Qt::Orientation orientation, in int ModFolderModel::columnCount(const QModelIndex &parent) const { - return NUM_COLUMNS; + return parent.isValid() ? 0 : NUM_COLUMNS; } Task* ModFolderModel::createUpdateTask() diff --git a/launcher/minecraft/mod/ResourceFolderModel.cpp b/launcher/minecraft/mod/ResourceFolderModel.cpp index b23563091..a52c5db34 100644 --- a/launcher/minecraft/mod/ResourceFolderModel.cpp +++ b/launcher/minecraft/mod/ResourceFolderModel.cpp @@ -20,6 +20,7 @@ ResourceFolderModel::ResourceFolderModel(QDir dir, QObject* parent) : QAbstractL m_dir.setSorting(QDir::Name | QDir::IgnoreCase | QDir::LocaleAware); connect(&m_watcher, &QFileSystemWatcher::directoryChanged, this, &ResourceFolderModel::directoryChanged); + connect(&m_helper_thread_task, &ConcurrentTask::finished, this, [this]{ m_helper_thread_task.clear(); }); } ResourceFolderModel::~ResourceFolderModel() @@ -275,7 +276,11 @@ void ResourceFolderModel::resolveResource(Resource* res) connect( task, &Task::finished, this, [=] { m_active_parse_tasks.remove(ticket); }, Qt::ConnectionType::QueuedConnection); - QThreadPool::globalInstance()->start(task); + m_helper_thread_task.addTask(task); + + if (!m_helper_thread_task.isRunning()) { + QThreadPool::globalInstance()->start(&m_helper_thread_task); + } } void ResourceFolderModel::onUpdateSucceeded() @@ -426,7 +431,7 @@ QVariant ResourceFolderModel::data(const QModelIndex& index, int role) const bool ResourceFolderModel::setData(const QModelIndex& index, const QVariant& value, int role) { int row = index.row(); - if (row < 0 || row >= rowCount(index) || !index.isValid()) + if (row < 0 || row >= rowCount(index.parent()) || !index.isValid()) return false; if (role == Qt::CheckStateRole) diff --git a/launcher/minecraft/mod/ResourceFolderModel.h b/launcher/minecraft/mod/ResourceFolderModel.h index 25095a456..f1bc2dd78 100644 --- a/launcher/minecraft/mod/ResourceFolderModel.h +++ b/launcher/minecraft/mod/ResourceFolderModel.h @@ -10,6 +10,7 @@ #include "Resource.h" #include "tasks/Task.h" +#include "tasks/ConcurrentTask.h" class QSortFilterProxyModel; @@ -90,8 +91,8 @@ class ResourceFolderModel : public QAbstractListModel { /* Basic columns */ enum Columns { ACTIVE_COLUMN = 0, NAME_COLUMN, DATE_COLUMN, NUM_COLUMNS }; - [[nodiscard]] int rowCount(const QModelIndex& = {}) const override { return size(); } - [[nodiscard]] int columnCount(const QModelIndex& = {}) const override { return NUM_COLUMNS; }; + [[nodiscard]] int rowCount(const QModelIndex& parent = {}) const override { return parent.isValid() ? 0 : static_cast(size()); } + [[nodiscard]] int columnCount(const QModelIndex& parent = {}) const override { return parent.isValid() ? 0 : NUM_COLUMNS; }; [[nodiscard]] Qt::DropActions supportedDropActions() const override; @@ -176,7 +177,7 @@ class ResourceFolderModel : public QAbstractListModel { * if the resource is complex and has more stuff to parse. */ virtual void onParseSucceeded(int ticket, QString resource_id); - virtual void onParseFailed(int ticket, QString resource_id) {} + virtual void onParseFailed(int ticket, QString resource_id) { Q_UNUSED(ticket); Q_UNUSED(resource_id); } protected: // Represents the relationship between a column's index (represented by the list index), and it's sorting key. @@ -197,6 +198,7 @@ class ResourceFolderModel : public QAbstractListModel { // Represents the relationship between a resource's internal ID and it's row position on the model. QMap m_resources_index; + ConcurrentTask m_helper_thread_task; QMap m_active_parse_tasks; std::atomic m_next_resolution_ticket = 0; }; diff --git a/launcher/minecraft/mod/ResourcePack.cpp b/launcher/minecraft/mod/ResourcePack.cpp index 3fc10a2fd..3a2fd771b 100644 --- a/launcher/minecraft/mod/ResourcePack.cpp +++ b/launcher/minecraft/mod/ResourcePack.cpp @@ -1,9 +1,11 @@ #include "ResourcePack.h" +#include #include #include #include +#include "MTPixmapCache.h" #include "Version.h" #include "minecraft/mod/tasks/LocalResourcePackParseTask.h" @@ -15,7 +17,7 @@ static const QMap> s_pack_format_versions = { { 3, { Version("1.11"), Version("1.12.2") } }, { 4, { Version("1.13"), Version("1.14.4") } }, { 5, { Version("1.15"), Version("1.16.1") } }, { 6, { Version("1.16.2"), Version("1.16.5") } }, { 7, { Version("1.17"), Version("1.17.1") } }, { 8, { Version("1.18"), Version("1.18.2") } }, - { 9, { Version("1.19"), Version("1.19.2") } }, + { 9, { Version("1.19"), Version("1.19.2") } }, { 11, { Version("1.19.3"), Version("1.19.3") } }, }; void ResourcePack::setPackFormat(int new_format_id) @@ -43,16 +45,22 @@ void ResourcePack::setImage(QImage new_image) Q_ASSERT(!new_image.isNull()); if (m_pack_image_cache_key.key.isValid()) - QPixmapCache::remove(m_pack_image_cache_key.key); + PixmapCache::instance().remove(m_pack_image_cache_key.key); - m_pack_image_cache_key.key = QPixmapCache::insert(QPixmap::fromImage(new_image)); + m_pack_image_cache_key.key = PixmapCache::instance().insert(QPixmap::fromImage(new_image)); m_pack_image_cache_key.was_ever_used = true; + + // This can happen if the pixmap is too big to fit in the cache :c + if (!m_pack_image_cache_key.key.isValid()) { + qWarning() << "Could not insert a image cache entry! Ignoring it."; + m_pack_image_cache_key.was_ever_used = false; + } } QPixmap ResourcePack::image(QSize size) { QPixmap cached_image; - if (QPixmapCache::find(m_pack_image_cache_key.key, &cached_image)) { + if (PixmapCache::instance().find(m_pack_image_cache_key.key, &cached_image)) { if (size.isNull()) return cached_image; return cached_image.scaled(size); @@ -114,3 +122,8 @@ bool ResourcePack::applyFilter(QRegularExpression filter) const return Resource::applyFilter(filter); } + +bool ResourcePack::valid() const +{ + return m_pack_format != 0; +} diff --git a/launcher/minecraft/mod/ResourcePack.h b/launcher/minecraft/mod/ResourcePack.h index 031219081..7cb414d83 100644 --- a/launcher/minecraft/mod/ResourcePack.h +++ b/launcher/minecraft/mod/ResourcePack.h @@ -42,6 +42,8 @@ class ResourcePack : public Resource { /** Thread-safe. */ void setImage(QImage new_image); + bool valid() const override; + [[nodiscard]] auto compare(Resource const& other, SortType type) const -> std::pair override; [[nodiscard]] bool applyFilter(QRegularExpression filter) const override; diff --git a/launcher/minecraft/mod/ResourcePackFolderModel.cpp b/launcher/minecraft/mod/ResourcePackFolderModel.cpp index f8a6c1cfb..ebac707da 100644 --- a/launcher/minecraft/mod/ResourcePackFolderModel.cpp +++ b/launcher/minecraft/mod/ResourcePackFolderModel.cpp @@ -137,7 +137,7 @@ QVariant ResourcePackFolderModel::headerData(int section, Qt::Orientation orient int ResourcePackFolderModel::columnCount(const QModelIndex& parent) const { - return NUM_COLUMNS; + return parent.isValid() ? 0 : NUM_COLUMNS; } Task* ResourcePackFolderModel::createUpdateTask() diff --git a/launcher/minecraft/mod/TexturePack.cpp b/launcher/minecraft/mod/TexturePack.cpp index 796eb69d0..99d555843 100644 --- a/launcher/minecraft/mod/TexturePack.cpp +++ b/launcher/minecraft/mod/TexturePack.cpp @@ -62,3 +62,8 @@ QPixmap TexturePack::image(QSize size) TexturePackUtils::process(*this); return image(size); } + +bool TexturePack::valid() const +{ + return m_description != nullptr; +} diff --git a/launcher/minecraft/mod/TexturePack.h b/launcher/minecraft/mod/TexturePack.h index 6aa5e18ef..81bd5c699 100644 --- a/launcher/minecraft/mod/TexturePack.h +++ b/launcher/minecraft/mod/TexturePack.h @@ -48,6 +48,8 @@ class TexturePack : public Resource { /** Thread-safe. */ void setImage(QImage new_image); + bool valid() const override; + protected: mutable QMutex m_data_lock; diff --git a/launcher/minecraft/mod/tasks/LocalModParseTask.cpp b/launcher/minecraft/mod/tasks/LocalModParseTask.cpp index a694e7b22..774f61145 100644 --- a/launcher/minecraft/mod/tasks/LocalModParseTask.cpp +++ b/launcher/minecraft/mod/tasks/LocalModParseTask.cpp @@ -121,7 +121,7 @@ ModDetails ReadMCModTOML(QByteArray contents) return {}; } auto modsTable = tomlModsTable0->as_table(); - if (!tomlModsTable0) { + if (!modsTable) { qWarning() << "Corrupted mods.toml? [[mods]] was not a table!"; return {}; } diff --git a/launcher/minecraft/mod/tasks/LocalResourcePackParseTask.cpp b/launcher/minecraft/mod/tasks/LocalResourcePackParseTask.cpp index 4f87bc130..6fd4b0245 100644 --- a/launcher/minecraft/mod/tasks/LocalResourcePackParseTask.cpp +++ b/launcher/minecraft/mod/tasks/LocalResourcePackParseTask.cpp @@ -28,14 +28,14 @@ namespace ResourcePackUtils { -bool process(ResourcePack& pack) +bool process(ResourcePack& pack, ProcessingLevel level) { switch (pack.type()) { case ResourceType::FOLDER: - ResourcePackUtils::processFolder(pack); + ResourcePackUtils::processFolder(pack, level); return true; case ResourceType::ZIPFILE: - ResourcePackUtils::processZIP(pack); + ResourcePackUtils::processZIP(pack, level); return true; default: qWarning() << "Invalid type for resource pack parse task!"; @@ -43,7 +43,7 @@ bool process(ResourcePack& pack) } } -void processFolder(ResourcePack& pack) +void processFolder(ResourcePack& pack, ProcessingLevel level) { Q_ASSERT(pack.type() == ResourceType::FOLDER); @@ -60,6 +60,9 @@ void processFolder(ResourcePack& pack) mcmeta_file.close(); } + if (level == ProcessingLevel::BasicInfoOnly) + return; + QFileInfo image_file_info(FS::PathCombine(pack.fileinfo().filePath(), "pack.png")); if (image_file_info.isFile()) { QFile mcmeta_file(image_file_info.filePath()); @@ -74,7 +77,7 @@ void processFolder(ResourcePack& pack) } } -void processZIP(ResourcePack& pack) +void processZIP(ResourcePack& pack, ProcessingLevel level) { Q_ASSERT(pack.type() == ResourceType::ZIPFILE); @@ -98,6 +101,11 @@ void processZIP(ResourcePack& pack) file.close(); } + if (level == ProcessingLevel::BasicInfoOnly) { + zip.close(); + return; + } + if (zip.setCurrentFile("pack.png")) { if (!file.open(QIODevice::ReadOnly)) { qCritical() << "Failed to open file in zip."; @@ -138,6 +146,13 @@ void processPackPNG(ResourcePack& pack, QByteArray&& raw_data) qWarning() << "Failed to parse pack.png."; } } + +bool validate(QFileInfo file) +{ + ResourcePack rp{ file }; + return ResourcePackUtils::process(rp, ProcessingLevel::BasicInfoOnly) && rp.valid(); +} + } // namespace ResourcePackUtils LocalResourcePackParseTask::LocalResourcePackParseTask(int token, ResourcePack& rp) @@ -152,8 +167,6 @@ bool LocalResourcePackParseTask::abort() void LocalResourcePackParseTask::executeTask() { - Q_ASSERT(m_resource_pack.valid()); - if (!ResourcePackUtils::process(m_resource_pack)) return; diff --git a/launcher/minecraft/mod/tasks/LocalResourcePackParseTask.h b/launcher/minecraft/mod/tasks/LocalResourcePackParseTask.h index d3c254645..69dbd6ad6 100644 --- a/launcher/minecraft/mod/tasks/LocalResourcePackParseTask.h +++ b/launcher/minecraft/mod/tasks/LocalResourcePackParseTask.h @@ -26,13 +26,19 @@ #include "tasks/Task.h" namespace ResourcePackUtils { -bool process(ResourcePack& pack); -void processZIP(ResourcePack& pack); -void processFolder(ResourcePack& pack); +enum class ProcessingLevel { Full, BasicInfoOnly }; + +bool process(ResourcePack& pack, ProcessingLevel level = ProcessingLevel::Full); + +void processZIP(ResourcePack& pack, ProcessingLevel level = ProcessingLevel::Full); +void processFolder(ResourcePack& pack, ProcessingLevel level = ProcessingLevel::Full); void processMCMeta(ResourcePack& pack, QByteArray&& raw_data); void processPackPNG(ResourcePack& pack, QByteArray&& raw_data); + +/** Checks whether a file is valid as a resource pack or not. */ +bool validate(QFileInfo file); } // namespace ResourcePackUtils class LocalResourcePackParseTask : public Task { diff --git a/launcher/minecraft/mod/tasks/LocalTexturePackParseTask.cpp b/launcher/minecraft/mod/tasks/LocalTexturePackParseTask.cpp index bf1e308fd..adb19aca9 100644 --- a/launcher/minecraft/mod/tasks/LocalTexturePackParseTask.cpp +++ b/launcher/minecraft/mod/tasks/LocalTexturePackParseTask.cpp @@ -28,14 +28,14 @@ namespace TexturePackUtils { -bool process(TexturePack& pack) +bool process(TexturePack& pack, ProcessingLevel level) { switch (pack.type()) { case ResourceType::FOLDER: - TexturePackUtils::processFolder(pack); + TexturePackUtils::processFolder(pack, level); return true; case ResourceType::ZIPFILE: - TexturePackUtils::processZIP(pack); + TexturePackUtils::processZIP(pack, level); return true; default: qWarning() << "Invalid type for resource pack parse task!"; @@ -43,7 +43,7 @@ bool process(TexturePack& pack) } } -void processFolder(TexturePack& pack) +void processFolder(TexturePack& pack, ProcessingLevel level) { Q_ASSERT(pack.type() == ResourceType::FOLDER); @@ -60,6 +60,9 @@ void processFolder(TexturePack& pack) mcmeta_file.close(); } + if (level == ProcessingLevel::BasicInfoOnly) + return; + QFileInfo image_file_info(FS::PathCombine(pack.fileinfo().filePath(), "pack.png")); if (image_file_info.isFile()) { QFile mcmeta_file(image_file_info.filePath()); @@ -74,7 +77,7 @@ void processFolder(TexturePack& pack) } } -void processZIP(TexturePack& pack) +void processZIP(TexturePack& pack, ProcessingLevel level) { Q_ASSERT(pack.type() == ResourceType::ZIPFILE); @@ -98,6 +101,11 @@ void processZIP(TexturePack& pack) file.close(); } + if (level == ProcessingLevel::BasicInfoOnly) { + zip.close(); + return; + } + if (zip.setCurrentFile("pack.png")) { if (!file.open(QIODevice::ReadOnly)) { qCritical() << "Failed to open file in zip."; @@ -129,6 +137,13 @@ void processPackPNG(TexturePack& pack, QByteArray&& raw_data) qWarning() << "Failed to parse pack.png."; } } + +bool validate(QFileInfo file) +{ + TexturePack rp{ file }; + return TexturePackUtils::process(rp, ProcessingLevel::BasicInfoOnly) && rp.valid(); +} + } // namespace TexturePackUtils LocalTexturePackParseTask::LocalTexturePackParseTask(int token, TexturePack& rp) @@ -143,8 +158,6 @@ bool LocalTexturePackParseTask::abort() void LocalTexturePackParseTask::executeTask() { - Q_ASSERT(m_texture_pack.valid()); - if (!TexturePackUtils::process(m_texture_pack)) return; diff --git a/launcher/minecraft/mod/tasks/LocalTexturePackParseTask.h b/launcher/minecraft/mod/tasks/LocalTexturePackParseTask.h index cb0e404a7..9f7aab75f 100644 --- a/launcher/minecraft/mod/tasks/LocalTexturePackParseTask.h +++ b/launcher/minecraft/mod/tasks/LocalTexturePackParseTask.h @@ -27,13 +27,19 @@ #include "tasks/Task.h" namespace TexturePackUtils { -bool process(TexturePack& pack); -void processZIP(TexturePack& pack); -void processFolder(TexturePack& pack); +enum class ProcessingLevel { Full, BasicInfoOnly }; + +bool process(TexturePack& pack, ProcessingLevel level = ProcessingLevel::Full); + +void processZIP(TexturePack& pack, ProcessingLevel level = ProcessingLevel::Full); +void processFolder(TexturePack& pack, ProcessingLevel level = ProcessingLevel::Full); void processPackTXT(TexturePack& pack, QByteArray&& raw_data); void processPackPNG(TexturePack& pack, QByteArray&& raw_data); + +/** Checks whether a file is valid as a texture pack or not. */ +bool validate(QFileInfo file); } // namespace TexturePackUtils class LocalTexturePackParseTask : public Task { diff --git a/launcher/minecraft/update/LibrariesTask.cpp b/launcher/minecraft/update/LibrariesTask.cpp index 3b129fe11..33a575c24 100644 --- a/launcher/minecraft/update/LibrariesTask.cpp +++ b/launcher/minecraft/update/LibrariesTask.cpp @@ -12,7 +12,7 @@ LibrariesTask::LibrariesTask(MinecraftInstance * inst) void LibrariesTask::executeTask() { - setStatus(tr("Getting the library files from Mojang...")); + setStatus(tr("Downloading required library files...")); qDebug() << m_inst->name() << ": downloading libraries"; MinecraftInstance *inst = (MinecraftInstance *)m_inst; diff --git a/launcher/modplatform/flame/FileResolvingTask.cpp b/launcher/modplatform/flame/FileResolvingTask.cpp index 25b56fbd1..7f1beb1af 100644 --- a/launcher/modplatform/flame/FileResolvingTask.cpp +++ b/launcher/modplatform/flame/FileResolvingTask.cpp @@ -42,12 +42,25 @@ void Flame::FileResolvingTask::executeTask() void Flame::FileResolvingTask::netJobFinished() { setProgress(1, 3); - int index = 0; // job to check modrinth for blocked projects m_checkJob = new NetJob("Modrinth check", m_network); blockedProjects = QMap(); - auto doc = Json::requireDocument(*result); - auto array = Json::requireArray(doc.object()["data"]); + + QJsonDocument doc; + QJsonArray array; + + try { + doc = Json::requireDocument(*result); + array = Json::requireArray(doc.object()["data"]); + } catch (Json::JsonException& e) { + qCritical() << "Non-JSON data returned from the CF API"; + qCritical() << e.cause(); + + emitFailed(tr("Invalid data returned from the API.")); + + return; + } + for (QJsonValueRef file : array) { auto fileid = Json::requireInteger(Json::requireObject(file)["id"]); auto& out = m_toProcess.files[fileid]; @@ -68,7 +81,6 @@ void Flame::FileResolvingTask::netJobFinished() blockedProjects.insert(&out, output); } } - index++; } connect(m_checkJob.get(), &NetJob::finished, this, &Flame::FileResolvingTask::modrinthCheckFinished); diff --git a/launcher/modplatform/flame/FlameInstanceCreationTask.cpp b/launcher/modplatform/flame/FlameInstanceCreationTask.cpp index f0fbdc965..1d441f092 100644 --- a/launcher/modplatform/flame/FlameInstanceCreationTask.cpp +++ b/launcher/modplatform/flame/FlameInstanceCreationTask.cpp @@ -1,3 +1,38 @@ +// SPDX-License-Identifier: GPL-3.0-only +/* + * Prism Launcher - Minecraft Launcher + * Copyright (C) 2022 Sefa Eyeoglu + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, version 3. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + * + * This file incorporates work covered by the following copyright and + * permission notice: + * + * Copyright 2013-2021 MultiMC Contributors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + #include "FlameInstanceCreationTask.h" #include "modplatform/flame/FlameAPI.h" @@ -46,13 +81,19 @@ bool FlameCreationTask::updateInstance() auto instance_list = APPLICATION->instances(); // FIXME: How to handle situations when there's more than one install already for a given modpack? - auto inst = instance_list->getInstanceByManagedName(originalName()); + InstancePtr inst; + if (auto original_id = originalInstanceID(); !original_id.isEmpty()) { + inst = instance_list->getInstanceById(original_id); + Q_ASSERT(inst); + } else { + inst = instance_list->getInstanceByManagedName(originalName()); - if (!inst) { - inst = instance_list->getInstanceById(originalName()); + if (!inst) { + inst = instance_list->getInstanceById(originalName()); - if (!inst) - return false; + if (!inst) + return false; + } } QString index_path(FS::PathCombine(m_stagingPath, "manifest.json")); @@ -67,24 +108,14 @@ bool FlameCreationTask::updateInstance() auto version_id = inst->getManagedPackVersionName(); auto version_str = !version_id.isEmpty() ? tr(" (version %1)").arg(version_id) : ""; - auto info = CustomMessageBox::selectable( - m_parent, tr("Similar modpack was found!"), - tr("One or more of your instances are from this same modpack%1. Do you want to create a " - "separate instance, or update the existing one?\n\nNOTE: Make sure you made a backup of your important instance data before " - "updating, as worlds can be corrupted and some configuration may be lost (due to pack overrides).") - .arg(version_str), QMessageBox::Information, QMessageBox::Ok | QMessageBox::Reset | QMessageBox::Abort); - info->setButtonText(QMessageBox::Ok, tr("Update existing instance")); - info->setButtonText(QMessageBox::Abort, tr("Create new instance")); - info->setButtonText(QMessageBox::Reset, tr("Cancel")); - - info->exec(); - - if (info->clickedButton() == info->button(QMessageBox::Abort)) - return false; - - if (info->clickedButton() == info->button(QMessageBox::Reset)) { - m_abort = true; - return false; + if (shouldConfirmUpdate()) { + auto should_update = askIfShouldUpdate(m_parent, version_str); + if (should_update == ShouldUpdate::SkipUpdating) + return false; + if (should_update == ShouldUpdate::Cancel) { + m_abort = true; + return false; + } } QDir old_inst_dir(inst->instanceRoot()); @@ -197,10 +228,10 @@ bool FlameCreationTask::updateInstance() m_process_update_file_info_job = nullptr; } else { // We don't have an old index file, so we may duplicate stuff! - auto dialog = CustomMessageBox::selectable(m_parent, - tr("No index file."), - tr("We couldn't find a suitable index file for the older version. This may cause some of the files to be duplicated. Do you want to continue?"), - QMessageBox::Warning, QMessageBox::Ok | QMessageBox::Cancel); + auto dialog = CustomMessageBox::selectable(m_parent, tr("No index file."), + tr("We couldn't find a suitable index file for the older version. This may cause some " + "of the files to be duplicated. Do you want to continue?"), + QMessageBox::Warning, QMessageBox::Ok | QMessageBox::Cancel); if (dialog->exec() == QDialog::DialogCode::Rejected) { m_abort = true; @@ -208,7 +239,7 @@ bool FlameCreationTask::updateInstance() } } - setOverride(true); + setOverride(true, inst->id()); qDebug() << "Will override instance!"; m_instance = inst; @@ -330,7 +361,9 @@ bool FlameCreationTask::createInstance() FS::deletePath(jarmodsPath); } - instance.setManagedPack("flame", {}, m_pack.name, {}, m_pack.version); + // Don't add managed info to packs without an ID (most likely imported from ZIP) + if (!m_managed_id.isEmpty()) + instance.setManagedPack("flame", m_managed_id, m_pack.name, m_managed_version_id, m_pack.version); instance.setName(name()); m_mod_id_resolver = new Flame::FileResolvingTask(APPLICATION->network(), m_pack); @@ -338,6 +371,7 @@ bool FlameCreationTask::createInstance() connect(m_mod_id_resolver.get(), &Flame::FileResolvingTask::failed, [&](QString reason) { m_mod_id_resolver.reset(); setError(tr("Unable to resolve mod IDs:\n") + reason); + loop.quit(); }); connect(m_mod_id_resolver.get(), &Flame::FileResolvingTask::progress, this, &FlameCreationTask::setProgress); connect(m_mod_id_resolver.get(), &Flame::FileResolvingTask::status, this, &FlameCreationTask::setStatus); @@ -353,14 +387,6 @@ bool FlameCreationTask::createInstance() setAbortable(false); auto inst = m_instance.value(); - // Only change the name if it didn't use a custom name, so that the previous custom name - // is preserved, but if we're using the original one, we update the version string. - // NOTE: This needs to come before the copyManagedPack call! - if (inst->name().contains(inst->getManagedPackVersionName())) { - if (askForChangingInstanceName(m_parent, inst->name(), instance.name()) == InstanceNameChange::ShouldChange) - inst->setName(instance.name()); - } - inst->copyManagedPack(instance); } @@ -376,13 +402,13 @@ void FlameCreationTask::idResolverSucceeded(QEventLoop& loop) auto anyBlocked = false; for (const auto& result : results.files.values()) { if (!result.resolved || result.url.isEmpty()) { - BlockedMod blocked_mod; blocked_mod.name = result.fileName; blocked_mod.websiteUrl = result.websiteUrl; blocked_mod.hash = result.hash; blocked_mod.matched = false; blocked_mod.localPath = ""; + blocked_mod.targetFolder = result.targetFolder; blocked_mods.append(blocked_mod); @@ -392,13 +418,14 @@ void FlameCreationTask::idResolverSucceeded(QEventLoop& loop) if (anyBlocked) { qWarning() << "Blocked mods found, displaying mod list"; - auto message_dialog = new BlockedModsDialog(m_parent, tr("Blocked mods found"), - tr("The following mods were blocked on third party launchers.
" - "You will need to manually download them and add them to the modpack"), - blocked_mods); - message_dialog->setModal(true); + BlockedModsDialog message_dialog(m_parent, 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."), + blocked_mods); - if (message_dialog->exec()) { + message_dialog.setModal(true); + + if (message_dialog.exec()) { qDebug() << "Post dialog blocked mods list: " << blocked_mods; copyBlockedMods(blocked_mods); setupDownloadJob(loop); @@ -427,7 +454,7 @@ void FlameCreationTask::copyBlockedMods(QList const& blocked_mods) continue; } - auto dest_path = FS::PathCombine(m_stagingPath, "minecraft", "mods", mod.name); + auto dest_path = FS::PathCombine(m_stagingPath, "minecraft", mod.targetFolder, mod.name); setStatus(tr("Copying Blocked Mods (%1 out of %2 are done)").arg(QString::number(i), QString::number(total))); @@ -482,9 +509,7 @@ void FlameCreationTask::setupDownloadJob(QEventLoop& loop) } m_mod_id_resolver.reset(); - connect(m_files_job.get(), &NetJob::succeeded, this, [&]() { - m_files_job.reset(); - }); + connect(m_files_job.get(), &NetJob::succeeded, this, [&]() { m_files_job.reset(); }); connect(m_files_job.get(), &NetJob::failed, [&](QString reason) { m_files_job.reset(); setError(reason); diff --git a/launcher/modplatform/flame/FlameInstanceCreationTask.h b/launcher/modplatform/flame/FlameInstanceCreationTask.h index fbc7d5bf7..3a1c729fc 100644 --- a/launcher/modplatform/flame/FlameInstanceCreationTask.h +++ b/launcher/modplatform/flame/FlameInstanceCreationTask.h @@ -1,3 +1,38 @@ +// SPDX-License-Identifier: GPL-3.0-only +/* + * Prism Launcher - Minecraft Launcher + * Copyright (C) 2022 Sefa Eyeoglu + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, version 3. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + * + * This file incorporates work covered by the following copyright and + * permission notice: + * + * Copyright 2013-2021 MultiMC Contributors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + #pragma once #include "InstanceCreationTask.h" @@ -16,11 +51,21 @@ class FlameCreationTask final : public InstanceCreationTask { Q_OBJECT public: - FlameCreationTask(const QString& staging_path, SettingsObjectPtr global_settings, QWidget* parent) - : InstanceCreationTask(), m_parent(parent) + FlameCreationTask(const QString& staging_path, + SettingsObjectPtr global_settings, + QWidget* parent, + QString id, + QString version_id, + QString original_instance_id = {}) + : InstanceCreationTask() + , m_parent(parent) + , m_managed_id(std::move(id)) + , m_managed_version_id(std::move(version_id)) { setStagingPath(staging_path); setParentSettings(global_settings); + + m_original_instance_id = std::move(original_instance_id); } bool abort() override; @@ -43,5 +88,7 @@ class FlameCreationTask final : public InstanceCreationTask { NetJob* m_process_update_file_info_job = nullptr; NetJob::Ptr m_files_job = nullptr; + QString m_managed_id, m_managed_version_id; + std::optional m_instance; }; diff --git a/launcher/modplatform/modpacksch/FTBPackInstallTask.cpp b/launcher/modplatform/modpacksch/FTBPackInstallTask.cpp index 40aee82b1..2979663df 100644 --- a/launcher/modplatform/modpacksch/FTBPackInstallTask.cpp +++ b/launcher/modplatform/modpacksch/FTBPackInstallTask.cpp @@ -1,6 +1,6 @@ // SPDX-License-Identifier: GPL-3.0-only /* - * PolyMC - Minecraft Launcher + * Prism Launcher - Minecraft Launcher * Copyright (C) 2022 flowln * Copyright (c) 2022 Jamie Mansfield * Copyright (C) 2022 Sefa Eyeoglu @@ -189,13 +189,13 @@ void PackInstallTask::onResolveModsSucceeded() // First check for blocked mods if (!results_file.resolved || results_file.url.isEmpty()) { - BlockedMod blocked_mod; blocked_mod.name = local_file.name; blocked_mod.websiteUrl = results_file.websiteUrl; blocked_mod.hash = results_file.hash; blocked_mod.matched = false; blocked_mod.localPath = ""; + blocked_mod.targetFolder = results_file.targetFolder; m_blocked_mods.append(blocked_mod); @@ -210,19 +210,20 @@ void PackInstallTask::onResolveModsSucceeded() if (anyBlocked) { qDebug() << "Blocked files found, displaying file list"; - auto message_dialog = new BlockedModsDialog(m_parent, tr("Blocked files 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."), - m_blocked_mods); + BlockedModsDialog message_dialog(m_parent, tr("Blocked files 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."), + m_blocked_mods); - if (message_dialog->exec() == QDialog::Accepted) { + message_dialog.setModal(true); + + if (message_dialog.exec() == QDialog::Accepted) { qDebug() << "Post dialog blocked mods list: " << m_blocked_mods; createInstance(); - } - else { + } else { abort(); } - + } else { createInstance(); } @@ -366,7 +367,7 @@ void PackInstallTask::copyBlockedMods() continue; } - auto dest_path = FS::PathCombine(m_stagingPath, ".minecraft", "mods", mod.name); + auto dest_path = FS::PathCombine(m_stagingPath, ".minecraft", mod.targetFolder, mod.name); setStatus(tr("Copying Blocked Mods (%1 out of %2 are done)").arg(QString::number(i), QString::number(total))); diff --git a/launcher/modplatform/modpacksch/FTBPackInstallTask.h b/launcher/modplatform/modpacksch/FTBPackInstallTask.h index 2cd4d7296..97b1eb0b1 100644 --- a/launcher/modplatform/modpacksch/FTBPackInstallTask.h +++ b/launcher/modplatform/modpacksch/FTBPackInstallTask.h @@ -1,6 +1,6 @@ // SPDX-License-Identifier: GPL-3.0-only /* - * PolyMC - Minecraft Launcher + * Prism Launcher - Minecraft Launcher * Copyright (C) 2022 flowln * Copyright (C) 2022 Sefa Eyeoglu * diff --git a/launcher/modplatform/modrinth/ModrinthInstanceCreationTask.cpp b/launcher/modplatform/modrinth/ModrinthInstanceCreationTask.cpp index ddeea224d..c5a27c9dc 100644 --- a/launcher/modplatform/modrinth/ModrinthInstanceCreationTask.cpp +++ b/launcher/modplatform/modrinth/ModrinthInstanceCreationTask.cpp @@ -33,13 +33,19 @@ bool ModrinthCreationTask::updateInstance() auto instance_list = APPLICATION->instances(); // FIXME: How to handle situations when there's more than one install already for a given modpack? - auto inst = instance_list->getInstanceByManagedName(originalName()); + InstancePtr inst; + if (auto original_id = originalInstanceID(); !original_id.isEmpty()) { + inst = instance_list->getInstanceById(original_id); + Q_ASSERT(inst); + } else { + inst = instance_list->getInstanceByManagedName(originalName()); - if (!inst) { - inst = instance_list->getInstanceById(originalName()); + if (!inst) { + inst = instance_list->getInstanceById(originalName()); - if (!inst) - return false; + if (!inst) + return false; + } } QString index_path = FS::PathCombine(m_stagingPath, "modrinth.index.json"); @@ -49,25 +55,14 @@ bool ModrinthCreationTask::updateInstance() auto version_name = inst->getManagedPackVersionName(); auto version_str = !version_name.isEmpty() ? tr(" (version %1)").arg(version_name) : ""; - auto info = CustomMessageBox::selectable( - m_parent, tr("Similar modpack was found!"), - tr("One or more of your instances are from this same modpack%1. Do you want to create a " - "separate instance, or update the existing one?\n\nNOTE: Make sure you made a backup of your important instance data before " - "updating, as worlds can be corrupted and some configuration may be lost (due to pack overrides).") - .arg(version_str), - QMessageBox::Information, QMessageBox::Ok | QMessageBox::Reset | QMessageBox::Abort); - info->setButtonText(QMessageBox::Ok, tr("Create new instance")); - info->setButtonText(QMessageBox::Abort, tr("Update existing instance")); - info->setButtonText(QMessageBox::Reset, tr("Cancel")); - - info->exec(); - - if (info->clickedButton() == info->button(QMessageBox::Ok)) - return false; - - if (info->clickedButton() == info->button(QMessageBox::Reset)) { - m_abort = true; - return false; + if (shouldConfirmUpdate()) { + auto should_update = askIfShouldUpdate(m_parent, version_str); + if (should_update == ShouldUpdate::SkipUpdating) + return false; + if (should_update == ShouldUpdate::Cancel) { + m_abort = true; + return false; + } } // Remove repeated files, we don't need to download them! @@ -149,7 +144,7 @@ bool ModrinthCreationTask::updateInstance() } - setOverride(true); + setOverride(true, inst->id()); qDebug() << "Will override instance!"; m_instance = inst; @@ -207,14 +202,14 @@ bool ModrinthCreationTask::createInstance() auto components = instance.getPackProfile(); components->buildingFromScratch(); - components->setComponentVersion("net.minecraft", minecraftVersion, true); + components->setComponentVersion("net.minecraft", m_minecraft_version, true); - if (!fabricVersion.isEmpty()) - components->setComponentVersion("net.fabricmc.fabric-loader", fabricVersion); - if (!quiltVersion.isEmpty()) - components->setComponentVersion("org.quiltmc.quilt-loader", quiltVersion); - if (!forgeVersion.isEmpty()) - components->setComponentVersion("net.minecraftforge", forgeVersion); + if (!m_fabric_version.isEmpty()) + components->setComponentVersion("net.fabricmc.fabric-loader", m_fabric_version); + if (!m_quilt_version.isEmpty()) + components->setComponentVersion("org.quiltmc.quilt-loader", m_quilt_version); + if (!m_forge_version.isEmpty()) + components->setComponentVersion("net.minecraftforge", m_forge_version); if (m_instIcon != "default") { instance.setIconKey(m_instIcon); @@ -222,7 +217,9 @@ bool ModrinthCreationTask::createInstance() instance.setIconKey("modrinth"); } - instance.setManagedPack("modrinth", getManagedPackID(), m_managed_name, m_managed_version_id, version()); + // Don't add managed info to packs without an ID (most likely imported from ZIP) + if (!m_managed_id.isEmpty()) + instance.setManagedPack("modrinth", m_managed_id, m_managed_name, m_managed_version_id, version()); instance.setName(name()); instance.saveNow(); @@ -282,7 +279,7 @@ bool ModrinthCreationTask::createInstance() return ended_well; } -bool ModrinthCreationTask::parseManifest(const QString& index_path, std::vector& files, bool set_managed_info, bool show_optional_dialog) +bool ModrinthCreationTask::parseManifest(const QString& index_path, std::vector& files, bool set_internal_data, bool show_optional_dialog) { try { auto doc = Json::requireDocument(index_path); @@ -294,8 +291,9 @@ bool ModrinthCreationTask::parseManifest(const QString& index_path, std::vector< throw JSONValidationError("Unknown game: " + game); } - if (set_managed_info) { - m_managed_version_id = Json::ensureString(obj, "versionId", {}, "Managed ID"); + if (set_internal_data) { + if (m_managed_version_id.isEmpty()) + m_managed_version_id = Json::ensureString(obj, "versionId", {}, "Managed ID"); m_managed_name = Json::ensureString(obj, "name", {}, "Managed Name"); } @@ -369,19 +367,21 @@ bool ModrinthCreationTask::parseManifest(const QString& index_path, std::vector< files.push_back(file); } - auto dependencies = Json::requireObject(obj, "dependencies", "modrinth.index.json"); - for (auto it = dependencies.begin(), end = dependencies.end(); it != end; ++it) { - QString name = it.key(); - if (name == "minecraft") { - minecraftVersion = Json::requireString(*it, "Minecraft version"); - } else if (name == "fabric-loader") { - fabricVersion = Json::requireString(*it, "Fabric Loader version"); - } else if (name == "quilt-loader") { - quiltVersion = Json::requireString(*it, "Quilt Loader version"); - } else if (name == "forge") { - forgeVersion = Json::requireString(*it, "Forge version"); - } else { - throw JSONValidationError("Unknown dependency type: " + name); + if (set_internal_data) { + auto dependencies = Json::requireObject(obj, "dependencies", "modrinth.index.json"); + for (auto it = dependencies.begin(), end = dependencies.end(); it != end; ++it) { + QString name = it.key(); + if (name == "minecraft") { + m_minecraft_version = Json::requireString(*it, "Minecraft version"); + } else if (name == "fabric-loader") { + m_fabric_version = Json::requireString(*it, "Fabric Loader version"); + } else if (name == "quilt-loader") { + m_quilt_version = Json::requireString(*it, "Quilt Loader version"); + } else if (name == "forge") { + m_forge_version = Json::requireString(*it, "Forge version"); + } else { + throw JSONValidationError("Unknown dependency type: " + name); + } } } } else { @@ -395,13 +395,3 @@ bool ModrinthCreationTask::parseManifest(const QString& index_path, std::vector< return true; } - -QString ModrinthCreationTask::getManagedPackID() const -{ - if (!m_source_url.isEmpty()) { - QRegularExpression regex(R"(data\/(.*)\/versions)"); - return regex.match(m_source_url).captured(1); - } - - return {}; -} diff --git a/launcher/modplatform/modrinth/ModrinthInstanceCreationTask.h b/launcher/modplatform/modrinth/ModrinthInstanceCreationTask.h index e459aadf9..6de24fd40 100644 --- a/launcher/modplatform/modrinth/ModrinthInstanceCreationTask.h +++ b/launcher/modplatform/modrinth/ModrinthInstanceCreationTask.h @@ -14,11 +14,21 @@ class ModrinthCreationTask final : public InstanceCreationTask { Q_OBJECT public: - ModrinthCreationTask(QString staging_path, SettingsObjectPtr global_settings, QWidget* parent, QString source_url = {}) - : InstanceCreationTask(), m_parent(parent), m_source_url(std::move(source_url)) + ModrinthCreationTask(QString staging_path, + SettingsObjectPtr global_settings, + QWidget* parent, + QString id, + QString version_id = {}, + QString original_instance_id = {}) + : InstanceCreationTask() + , m_parent(parent) + , m_managed_id(std::move(id)) + , m_managed_version_id(std::move(version_id)) { setStagingPath(staging_path); setParentSettings(global_settings); + + m_original_instance_id = std::move(original_instance_id); } bool abort() override; @@ -27,15 +37,13 @@ class ModrinthCreationTask final : public InstanceCreationTask { bool createInstance() override; private: - bool parseManifest(const QString&, std::vector&, bool set_managed_info = true, bool show_optional_dialog = true); - QString getManagedPackID() const; + bool parseManifest(const QString&, std::vector&, bool set_internal_data = true, bool show_optional_dialog = true); private: QWidget* m_parent = nullptr; - QString minecraftVersion, fabricVersion, quiltVersion, forgeVersion; + QString m_minecraft_version, m_fabric_version, m_quilt_version, m_forge_version; QString m_managed_id, m_managed_version_id, m_managed_name; - QString m_source_url; std::vector m_files; NetJob::Ptr m_files_job; diff --git a/launcher/modplatform/modrinth/ModrinthPackManifest.cpp b/launcher/modplatform/modrinth/ModrinthPackManifest.cpp index 96f54067a..4dca786f0 100644 --- a/launcher/modplatform/modrinth/ModrinthPackManifest.cpp +++ b/launcher/modplatform/modrinth/ModrinthPackManifest.cpp @@ -128,6 +128,7 @@ auto loadIndexedVersion(QJsonObject &obj) -> ModpackVersion file.name = Json::requireString(obj, "name"); file.version = Json::requireString(obj, "version_number"); + file.changelog = Json::ensureString(obj, "changelog"); file.id = Json::requireString(obj, "id"); file.project_id = Json::requireString(obj, "project_id"); diff --git a/launcher/modplatform/modrinth/ModrinthPackManifest.h b/launcher/modplatform/modrinth/ModrinthPackManifest.h index 035dc62e4..2973dfba2 100644 --- a/launcher/modplatform/modrinth/ModrinthPackManifest.h +++ b/launcher/modplatform/modrinth/ModrinthPackManifest.h @@ -80,6 +80,7 @@ struct ModpackExtra { struct ModpackVersion { QString name; QString version; + QString changelog; QString id; QString project_id; diff --git a/launcher/net/HttpMetaCache.cpp b/launcher/net/HttpMetaCache.cpp index e242dcf45..0d7ca7691 100644 --- a/launcher/net/HttpMetaCache.cpp +++ b/launcher/net/HttpMetaCache.cpp @@ -47,7 +47,7 @@ auto MetaEntry::getFullPath() -> QString { // FIXME: make local? - return FS::PathCombine(basePath, relativePath); + return FS::PathCombine(m_basePath, m_relativePath); } HttpMetaCache::HttpMetaCache(QString path) : QObject(), m_index_file(path) @@ -99,7 +99,7 @@ auto HttpMetaCache::resolveEntry(QString base, QString resource_path, QString ex return staleEntry(base, resource_path); } - if (!expected_etag.isEmpty() && expected_etag != entry->etag) { + if (!expected_etag.isEmpty() && expected_etag != entry->m_etag) { // if the etag doesn't match expected, we disown the entry selected_base.entry_list.remove(resource_path); return staleEntry(base, resource_path); @@ -107,17 +107,17 @@ auto HttpMetaCache::resolveEntry(QString base, QString resource_path, QString ex // if the file changed, check md5sum qint64 file_last_changed = finfo.lastModified().toUTC().toMSecsSinceEpoch(); - if (file_last_changed != entry->local_changed_timestamp) { + if (file_last_changed != entry->m_local_changed_timestamp) { QFile input(real_path); input.open(QIODevice::ReadOnly); QString md5sum = QCryptographicHash::hash(input.readAll(), QCryptographicHash::Md5).toHex().constData(); - if (entry->md5sum != md5sum) { + if (entry->m_md5sum != md5sum) { selected_base.entry_list.remove(resource_path); return staleEntry(base, resource_path); } // md5sums matched... keep entry and save the new state to file - entry->local_changed_timestamp = file_last_changed; + entry->m_local_changed_timestamp = file_last_changed; SaveEventually(); } @@ -130,23 +130,23 @@ auto HttpMetaCache::resolveEntry(QString base, QString resource_path, QString ex } // entry passed all the checks we cared about. - entry->basePath = getBasePath(base); + entry->m_basePath = getBasePath(base); return entry; } auto HttpMetaCache::updateEntry(MetaEntryPtr stale_entry) -> bool { - if (!m_entries.contains(stale_entry->baseId)) { - qCritical() << "Cannot add entry with unknown base: " << stale_entry->baseId.toLocal8Bit(); + if (!m_entries.contains(stale_entry->m_baseId)) { + qCritical() << "Cannot add entry with unknown base: " << stale_entry->m_baseId.toLocal8Bit(); return false; } - if (stale_entry->stale) { + if (stale_entry->m_stale) { qCritical() << "Cannot add stale entry: " << stale_entry->getFullPath().toLocal8Bit(); return false; } - m_entries[stale_entry->baseId].entry_list[stale_entry->relativePath] = stale_entry; + m_entries[stale_entry->m_baseId].entry_list[stale_entry->m_relativePath] = stale_entry; SaveEventually(); return true; @@ -157,7 +157,7 @@ auto HttpMetaCache::evictEntry(MetaEntryPtr entry) -> bool if (!entry) return false; - entry->stale = true; + entry->m_stale = true; SaveEventually(); return true; } @@ -169,7 +169,7 @@ void HttpMetaCache::evictAll() qDebug() << "Evicting base" << base; for (MetaEntryPtr entry : map.entry_list) { if (!evictEntry(entry)) - qWarning() << "Unexpected missing cache entry" << entry->basePath; + qWarning() << "Unexpected missing cache entry" << entry->m_basePath; } } } @@ -177,10 +177,10 @@ void HttpMetaCache::evictAll() auto HttpMetaCache::staleEntry(QString base, QString resource_path) -> MetaEntryPtr { auto foo = new MetaEntry(); - foo->baseId = base; - foo->basePath = getBasePath(base); - foo->relativePath = resource_path; - foo->stale = true; + foo->m_baseId = base; + foo->m_basePath = getBasePath(base); + foo->m_relativePath = resource_path; + foo->m_stale = true; return MetaEntryPtr(foo); } @@ -235,23 +235,23 @@ void HttpMetaCache::Load() auto& entrymap = m_entries[base]; auto foo = new MetaEntry(); - foo->baseId = base; - foo->relativePath = Json::ensureString(element_obj, "path"); - foo->md5sum = Json::ensureString(element_obj, "md5sum"); - foo->etag = Json::ensureString(element_obj, "etag"); - foo->local_changed_timestamp = Json::ensureDouble(element_obj, "last_changed_timestamp"); - foo->remote_changed_timestamp = Json::ensureString(element_obj, "remote_changed_timestamp"); + foo->m_baseId = base; + foo->m_relativePath = Json::ensureString(element_obj, "path"); + foo->m_md5sum = Json::ensureString(element_obj, "md5sum"); + foo->m_etag = Json::ensureString(element_obj, "etag"); + foo->m_local_changed_timestamp = Json::ensureDouble(element_obj, "last_changed_timestamp"); + foo->m_remote_changed_timestamp = Json::ensureString(element_obj, "remote_changed_timestamp"); foo->makeEternal(Json::ensureBoolean(element_obj, (const QString)QStringLiteral("eternal"), false)); if (!foo->isEternal()) { - foo->current_age = Json::ensureDouble(element_obj, "current_age"); - foo->max_age = Json::ensureDouble(element_obj, "max_age"); + foo->m_current_age = Json::ensureDouble(element_obj, "current_age"); + foo->m_max_age = Json::ensureDouble(element_obj, "max_age"); } // presumed innocent until closer examination - foo->stale = false; + foo->m_stale = false; - entrymap.entry_list[foo->relativePath] = MetaEntryPtr(foo); + entrymap.entry_list[foo->m_relativePath] = MetaEntryPtr(foo); } } @@ -276,23 +276,23 @@ void HttpMetaCache::SaveNow() for (auto group : m_entries) { for (auto entry : group.entry_list) { // do not save stale entries. they are dead. - if (entry->stale) { + if (entry->m_stale) { continue; } QJsonObject entryObj; - Json::writeString(entryObj, "base", entry->baseId); - Json::writeString(entryObj, "path", entry->relativePath); - Json::writeString(entryObj, "md5sum", entry->md5sum); - Json::writeString(entryObj, "etag", entry->etag); - entryObj.insert("last_changed_timestamp", QJsonValue(double(entry->local_changed_timestamp))); - if (!entry->remote_changed_timestamp.isEmpty()) - entryObj.insert("remote_changed_timestamp", QJsonValue(entry->remote_changed_timestamp)); + Json::writeString(entryObj, "base", entry->m_baseId); + Json::writeString(entryObj, "path", entry->m_relativePath); + Json::writeString(entryObj, "md5sum", entry->m_md5sum); + Json::writeString(entryObj, "etag", entry->m_etag); + entryObj.insert("last_changed_timestamp", QJsonValue(double(entry->m_local_changed_timestamp))); + if (!entry->m_remote_changed_timestamp.isEmpty()) + entryObj.insert("remote_changed_timestamp", QJsonValue(entry->m_remote_changed_timestamp)); if (entry->isEternal()) { entryObj.insert("eternal", true); } else { - entryObj.insert("current_age", QJsonValue(double(entry->current_age))); - entryObj.insert("max_age", QJsonValue(double(entry->max_age))); + entryObj.insert("current_age", QJsonValue(double(entry->m_current_age))); + entryObj.insert("max_age", QJsonValue(double(entry->m_max_age))); } entriesArr.append(entryObj); } diff --git a/launcher/net/HttpMetaCache.h b/launcher/net/HttpMetaCache.h index 2a07d65a5..37f4b49a2 100644 --- a/launcher/net/HttpMetaCache.h +++ b/launcher/net/HttpMetaCache.h @@ -49,47 +49,47 @@ class MetaEntry { MetaEntry() = default; public: - auto isStale() -> bool { return stale; } - void setStale(bool stale) { this->stale = stale; } + auto isStale() -> bool { return m_stale; } + void setStale(bool stale) { m_stale = stale; } auto getFullPath() -> QString; - auto getRemoteChangedTimestamp() -> QString { return remote_changed_timestamp; } - void setRemoteChangedTimestamp(QString remote_changed_timestamp) { this->remote_changed_timestamp = remote_changed_timestamp; } - void setLocalChangedTimestamp(qint64 timestamp) { local_changed_timestamp = timestamp; } + auto getRemoteChangedTimestamp() -> QString { return m_remote_changed_timestamp; } + void setRemoteChangedTimestamp(QString remote_changed_timestamp) { m_remote_changed_timestamp = remote_changed_timestamp; } + void setLocalChangedTimestamp(qint64 timestamp) { m_local_changed_timestamp = timestamp; } - auto getETag() -> QString { return etag; } - void setETag(QString etag) { this->etag = etag; } + auto getETag() -> QString { return m_etag; } + void setETag(QString etag) { m_etag = etag; } - auto getMD5Sum() -> QString { return md5sum; } - void setMD5Sum(QString md5sum) { this->md5sum = md5sum; } + auto getMD5Sum() -> QString { return m_md5sum; } + void setMD5Sum(QString md5sum) { m_md5sum = md5sum; } /* Whether the entry expires after some time (false) or not (true). */ - void makeEternal(bool eternal) { is_eternal = eternal; } - [[nodiscard]] bool isEternal() const { return is_eternal; } + void makeEternal(bool eternal) { m_is_eternal = eternal; } + [[nodiscard]] bool isEternal() const { return m_is_eternal; } - auto getCurrentAge() -> qint64 { return current_age; } - void setCurrentAge(qint64 age) { current_age = age; } + auto getCurrentAge() -> qint64 { return m_current_age; } + void setCurrentAge(qint64 age) { m_current_age = age; } - auto getMaximumAge() -> qint64 { return max_age; } - void setMaximumAge(qint64 age) { max_age = age; } + auto getMaximumAge() -> qint64 { return m_max_age; } + void setMaximumAge(qint64 age) { m_max_age = age; } - bool isExpired(qint64 offset) { return !is_eternal && (current_age >= max_age - offset); }; + bool isExpired(qint64 offset) { return !m_is_eternal && (m_current_age >= m_max_age - offset); }; protected: - QString baseId; - QString basePath; - QString relativePath; - QString md5sum; - QString etag; + QString m_baseId; + QString m_basePath; + QString m_relativePath; + QString m_md5sum; + QString m_etag; - qint64 local_changed_timestamp = 0; - QString remote_changed_timestamp; // QString for now, RFC 2822 encoded time - qint64 current_age = 0; - qint64 max_age = 0; - bool is_eternal = false; + qint64 m_local_changed_timestamp = 0; + QString m_remote_changed_timestamp; // QString for now, RFC 2822 encoded time + qint64 m_current_age = 0; + qint64 m_max_age = 0; + bool m_is_eternal = false; - bool stale = true; + bool m_stale = true; }; using MetaEntryPtr = std::shared_ptr; diff --git a/launcher/net/NetJob.cpp b/launcher/net/NetJob.cpp index 8ced1b7ef..9b5d4f1be 100644 --- a/launcher/net/NetJob.cpp +++ b/launcher/net/NetJob.cpp @@ -123,7 +123,7 @@ auto NetJob::getFailedFiles() -> QList void NetJob::updateState() { - emit progress(m_done.count(), m_total_size); + emit progress(m_done.count(), totalSize()); setStatus(tr("Executing %1 task(s) (%2 out of %3 are done)") - .arg(QString::number(m_doing.count()), QString::number(m_done.count()), QString::number(m_total_size))); + .arg(QString::number(m_doing.count()), QString::number(m_done.count()), QString::number(totalSize()))); } diff --git a/launcher/pathmatcher/SimplePrefixMatcher.h b/launcher/pathmatcher/SimplePrefixMatcher.h new file mode 100644 index 000000000..fc1f5cede --- /dev/null +++ b/launcher/pathmatcher/SimplePrefixMatcher.h @@ -0,0 +1,25 @@ +// SPDX-FileCopyrightText: 2022 Sefa Eyeoglu +// +// SPDX-License-Identifier: GPL-3.0-only + +#include +#include "IPathMatcher.h" + +class SimplePrefixMatcher : public IPathMatcher { + public: + virtual ~SimplePrefixMatcher(){}; + SimplePrefixMatcher(const QString& prefix) + { + m_prefix = prefix; + m_isPrefix = prefix.endsWith('/'); + } + + virtual bool matches(const QString& string) const override + { + if (m_isPrefix) + return string.startsWith(m_prefix); + return string == m_prefix; + } + QString m_prefix; + bool m_isPrefix = false; +}; diff --git a/launcher/resources/OSX/OSX.qrc b/launcher/resources/OSX/OSX.qrc index 19fe312b6..9d4511d1a 100644 --- a/launcher/resources/OSX/OSX.qrc +++ b/launcher/resources/OSX/OSX.qrc @@ -39,5 +39,6 @@ scalable/export.svg scalable/rename.svg scalable/launch.svg + scalable/shortcut.svg diff --git a/launcher/resources/OSX/scalable/shortcut.svg b/launcher/resources/OSX/scalable/shortcut.svg new file mode 100644 index 000000000..a2b7488e1 --- /dev/null +++ b/launcher/resources/OSX/scalable/shortcut.svg @@ -0,0 +1,14 @@ + + + + + + + + + + + diff --git a/launcher/resources/breeze_dark/breeze_dark.qrc b/launcher/resources/breeze_dark/breeze_dark.qrc new file mode 100644 index 000000000..320ca8171 --- /dev/null +++ b/launcher/resources/breeze_dark/breeze_dark.qrc @@ -0,0 +1,46 @@ + + + index.theme + scalable/about.svg + scalable/accounts.svg + scalable/bug.svg + scalable/centralmods.svg + scalable/checkupdate.svg + scalable/copy.svg + scalable/coremods.svg + scalable/custom-commands.svg + scalable/discord.svg + scalable/externaltools.svg + scalable/help.svg + scalable/instance-settings.svg + scalable/jarmods.svg + scalable/java.svg + scalable/language.svg + scalable/loadermods.svg + scalable/log.svg + scalable/minecraft.svg + scalable/matrix.svg + scalable/new.svg + scalable/news.svg + scalable/notes.svg + scalable/proxy.svg + scalable/reddit-alien.svg + scalable/refresh.svg + scalable/resourcepacks.svg + scalable/shaderpacks.svg + scalable/shortcut.svg + scalable/screenshots.svg + scalable/settings.svg + scalable/status-bad.svg + scalable/status-good.svg + scalable/status-yellow.svg + scalable/viewfolder.svg + scalable/worlds.svg + scalable/delete.svg + scalable/tag.svg + scalable/export.svg + scalable/rename.svg + scalable/launch.svg + scalable/server.svg + + diff --git a/launcher/resources/breeze_dark/index.theme b/launcher/resources/breeze_dark/index.theme new file mode 100644 index 000000000..f9f6f4dc0 --- /dev/null +++ b/launcher/resources/breeze_dark/index.theme @@ -0,0 +1,11 @@ +[Icon Theme] +Name=Breeze Dark +Comment=Breeze Dark Icons +Inherits=multimc +Directories=scalable + +[scalable] +Size=48 +Type=Scalable +MinSize=16 +MaxSize=256 diff --git a/launcher/resources/breeze_dark/scalable/about.svg b/launcher/resources/breeze_dark/scalable/about.svg new file mode 100644 index 000000000..856d1b2b8 --- /dev/null +++ b/launcher/resources/breeze_dark/scalable/about.svg @@ -0,0 +1,12 @@ + + + + + + + + diff --git a/launcher/resources/breeze_dark/scalable/accounts.svg b/launcher/resources/breeze_dark/scalable/accounts.svg new file mode 100644 index 000000000..fbb519592 --- /dev/null +++ b/launcher/resources/breeze_dark/scalable/accounts.svg @@ -0,0 +1,17 @@ + + + + + + diff --git a/launcher/resources/breeze_dark/scalable/bug.svg b/launcher/resources/breeze_dark/scalable/bug.svg new file mode 100644 index 000000000..6ddf482f7 --- /dev/null +++ b/launcher/resources/breeze_dark/scalable/bug.svg @@ -0,0 +1,13 @@ + + + + + + diff --git a/launcher/resources/breeze_dark/scalable/centralmods.svg b/launcher/resources/breeze_dark/scalable/centralmods.svg new file mode 100644 index 000000000..4035e51cb --- /dev/null +++ b/launcher/resources/breeze_dark/scalable/centralmods.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/launcher/resources/breeze_dark/scalable/checkupdate.svg b/launcher/resources/breeze_dark/scalable/checkupdate.svg new file mode 100644 index 000000000..cc5dfc163 --- /dev/null +++ b/launcher/resources/breeze_dark/scalable/checkupdate.svg @@ -0,0 +1,14 @@ + + + + + + diff --git a/launcher/resources/breeze_dark/scalable/copy.svg b/launcher/resources/breeze_dark/scalable/copy.svg new file mode 100644 index 000000000..fe4a36acd --- /dev/null +++ b/launcher/resources/breeze_dark/scalable/copy.svg @@ -0,0 +1,11 @@ + + + + + + + diff --git a/launcher/resources/breeze_dark/scalable/coremods.svg b/launcher/resources/breeze_dark/scalable/coremods.svg new file mode 100644 index 000000000..ec4ecea85 --- /dev/null +++ b/launcher/resources/breeze_dark/scalable/coremods.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/launcher/resources/breeze_dark/scalable/custom-commands.svg b/launcher/resources/breeze_dark/scalable/custom-commands.svg new file mode 100644 index 000000000..44efd39ef --- /dev/null +++ b/launcher/resources/breeze_dark/scalable/custom-commands.svg @@ -0,0 +1,13 @@ + + + + + + diff --git a/launcher/resources/breeze_dark/scalable/delete.svg b/launcher/resources/breeze_dark/scalable/delete.svg new file mode 100644 index 000000000..c7074585b --- /dev/null +++ b/launcher/resources/breeze_dark/scalable/delete.svg @@ -0,0 +1,13 @@ + + + + + + diff --git a/launcher/resources/breeze_dark/scalable/discord.svg b/launcher/resources/breeze_dark/scalable/discord.svg new file mode 100644 index 000000000..2e6d88999 --- /dev/null +++ b/launcher/resources/breeze_dark/scalable/discord.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/launcher/resources/breeze_dark/scalable/export.svg b/launcher/resources/breeze_dark/scalable/export.svg new file mode 100644 index 000000000..b1fe39d14 --- /dev/null +++ b/launcher/resources/breeze_dark/scalable/export.svg @@ -0,0 +1,11 @@ + + + + + + + diff --git a/launcher/resources/breeze_dark/scalable/externaltools.svg b/launcher/resources/breeze_dark/scalable/externaltools.svg new file mode 100644 index 000000000..dd19fb90f --- /dev/null +++ b/launcher/resources/breeze_dark/scalable/externaltools.svg @@ -0,0 +1,13 @@ + + + + + + diff --git a/launcher/resources/breeze_dark/scalable/help.svg b/launcher/resources/breeze_dark/scalable/help.svg new file mode 100644 index 000000000..b273a8bcf --- /dev/null +++ b/launcher/resources/breeze_dark/scalable/help.svg @@ -0,0 +1,13 @@ + + + + + + diff --git a/launcher/resources/breeze_dark/scalable/instance-settings.svg b/launcher/resources/breeze_dark/scalable/instance-settings.svg new file mode 100644 index 000000000..c5f0504b6 --- /dev/null +++ b/launcher/resources/breeze_dark/scalable/instance-settings.svg @@ -0,0 +1,13 @@ + + + + + + diff --git a/launcher/resources/breeze_dark/scalable/jarmods.svg b/launcher/resources/breeze_dark/scalable/jarmods.svg new file mode 100644 index 000000000..49a45d36a --- /dev/null +++ b/launcher/resources/breeze_dark/scalable/jarmods.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/launcher/resources/breeze_dark/scalable/java.svg b/launcher/resources/breeze_dark/scalable/java.svg new file mode 100644 index 000000000..7149981cc --- /dev/null +++ b/launcher/resources/breeze_dark/scalable/java.svg @@ -0,0 +1,10 @@ + + + + + + + + + + diff --git a/launcher/resources/breeze_dark/scalable/language.svg b/launcher/resources/breeze_dark/scalable/language.svg new file mode 100644 index 000000000..239cdf94e --- /dev/null +++ b/launcher/resources/breeze_dark/scalable/language.svg @@ -0,0 +1,13 @@ + + + + + + diff --git a/launcher/resources/breeze_dark/scalable/launch.svg b/launcher/resources/breeze_dark/scalable/launch.svg new file mode 100644 index 000000000..25c5fabc0 --- /dev/null +++ b/launcher/resources/breeze_dark/scalable/launch.svg @@ -0,0 +1,8 @@ + + + + diff --git a/launcher/resources/breeze_dark/scalable/launcher.svg b/launcher/resources/breeze_dark/scalable/launcher.svg new file mode 100644 index 000000000..aeee84338 --- /dev/null +++ b/launcher/resources/breeze_dark/scalable/launcher.svg @@ -0,0 +1,57 @@ + + + + Prism Launcher Logo + + + + + + + + + + + + + + + + + + + + + + + Prism Launcher Logo + 19/10/2022 + + + Prism Launcher + + + + + AutiOne, Boba, ely, Fulmine, gon sawa, Pankakes, tobimori, Zeke + + + https://github.com/PrismLauncher/PrismLauncher + + + Prism Launcher + + + + + + + + + + + + + + + diff --git a/launcher/resources/breeze_dark/scalable/loadermods.svg b/launcher/resources/breeze_dark/scalable/loadermods.svg new file mode 100644 index 000000000..7bd871882 --- /dev/null +++ b/launcher/resources/breeze_dark/scalable/loadermods.svg @@ -0,0 +1,13 @@ + + + + + + diff --git a/launcher/resources/breeze_dark/scalable/log.svg b/launcher/resources/breeze_dark/scalable/log.svg new file mode 100644 index 000000000..fcd83c4d8 --- /dev/null +++ b/launcher/resources/breeze_dark/scalable/log.svg @@ -0,0 +1,13 @@ + + + + + + diff --git a/launcher/resources/breeze_dark/scalable/matrix.svg b/launcher/resources/breeze_dark/scalable/matrix.svg new file mode 100644 index 000000000..214f57080 --- /dev/null +++ b/launcher/resources/breeze_dark/scalable/matrix.svg @@ -0,0 +1,9 @@ + + + Matrix (protocol) logo + + + + + + \ No newline at end of file diff --git a/launcher/resources/breeze_dark/scalable/minecraft.svg b/launcher/resources/breeze_dark/scalable/minecraft.svg new file mode 100644 index 000000000..1d8d01675 --- /dev/null +++ b/launcher/resources/breeze_dark/scalable/minecraft.svg @@ -0,0 +1,13 @@ + + + + + + + + + + + diff --git a/launcher/resources/breeze_dark/scalable/new.svg b/launcher/resources/breeze_dark/scalable/new.svg new file mode 100644 index 000000000..316017273 --- /dev/null +++ b/launcher/resources/breeze_dark/scalable/new.svg @@ -0,0 +1,13 @@ + + + + + + diff --git a/launcher/resources/breeze_dark/scalable/news.svg b/launcher/resources/breeze_dark/scalable/news.svg new file mode 100644 index 000000000..a2ff0c8d1 --- /dev/null +++ b/launcher/resources/breeze_dark/scalable/news.svg @@ -0,0 +1,13 @@ + + + + + + diff --git a/launcher/resources/breeze_dark/scalable/notes.svg b/launcher/resources/breeze_dark/scalable/notes.svg new file mode 100644 index 000000000..6452d3c8d --- /dev/null +++ b/launcher/resources/breeze_dark/scalable/notes.svg @@ -0,0 +1,13 @@ + + + + + + diff --git a/launcher/resources/breeze_dark/scalable/patreon.svg b/launcher/resources/breeze_dark/scalable/patreon.svg new file mode 100644 index 000000000..7f98dd132 --- /dev/null +++ b/launcher/resources/breeze_dark/scalable/patreon.svg @@ -0,0 +1,3 @@ + + + diff --git a/launcher/resources/breeze_dark/scalable/proxy.svg b/launcher/resources/breeze_dark/scalable/proxy.svg new file mode 100644 index 000000000..c6efb1716 --- /dev/null +++ b/launcher/resources/breeze_dark/scalable/proxy.svg @@ -0,0 +1,14 @@ + + + + + + diff --git a/launcher/resources/breeze_dark/scalable/reddit-alien.svg b/launcher/resources/breeze_dark/scalable/reddit-alien.svg new file mode 100644 index 000000000..00f82bb34 --- /dev/null +++ b/launcher/resources/breeze_dark/scalable/reddit-alien.svg @@ -0,0 +1,3 @@ + + + diff --git a/launcher/resources/breeze_dark/scalable/refresh.svg b/launcher/resources/breeze_dark/scalable/refresh.svg new file mode 100644 index 000000000..7b4864639 --- /dev/null +++ b/launcher/resources/breeze_dark/scalable/refresh.svg @@ -0,0 +1,8 @@ + + + + + + diff --git a/launcher/resources/breeze_dark/scalable/rename.svg b/launcher/resources/breeze_dark/scalable/rename.svg new file mode 100644 index 000000000..6a844965e --- /dev/null +++ b/launcher/resources/breeze_dark/scalable/rename.svg @@ -0,0 +1,13 @@ + + + + + + diff --git a/launcher/resources/breeze_dark/scalable/resourcepacks.svg b/launcher/resources/breeze_dark/scalable/resourcepacks.svg new file mode 100644 index 000000000..0986c2167 --- /dev/null +++ b/launcher/resources/breeze_dark/scalable/resourcepacks.svg @@ -0,0 +1,11 @@ + + + + + + + diff --git a/launcher/resources/breeze_dark/scalable/screenshots.svg b/launcher/resources/breeze_dark/scalable/screenshots.svg new file mode 100644 index 000000000..a10ed713d --- /dev/null +++ b/launcher/resources/breeze_dark/scalable/screenshots.svg @@ -0,0 +1,13 @@ + + + + + + diff --git a/launcher/resources/breeze_dark/scalable/server.svg b/launcher/resources/breeze_dark/scalable/server.svg new file mode 100644 index 000000000..7d9af3e71 --- /dev/null +++ b/launcher/resources/breeze_dark/scalable/server.svg @@ -0,0 +1,13 @@ + + + + + + diff --git a/launcher/resources/breeze_dark/scalable/settings.svg b/launcher/resources/breeze_dark/scalable/settings.svg new file mode 100644 index 000000000..009d81547 --- /dev/null +++ b/launcher/resources/breeze_dark/scalable/settings.svg @@ -0,0 +1,17 @@ + + + + + + diff --git a/launcher/resources/breeze_dark/scalable/shaderpacks.svg b/launcher/resources/breeze_dark/scalable/shaderpacks.svg new file mode 100644 index 000000000..b2887947a --- /dev/null +++ b/launcher/resources/breeze_dark/scalable/shaderpacks.svg @@ -0,0 +1,13 @@ + + + + + + diff --git a/launcher/resources/breeze_dark/scalable/shortcut.svg b/launcher/resources/breeze_dark/scalable/shortcut.svg new file mode 100644 index 000000000..5559be1df --- /dev/null +++ b/launcher/resources/breeze_dark/scalable/shortcut.svg @@ -0,0 +1,18 @@ + + + + + + + + diff --git a/launcher/resources/breeze_dark/scalable/status-bad.svg b/launcher/resources/breeze_dark/scalable/status-bad.svg new file mode 100644 index 000000000..6fc3137e4 --- /dev/null +++ b/launcher/resources/breeze_dark/scalable/status-bad.svg @@ -0,0 +1,9 @@ + + + + + diff --git a/launcher/resources/breeze_dark/scalable/status-good.svg b/launcher/resources/breeze_dark/scalable/status-good.svg new file mode 100644 index 000000000..eb8bc03be --- /dev/null +++ b/launcher/resources/breeze_dark/scalable/status-good.svg @@ -0,0 +1,10 @@ + + + + + diff --git a/launcher/resources/breeze_dark/scalable/status-yellow.svg b/launcher/resources/breeze_dark/scalable/status-yellow.svg new file mode 100644 index 000000000..1dc4d0f51 --- /dev/null +++ b/launcher/resources/breeze_dark/scalable/status-yellow.svg @@ -0,0 +1,9 @@ + + + + + diff --git a/launcher/resources/breeze_dark/scalable/tag.svg b/launcher/resources/breeze_dark/scalable/tag.svg new file mode 100644 index 000000000..b54b515fc --- /dev/null +++ b/launcher/resources/breeze_dark/scalable/tag.svg @@ -0,0 +1,17 @@ + + + + + + diff --git a/launcher/resources/breeze_dark/scalable/viewfolder.svg b/launcher/resources/breeze_dark/scalable/viewfolder.svg new file mode 100644 index 000000000..0189b9544 --- /dev/null +++ b/launcher/resources/breeze_dark/scalable/viewfolder.svg @@ -0,0 +1,13 @@ + + + + + + diff --git a/launcher/resources/breeze_dark/scalable/worlds.svg b/launcher/resources/breeze_dark/scalable/worlds.svg new file mode 100644 index 000000000..0cff82666 --- /dev/null +++ b/launcher/resources/breeze_dark/scalable/worlds.svg @@ -0,0 +1,16 @@ + + + + + + + + diff --git a/launcher/resources/breeze_light/breeze_light.qrc b/launcher/resources/breeze_light/breeze_light.qrc new file mode 100644 index 000000000..e88cd9a00 --- /dev/null +++ b/launcher/resources/breeze_light/breeze_light.qrc @@ -0,0 +1,46 @@ + + + index.theme + scalable/about.svg + scalable/accounts.svg + scalable/bug.svg + scalable/centralmods.svg + scalable/checkupdate.svg + scalable/copy.svg + scalable/coremods.svg + scalable/custom-commands.svg + scalable/discord.svg + scalable/externaltools.svg + scalable/help.svg + scalable/instance-settings.svg + scalable/jarmods.svg + scalable/java.svg + scalable/language.svg + scalable/loadermods.svg + scalable/log.svg + scalable/minecraft.svg + scalable/matrix.svg + scalable/new.svg + scalable/news.svg + scalable/notes.svg + scalable/proxy.svg + scalable/reddit-alien.svg + scalable/refresh.svg + scalable/resourcepacks.svg + scalable/shaderpacks.svg + scalable/shortcut.svg + scalable/screenshots.svg + scalable/settings.svg + scalable/status-bad.svg + scalable/status-good.svg + scalable/status-yellow.svg + scalable/viewfolder.svg + scalable/worlds.svg + scalable/delete.svg + scalable/tag.svg + scalable/export.svg + scalable/rename.svg + scalable/launch.svg + scalable/server.svg + + diff --git a/launcher/resources/breeze_light/index.theme b/launcher/resources/breeze_light/index.theme new file mode 100644 index 000000000..126d42d73 --- /dev/null +++ b/launcher/resources/breeze_light/index.theme @@ -0,0 +1,11 @@ +[Icon Theme] +Name=Breeze Light +Comment=Breeze Light Icons +Inherits=multimc +Directories=scalable + +[scalable] +Size=48 +Type=Scalable +MinSize=16 +MaxSize=256 diff --git a/launcher/resources/breeze_light/scalable/about.svg b/launcher/resources/breeze_light/scalable/about.svg new file mode 100644 index 000000000..ea1dc02cd --- /dev/null +++ b/launcher/resources/breeze_light/scalable/about.svg @@ -0,0 +1,12 @@ + + + + + + + + diff --git a/launcher/resources/breeze_light/scalable/accounts.svg b/launcher/resources/breeze_light/scalable/accounts.svg new file mode 100644 index 000000000..8a542f369 --- /dev/null +++ b/launcher/resources/breeze_light/scalable/accounts.svg @@ -0,0 +1,17 @@ + + + + + + diff --git a/launcher/resources/breeze_light/scalable/bug.svg b/launcher/resources/breeze_light/scalable/bug.svg new file mode 100644 index 000000000..4f41ad6b8 --- /dev/null +++ b/launcher/resources/breeze_light/scalable/bug.svg @@ -0,0 +1,13 @@ + + + + + + diff --git a/launcher/resources/breeze_light/scalable/centralmods.svg b/launcher/resources/breeze_light/scalable/centralmods.svg new file mode 100644 index 000000000..174206c43 --- /dev/null +++ b/launcher/resources/breeze_light/scalable/centralmods.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/launcher/resources/breeze_light/scalable/checkupdate.svg b/launcher/resources/breeze_light/scalable/checkupdate.svg new file mode 100644 index 000000000..06b318273 --- /dev/null +++ b/launcher/resources/breeze_light/scalable/checkupdate.svg @@ -0,0 +1,14 @@ + + + + + + diff --git a/launcher/resources/breeze_light/scalable/copy.svg b/launcher/resources/breeze_light/scalable/copy.svg new file mode 100644 index 000000000..2557953b0 --- /dev/null +++ b/launcher/resources/breeze_light/scalable/copy.svg @@ -0,0 +1,11 @@ + + + + + + + diff --git a/launcher/resources/breeze_light/scalable/coremods.svg b/launcher/resources/breeze_light/scalable/coremods.svg new file mode 100644 index 000000000..e4615cfa7 --- /dev/null +++ b/launcher/resources/breeze_light/scalable/coremods.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/launcher/resources/breeze_light/scalable/custom-commands.svg b/launcher/resources/breeze_light/scalable/custom-commands.svg new file mode 100644 index 000000000..b2ac78c59 --- /dev/null +++ b/launcher/resources/breeze_light/scalable/custom-commands.svg @@ -0,0 +1,13 @@ + + + + + + diff --git a/launcher/resources/breeze_light/scalable/delete.svg b/launcher/resources/breeze_light/scalable/delete.svg new file mode 100644 index 000000000..f2aea6e84 --- /dev/null +++ b/launcher/resources/breeze_light/scalable/delete.svg @@ -0,0 +1,13 @@ + + + + + + diff --git a/launcher/resources/breeze_light/scalable/discord.svg b/launcher/resources/breeze_light/scalable/discord.svg new file mode 100644 index 000000000..136239f7f --- /dev/null +++ b/launcher/resources/breeze_light/scalable/discord.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/launcher/resources/breeze_light/scalable/export.svg b/launcher/resources/breeze_light/scalable/export.svg new file mode 100644 index 000000000..d6314bd70 --- /dev/null +++ b/launcher/resources/breeze_light/scalable/export.svg @@ -0,0 +1,11 @@ + + + + + + + diff --git a/launcher/resources/breeze_light/scalable/externaltools.svg b/launcher/resources/breeze_light/scalable/externaltools.svg new file mode 100644 index 000000000..c965b6c37 --- /dev/null +++ b/launcher/resources/breeze_light/scalable/externaltools.svg @@ -0,0 +1,13 @@ + + + + + + diff --git a/launcher/resources/breeze_light/scalable/help.svg b/launcher/resources/breeze_light/scalable/help.svg new file mode 100644 index 000000000..bcd14e054 --- /dev/null +++ b/launcher/resources/breeze_light/scalable/help.svg @@ -0,0 +1,13 @@ + + + + + + diff --git a/launcher/resources/breeze_light/scalable/instance-settings.svg b/launcher/resources/breeze_light/scalable/instance-settings.svg new file mode 100644 index 000000000..69854d738 --- /dev/null +++ b/launcher/resources/breeze_light/scalable/instance-settings.svg @@ -0,0 +1,13 @@ + + + + + + diff --git a/launcher/resources/breeze_light/scalable/jarmods.svg b/launcher/resources/breeze_light/scalable/jarmods.svg new file mode 100644 index 000000000..72a8e504f --- /dev/null +++ b/launcher/resources/breeze_light/scalable/jarmods.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/launcher/resources/breeze_light/scalable/java.svg b/launcher/resources/breeze_light/scalable/java.svg new file mode 100644 index 000000000..ff86c9ccc --- /dev/null +++ b/launcher/resources/breeze_light/scalable/java.svg @@ -0,0 +1,10 @@ + + + + + + + + + + diff --git a/launcher/resources/breeze_light/scalable/language.svg b/launcher/resources/breeze_light/scalable/language.svg new file mode 100644 index 000000000..3d56d33e1 --- /dev/null +++ b/launcher/resources/breeze_light/scalable/language.svg @@ -0,0 +1,13 @@ + + + + + + diff --git a/launcher/resources/breeze_light/scalable/launch.svg b/launcher/resources/breeze_light/scalable/launch.svg new file mode 100644 index 000000000..678fd0988 --- /dev/null +++ b/launcher/resources/breeze_light/scalable/launch.svg @@ -0,0 +1,8 @@ + + + + diff --git a/launcher/resources/breeze_light/scalable/loadermods.svg b/launcher/resources/breeze_light/scalable/loadermods.svg new file mode 100644 index 000000000..4fb0f96d9 --- /dev/null +++ b/launcher/resources/breeze_light/scalable/loadermods.svg @@ -0,0 +1,13 @@ + + + + + + diff --git a/launcher/resources/breeze_light/scalable/log.svg b/launcher/resources/breeze_light/scalable/log.svg new file mode 100644 index 000000000..cf9c9b225 --- /dev/null +++ b/launcher/resources/breeze_light/scalable/log.svg @@ -0,0 +1,13 @@ + + + + + + diff --git a/launcher/resources/breeze_light/scalable/matrix.svg b/launcher/resources/breeze_light/scalable/matrix.svg new file mode 100644 index 000000000..4745efc11 --- /dev/null +++ b/launcher/resources/breeze_light/scalable/matrix.svg @@ -0,0 +1,9 @@ + + + Matrix (protocol) logo + + + + + + \ No newline at end of file diff --git a/launcher/resources/breeze_light/scalable/minecraft.svg b/launcher/resources/breeze_light/scalable/minecraft.svg new file mode 100644 index 000000000..1ffb4565f --- /dev/null +++ b/launcher/resources/breeze_light/scalable/minecraft.svg @@ -0,0 +1,13 @@ + + + + + + + + + + + diff --git a/launcher/resources/breeze_light/scalable/new.svg b/launcher/resources/breeze_light/scalable/new.svg new file mode 100644 index 000000000..6434a18e6 --- /dev/null +++ b/launcher/resources/breeze_light/scalable/new.svg @@ -0,0 +1,13 @@ + + + + + + diff --git a/launcher/resources/breeze_light/scalable/news.svg b/launcher/resources/breeze_light/scalable/news.svg new file mode 100644 index 000000000..3e3ebe950 --- /dev/null +++ b/launcher/resources/breeze_light/scalable/news.svg @@ -0,0 +1,13 @@ + + + + + + diff --git a/launcher/resources/breeze_light/scalable/notes.svg b/launcher/resources/breeze_light/scalable/notes.svg new file mode 100644 index 000000000..a8eaf279b --- /dev/null +++ b/launcher/resources/breeze_light/scalable/notes.svg @@ -0,0 +1,13 @@ + + + + + + diff --git a/launcher/resources/breeze_light/scalable/patreon.svg b/launcher/resources/breeze_light/scalable/patreon.svg new file mode 100644 index 000000000..e12f1f8d9 --- /dev/null +++ b/launcher/resources/breeze_light/scalable/patreon.svg @@ -0,0 +1,3 @@ + + + diff --git a/launcher/resources/breeze_light/scalable/proxy.svg b/launcher/resources/breeze_light/scalable/proxy.svg new file mode 100644 index 000000000..2e67ff6c9 --- /dev/null +++ b/launcher/resources/breeze_light/scalable/proxy.svg @@ -0,0 +1,14 @@ + + + + + + diff --git a/launcher/resources/breeze_light/scalable/reddit-alien.svg b/launcher/resources/breeze_light/scalable/reddit-alien.svg new file mode 100644 index 000000000..93b8eedc9 --- /dev/null +++ b/launcher/resources/breeze_light/scalable/reddit-alien.svg @@ -0,0 +1,3 @@ + + + diff --git a/launcher/resources/breeze_light/scalable/refresh.svg b/launcher/resources/breeze_light/scalable/refresh.svg new file mode 100644 index 000000000..ecd2b394e --- /dev/null +++ b/launcher/resources/breeze_light/scalable/refresh.svg @@ -0,0 +1,8 @@ + + + + + + diff --git a/launcher/resources/breeze_light/scalable/rename.svg b/launcher/resources/breeze_light/scalable/rename.svg new file mode 100644 index 000000000..18ccc58a8 --- /dev/null +++ b/launcher/resources/breeze_light/scalable/rename.svg @@ -0,0 +1,13 @@ + + + + + + diff --git a/launcher/resources/breeze_light/scalable/resourcepacks.svg b/launcher/resources/breeze_light/scalable/resourcepacks.svg new file mode 100644 index 000000000..913d3c1fa --- /dev/null +++ b/launcher/resources/breeze_light/scalable/resourcepacks.svg @@ -0,0 +1,11 @@ + + + + + + + diff --git a/launcher/resources/breeze_light/scalable/screenshots.svg b/launcher/resources/breeze_light/scalable/screenshots.svg new file mode 100644 index 000000000..d984b3307 --- /dev/null +++ b/launcher/resources/breeze_light/scalable/screenshots.svg @@ -0,0 +1,13 @@ + + + + + + diff --git a/launcher/resources/breeze_light/scalable/server.svg b/launcher/resources/breeze_light/scalable/server.svg new file mode 100644 index 000000000..52d7dd7d2 --- /dev/null +++ b/launcher/resources/breeze_light/scalable/server.svg @@ -0,0 +1,13 @@ + + + + + + diff --git a/launcher/resources/breeze_light/scalable/settings.svg b/launcher/resources/breeze_light/scalable/settings.svg new file mode 100644 index 000000000..19e86e26e --- /dev/null +++ b/launcher/resources/breeze_light/scalable/settings.svg @@ -0,0 +1,17 @@ + + + + + + diff --git a/launcher/resources/breeze_light/scalable/shaderpacks.svg b/launcher/resources/breeze_light/scalable/shaderpacks.svg new file mode 100644 index 000000000..591c6af5c --- /dev/null +++ b/launcher/resources/breeze_light/scalable/shaderpacks.svg @@ -0,0 +1,13 @@ + + + + + + diff --git a/launcher/resources/breeze_light/scalable/shortcut.svg b/launcher/resources/breeze_light/scalable/shortcut.svg new file mode 100644 index 000000000..426769d17 --- /dev/null +++ b/launcher/resources/breeze_light/scalable/shortcut.svg @@ -0,0 +1,18 @@ + + + + + + + + diff --git a/launcher/resources/breeze_light/scalable/status-bad.svg b/launcher/resources/breeze_light/scalable/status-bad.svg new file mode 100644 index 000000000..6fc3137e4 --- /dev/null +++ b/launcher/resources/breeze_light/scalable/status-bad.svg @@ -0,0 +1,9 @@ + + + + + diff --git a/launcher/resources/breeze_light/scalable/status-good.svg b/launcher/resources/breeze_light/scalable/status-good.svg new file mode 100644 index 000000000..eb8bc03be --- /dev/null +++ b/launcher/resources/breeze_light/scalable/status-good.svg @@ -0,0 +1,10 @@ + + + + + diff --git a/launcher/resources/breeze_light/scalable/status-yellow.svg b/launcher/resources/breeze_light/scalable/status-yellow.svg new file mode 100644 index 000000000..1dc4d0f51 --- /dev/null +++ b/launcher/resources/breeze_light/scalable/status-yellow.svg @@ -0,0 +1,9 @@ + + + + + diff --git a/launcher/resources/breeze_light/scalable/tag.svg b/launcher/resources/breeze_light/scalable/tag.svg new file mode 100644 index 000000000..4887d126e --- /dev/null +++ b/launcher/resources/breeze_light/scalable/tag.svg @@ -0,0 +1,17 @@ + + + + + + diff --git a/launcher/resources/breeze_light/scalable/viewfolder.svg b/launcher/resources/breeze_light/scalable/viewfolder.svg new file mode 100644 index 000000000..4a8498ceb --- /dev/null +++ b/launcher/resources/breeze_light/scalable/viewfolder.svg @@ -0,0 +1,13 @@ + + + + + + diff --git a/launcher/resources/breeze_light/scalable/worlds.svg b/launcher/resources/breeze_light/scalable/worlds.svg new file mode 100644 index 000000000..543cc55e4 --- /dev/null +++ b/launcher/resources/breeze_light/scalable/worlds.svg @@ -0,0 +1,16 @@ + + + + + + + + diff --git a/launcher/resources/flat/flat.qrc b/launcher/resources/flat/flat.qrc index 508e0a9f5..cadf87364 100644 --- a/launcher/resources/flat/flat.qrc +++ b/launcher/resources/flat/flat.qrc @@ -35,6 +35,7 @@ scalable/screenshot-placeholder.svg scalable/screenshots.svg scalable/settings.svg + scalable/shortcut.svg scalable/star.svg scalable/status-bad.svg scalable/status-good.svg @@ -46,6 +47,7 @@ scalable/tag.svg scalable/export.svg scalable/rename.svg + scalable/server.svg scalable/launch.svg diff --git a/launcher/resources/flat/scalable/server.svg b/launcher/resources/flat/scalable/server.svg new file mode 100644 index 000000000..c1d09d29a --- /dev/null +++ b/launcher/resources/flat/scalable/server.svg @@ -0,0 +1,44 @@ + + + + + + + diff --git a/launcher/resources/flat/scalable/shortcut.svg b/launcher/resources/flat/scalable/shortcut.svg new file mode 100644 index 000000000..83878d19f --- /dev/null +++ b/launcher/resources/flat/scalable/shortcut.svg @@ -0,0 +1,3 @@ + + + diff --git a/launcher/resources/flat_white/flat_white.qrc b/launcher/resources/flat_white/flat_white.qrc index e11d63165..2701462fe 100644 --- a/launcher/resources/flat_white/flat_white.qrc +++ b/launcher/resources/flat_white/flat_white.qrc @@ -35,6 +35,7 @@ scalable/screenshot-placeholder.svg scalable/screenshots.svg scalable/settings.svg + scalable/shortcut.svg scalable/star.svg scalable/status-bad.svg scalable/status-good.svg @@ -47,5 +48,6 @@ scalable/rename.svg scalable/tag.svg scalable/launch.svg + scalable/server.svg diff --git a/launcher/resources/flat_white/scalable/about.svg b/launcher/resources/flat_white/scalable/about.svg index e42ca9481..e2071c84f 100644 --- a/launcher/resources/flat_white/scalable/about.svg +++ b/launcher/resources/flat_white/scalable/about.svg @@ -1,3 +1,3 @@ - + - \ No newline at end of file + diff --git a/launcher/resources/flat_white/scalable/accounts.svg b/launcher/resources/flat_white/scalable/accounts.svg index e714bde13..0b413e2ac 100644 --- a/launcher/resources/flat_white/scalable/accounts.svg +++ b/launcher/resources/flat_white/scalable/accounts.svg @@ -1,3 +1,3 @@ - + - \ No newline at end of file + diff --git a/launcher/resources/flat_white/scalable/bug.svg b/launcher/resources/flat_white/scalable/bug.svg index 3122702e3..1e270acdb 100644 --- a/launcher/resources/flat_white/scalable/bug.svg +++ b/launcher/resources/flat_white/scalable/bug.svg @@ -1,3 +1,3 @@ - + - \ No newline at end of file + diff --git a/launcher/resources/flat_white/scalable/cat.svg b/launcher/resources/flat_white/scalable/cat.svg index 18da097a7..93470c4ff 100644 --- a/launcher/resources/flat_white/scalable/cat.svg +++ b/launcher/resources/flat_white/scalable/cat.svg @@ -1,3 +1,3 @@ - + - \ No newline at end of file + diff --git a/launcher/resources/flat_white/scalable/centralmods.svg b/launcher/resources/flat_white/scalable/centralmods.svg index d8d10f2f5..277fe1115 100644 --- a/launcher/resources/flat_white/scalable/centralmods.svg +++ b/launcher/resources/flat_white/scalable/centralmods.svg @@ -1,3 +1,3 @@ - + - \ No newline at end of file + diff --git a/launcher/resources/flat_white/scalable/checkupdate.svg b/launcher/resources/flat_white/scalable/checkupdate.svg index 0fa66fc2f..78db2b0cc 100644 --- a/launcher/resources/flat_white/scalable/checkupdate.svg +++ b/launcher/resources/flat_white/scalable/checkupdate.svg @@ -1,3 +1,3 @@ - + - \ No newline at end of file + diff --git a/launcher/resources/flat_white/scalable/copy.svg b/launcher/resources/flat_white/scalable/copy.svg index 1aaed97b1..abcb2b696 100644 --- a/launcher/resources/flat_white/scalable/copy.svg +++ b/launcher/resources/flat_white/scalable/copy.svg @@ -1,3 +1,3 @@ - + - \ No newline at end of file + diff --git a/launcher/resources/flat_white/scalable/coremods.svg b/launcher/resources/flat_white/scalable/coremods.svg index 32c343839..f3132a5fd 100644 --- a/launcher/resources/flat_white/scalable/coremods.svg +++ b/launcher/resources/flat_white/scalable/coremods.svg @@ -1,3 +1,3 @@ - + - \ No newline at end of file + diff --git a/launcher/resources/flat_white/scalable/custom-commands.svg b/launcher/resources/flat_white/scalable/custom-commands.svg index 3d67d8f10..fe1cf9987 100644 --- a/launcher/resources/flat_white/scalable/custom-commands.svg +++ b/launcher/resources/flat_white/scalable/custom-commands.svg @@ -1,6 +1,6 @@ - + - - \ No newline at end of file + + diff --git a/launcher/resources/flat_white/scalable/discord.svg b/launcher/resources/flat_white/scalable/discord.svg index ee07ed25e..6a07d2289 100644 --- a/launcher/resources/flat_white/scalable/discord.svg +++ b/launcher/resources/flat_white/scalable/discord.svg @@ -1,4 +1,4 @@ - + - \ No newline at end of file + diff --git a/launcher/resources/flat_white/scalable/export.svg b/launcher/resources/flat_white/scalable/export.svg index a28bb2540..095952118 100644 --- a/launcher/resources/flat_white/scalable/export.svg +++ b/launcher/resources/flat_white/scalable/export.svg @@ -1,5 +1,5 @@ - + - \ No newline at end of file + diff --git a/launcher/resources/flat_white/scalable/externaltools.svg b/launcher/resources/flat_white/scalable/externaltools.svg index e7c0930c9..d641f4f21 100644 --- a/launcher/resources/flat_white/scalable/externaltools.svg +++ b/launcher/resources/flat_white/scalable/externaltools.svg @@ -1,3 +1,3 @@ - + - \ No newline at end of file + diff --git a/launcher/resources/flat_white/scalable/help.svg b/launcher/resources/flat_white/scalable/help.svg index 82b413fe5..31e8c092b 100644 --- a/launcher/resources/flat_white/scalable/help.svg +++ b/launcher/resources/flat_white/scalable/help.svg @@ -10,7 +10,7 @@ width="24" viewBox="0 0 24 24" height="24" - fill="#D8DEE9"> + fill="#eeeeee"> diff --git a/launcher/resources/flat_white/scalable/instance-settings.svg b/launcher/resources/flat_white/scalable/instance-settings.svg index 7dac7b141..95a0a8026 100644 --- a/launcher/resources/flat_white/scalable/instance-settings.svg +++ b/launcher/resources/flat_white/scalable/instance-settings.svg @@ -1,3 +1,3 @@ - + - \ No newline at end of file + diff --git a/launcher/resources/flat_white/scalable/jarmods.svg b/launcher/resources/flat_white/scalable/jarmods.svg index f0f298f32..603a8ae9a 100644 --- a/launcher/resources/flat_white/scalable/jarmods.svg +++ b/launcher/resources/flat_white/scalable/jarmods.svg @@ -1,3 +1,3 @@ - + - \ No newline at end of file + diff --git a/launcher/resources/flat_white/scalable/java.svg b/launcher/resources/flat_white/scalable/java.svg index 56bb481f2..db81128e0 100644 --- a/launcher/resources/flat_white/scalable/java.svg +++ b/launcher/resources/flat_white/scalable/java.svg @@ -1,3 +1,3 @@ - + - \ No newline at end of file + diff --git a/launcher/resources/flat_white/scalable/language.svg b/launcher/resources/flat_white/scalable/language.svg index 18c22efb5..4aef29468 100644 --- a/launcher/resources/flat_white/scalable/language.svg +++ b/launcher/resources/flat_white/scalable/language.svg @@ -7,7 +7,7 @@ xmlns="http://www.w3.org/2000/svg" xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd" xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape" - fill="#D8DEE9" + fill="#eeeeee" height="24" viewBox="0 0 24 24" width="24" diff --git a/launcher/resources/flat_white/scalable/launch.svg b/launcher/resources/flat_white/scalable/launch.svg index 9e7594318..ddd6d5f2c 100644 --- a/launcher/resources/flat_white/scalable/launch.svg +++ b/launcher/resources/flat_white/scalable/launch.svg @@ -1,6 +1,6 @@ - + diff --git a/launcher/resources/flat_white/scalable/loadermods.svg b/launcher/resources/flat_white/scalable/loadermods.svg index 100f7a93e..95c72084a 100644 --- a/launcher/resources/flat_white/scalable/loadermods.svg +++ b/launcher/resources/flat_white/scalable/loadermods.svg @@ -1,3 +1,3 @@ - + - \ No newline at end of file + diff --git a/launcher/resources/flat_white/scalable/log.svg b/launcher/resources/flat_white/scalable/log.svg index 69b7c1dcc..a40139d36 100644 --- a/launcher/resources/flat_white/scalable/log.svg +++ b/launcher/resources/flat_white/scalable/log.svg @@ -1,3 +1,3 @@ - + - \ No newline at end of file + diff --git a/launcher/resources/flat_white/scalable/minecraft.svg b/launcher/resources/flat_white/scalable/minecraft.svg index a0348e797..94aaebd13 100644 --- a/launcher/resources/flat_white/scalable/minecraft.svg +++ b/launcher/resources/flat_white/scalable/minecraft.svg @@ -1,3 +1,3 @@ - + - \ No newline at end of file + diff --git a/launcher/resources/flat_white/scalable/multimc.svg b/launcher/resources/flat_white/scalable/multimc.svg index 3dce2699b..9afe68d96 100644 --- a/launcher/resources/flat_white/scalable/multimc.svg +++ b/launcher/resources/flat_white/scalable/multimc.svg @@ -1,3 +1,3 @@ - + - \ No newline at end of file + diff --git a/launcher/resources/flat_white/scalable/new.svg b/launcher/resources/flat_white/scalable/new.svg index 46dc3361a..22c6a6fe3 100644 --- a/launcher/resources/flat_white/scalable/new.svg +++ b/launcher/resources/flat_white/scalable/new.svg @@ -1,3 +1,3 @@ - + - \ No newline at end of file + diff --git a/launcher/resources/flat_white/scalable/news.svg b/launcher/resources/flat_white/scalable/news.svg index 414e54540..76623f341 100644 --- a/launcher/resources/flat_white/scalable/news.svg +++ b/launcher/resources/flat_white/scalable/news.svg @@ -1,3 +1,3 @@ - + - \ No newline at end of file + diff --git a/launcher/resources/flat_white/scalable/notes.svg b/launcher/resources/flat_white/scalable/notes.svg index 4ce5f6f19..18a1265de 100644 --- a/launcher/resources/flat_white/scalable/notes.svg +++ b/launcher/resources/flat_white/scalable/notes.svg @@ -1,3 +1,3 @@ - + - \ No newline at end of file + diff --git a/launcher/resources/flat_white/scalable/packages.svg b/launcher/resources/flat_white/scalable/packages.svg index 909ad0b27..d2c879557 100644 --- a/launcher/resources/flat_white/scalable/packages.svg +++ b/launcher/resources/flat_white/scalable/packages.svg @@ -1,3 +1,3 @@ - + - \ No newline at end of file + diff --git a/launcher/resources/flat_white/scalable/patreon.svg b/launcher/resources/flat_white/scalable/patreon.svg index b745765b4..d5385eac1 100644 --- a/launcher/resources/flat_white/scalable/patreon.svg +++ b/launcher/resources/flat_white/scalable/patreon.svg @@ -1,3 +1,3 @@ - + - \ No newline at end of file + diff --git a/launcher/resources/flat_white/scalable/proxy.svg b/launcher/resources/flat_white/scalable/proxy.svg index a86703f45..30e27e8a4 100644 --- a/launcher/resources/flat_white/scalable/proxy.svg +++ b/launcher/resources/flat_white/scalable/proxy.svg @@ -1,3 +1,3 @@ - + - \ No newline at end of file + diff --git a/launcher/resources/flat_white/scalable/quickmods.svg b/launcher/resources/flat_white/scalable/quickmods.svg index 9e0045b20..599bd2bf3 100644 --- a/launcher/resources/flat_white/scalable/quickmods.svg +++ b/launcher/resources/flat_white/scalable/quickmods.svg @@ -1,3 +1,3 @@ - + - \ No newline at end of file + diff --git a/launcher/resources/flat_white/scalable/reddit-alien.svg b/launcher/resources/flat_white/scalable/reddit-alien.svg index be22148ca..291b12e0e 100644 --- a/launcher/resources/flat_white/scalable/reddit-alien.svg +++ b/launcher/resources/flat_white/scalable/reddit-alien.svg @@ -1,3 +1,3 @@ - + - \ No newline at end of file + diff --git a/launcher/resources/flat_white/scalable/refresh.svg b/launcher/resources/flat_white/scalable/refresh.svg index 08c63bdf7..e8c6c44b3 100644 --- a/launcher/resources/flat_white/scalable/refresh.svg +++ b/launcher/resources/flat_white/scalable/refresh.svg @@ -1,3 +1,3 @@ - + - \ No newline at end of file + diff --git a/launcher/resources/flat_white/scalable/rename.svg b/launcher/resources/flat_white/scalable/rename.svg index f2067c16b..e7d6634a2 100644 --- a/launcher/resources/flat_white/scalable/rename.svg +++ b/launcher/resources/flat_white/scalable/rename.svg @@ -1,4 +1,4 @@ - + - \ No newline at end of file + diff --git a/launcher/resources/flat_white/scalable/resourcepacks.svg b/launcher/resources/flat_white/scalable/resourcepacks.svg index 9dd73c3af..272af76b7 100644 --- a/launcher/resources/flat_white/scalable/resourcepacks.svg +++ b/launcher/resources/flat_white/scalable/resourcepacks.svg @@ -1,3 +1,3 @@ - + - \ No newline at end of file + diff --git a/launcher/resources/flat_white/scalable/screenshot-placeholder.svg b/launcher/resources/flat_white/scalable/screenshot-placeholder.svg index 41eb6fcfc..162b78040 100644 --- a/launcher/resources/flat_white/scalable/screenshot-placeholder.svg +++ b/launcher/resources/flat_white/scalable/screenshot-placeholder.svg @@ -1,3 +1,3 @@ - + - \ No newline at end of file + diff --git a/launcher/resources/flat_white/scalable/screenshots.svg b/launcher/resources/flat_white/scalable/screenshots.svg index 68cf89696..ae1c876df 100644 --- a/launcher/resources/flat_white/scalable/screenshots.svg +++ b/launcher/resources/flat_white/scalable/screenshots.svg @@ -1,3 +1,3 @@ - + - \ No newline at end of file + diff --git a/launcher/resources/flat_white/scalable/server.svg b/launcher/resources/flat_white/scalable/server.svg new file mode 100644 index 000000000..f41db1b22 --- /dev/null +++ b/launcher/resources/flat_white/scalable/server.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/launcher/resources/flat_white/scalable/settings.svg b/launcher/resources/flat_white/scalable/settings.svg index 7dac7b141..95a0a8026 100644 --- a/launcher/resources/flat_white/scalable/settings.svg +++ b/launcher/resources/flat_white/scalable/settings.svg @@ -1,3 +1,3 @@ - + - \ No newline at end of file + diff --git a/launcher/resources/flat_white/scalable/shaderpacks.svg b/launcher/resources/flat_white/scalable/shaderpacks.svg index ccae221c1..bfd8b8332 100644 --- a/launcher/resources/flat_white/scalable/shaderpacks.svg +++ b/launcher/resources/flat_white/scalable/shaderpacks.svg @@ -1,6 +1,6 @@ diff --git a/launcher/resources/flat_white/scalable/shortcut.svg b/launcher/resources/flat_white/scalable/shortcut.svg new file mode 100644 index 000000000..77ccbdd46 --- /dev/null +++ b/launcher/resources/flat_white/scalable/shortcut.svg @@ -0,0 +1,3 @@ + + + diff --git a/launcher/resources/flat_white/scalable/star.svg b/launcher/resources/flat_white/scalable/star.svg index 116f952eb..2a573ca30 100644 --- a/launcher/resources/flat_white/scalable/star.svg +++ b/launcher/resources/flat_white/scalable/star.svg @@ -1,3 +1,3 @@ - + - \ No newline at end of file + diff --git a/launcher/resources/flat_white/scalable/status-bad.svg b/launcher/resources/flat_white/scalable/status-bad.svg index 5a121c09c..b6b42a968 100644 --- a/launcher/resources/flat_white/scalable/status-bad.svg +++ b/launcher/resources/flat_white/scalable/status-bad.svg @@ -1,3 +1,3 @@ - + - \ No newline at end of file + diff --git a/launcher/resources/flat_white/scalable/status-good.svg b/launcher/resources/flat_white/scalable/status-good.svg index ccb732ab8..aee4c5234 100644 --- a/launcher/resources/flat_white/scalable/status-good.svg +++ b/launcher/resources/flat_white/scalable/status-good.svg @@ -1,3 +1,3 @@ - + - \ No newline at end of file + diff --git a/launcher/resources/flat_white/scalable/status-running.svg b/launcher/resources/flat_white/scalable/status-running.svg index aa2d5fbff..d4d551944 100644 --- a/launcher/resources/flat_white/scalable/status-running.svg +++ b/launcher/resources/flat_white/scalable/status-running.svg @@ -1,3 +1,3 @@ - + - \ No newline at end of file + diff --git a/launcher/resources/flat_white/scalable/status-yellow.svg b/launcher/resources/flat_white/scalable/status-yellow.svg index 772699d32..00737f515 100644 --- a/launcher/resources/flat_white/scalable/status-yellow.svg +++ b/launcher/resources/flat_white/scalable/status-yellow.svg @@ -1,3 +1,3 @@ - + - \ No newline at end of file + diff --git a/launcher/resources/flat_white/scalable/tag.svg b/launcher/resources/flat_white/scalable/tag.svg index f91fb0b42..0d7661e0d 100644 --- a/launcher/resources/flat_white/scalable/tag.svg +++ b/launcher/resources/flat_white/scalable/tag.svg @@ -1,4 +1,4 @@ - - - \ No newline at end of file + + + diff --git a/launcher/resources/flat_white/scalable/viewfolder.svg b/launcher/resources/flat_white/scalable/viewfolder.svg index 145f86248..b13c8eb36 100644 --- a/launcher/resources/flat_white/scalable/viewfolder.svg +++ b/launcher/resources/flat_white/scalable/viewfolder.svg @@ -1,3 +1,3 @@ - + - \ No newline at end of file + diff --git a/launcher/resources/flat_white/scalable/worlds.svg b/launcher/resources/flat_white/scalable/worlds.svg index cea30cf81..d7aaef1d5 100644 --- a/launcher/resources/flat_white/scalable/worlds.svg +++ b/launcher/resources/flat_white/scalable/worlds.svg @@ -1,3 +1,3 @@ - + - \ No newline at end of file + diff --git a/launcher/resources/iOS/iOS.qrc b/launcher/resources/iOS/iOS.qrc index aa08d811a..0b79efb2a 100644 --- a/launcher/resources/iOS/iOS.qrc +++ b/launcher/resources/iOS/iOS.qrc @@ -39,5 +39,6 @@ scalable/export.svg scalable/rename.svg scalable/launch.svg + scalable/shortcut.svg diff --git a/launcher/resources/iOS/scalable/shortcut.svg b/launcher/resources/iOS/scalable/shortcut.svg new file mode 100644 index 000000000..16e9fa488 --- /dev/null +++ b/launcher/resources/iOS/scalable/shortcut.svg @@ -0,0 +1,13 @@ + + + + + + + + + + diff --git a/launcher/resources/multimc/128x128/instances/chicken.png b/launcher/resources/multimc/128x128/instances/chicken_legacy.png similarity index 100% rename from launcher/resources/multimc/128x128/instances/chicken.png rename to launcher/resources/multimc/128x128/instances/chicken_legacy.png diff --git a/launcher/resources/multimc/128x128/instances/creeper.png b/launcher/resources/multimc/128x128/instances/creeper_legacy.png similarity index 100% rename from launcher/resources/multimc/128x128/instances/creeper.png rename to launcher/resources/multimc/128x128/instances/creeper_legacy.png diff --git a/launcher/resources/multimc/128x128/instances/enderpearl.png b/launcher/resources/multimc/128x128/instances/enderpearl_legacy.png similarity index 100% rename from launcher/resources/multimc/128x128/instances/enderpearl.png rename to launcher/resources/multimc/128x128/instances/enderpearl_legacy.png diff --git a/launcher/resources/multimc/128x128/instances/flame.png b/launcher/resources/multimc/128x128/instances/flame_legacy.png similarity index 100% rename from launcher/resources/multimc/128x128/instances/flame.png rename to launcher/resources/multimc/128x128/instances/flame_legacy.png diff --git a/launcher/resources/multimc/128x128/instances/ftb_logo.png b/launcher/resources/multimc/128x128/instances/ftb_logo_legacy.png similarity index 100% rename from launcher/resources/multimc/128x128/instances/ftb_logo.png rename to launcher/resources/multimc/128x128/instances/ftb_logo_legacy.png diff --git a/launcher/resources/multimc/128x128/instances/gear.png b/launcher/resources/multimc/128x128/instances/gear_legacy.png similarity index 100% rename from launcher/resources/multimc/128x128/instances/gear.png rename to launcher/resources/multimc/128x128/instances/gear_legacy.png diff --git a/launcher/resources/multimc/128x128/instances/herobrine.png b/launcher/resources/multimc/128x128/instances/herobrine_legacy.png similarity index 100% rename from launcher/resources/multimc/128x128/instances/herobrine.png rename to launcher/resources/multimc/128x128/instances/herobrine_legacy.png diff --git a/launcher/resources/multimc/128x128/instances/infinity.png b/launcher/resources/multimc/128x128/instances/infinity_legacy.png similarity index 100% rename from launcher/resources/multimc/128x128/instances/infinity.png rename to launcher/resources/multimc/128x128/instances/infinity_legacy.png diff --git a/launcher/resources/multimc/128x128/instances/magitech.png b/launcher/resources/multimc/128x128/instances/magitech_legacy.png similarity index 100% rename from launcher/resources/multimc/128x128/instances/magitech.png rename to launcher/resources/multimc/128x128/instances/magitech_legacy.png diff --git a/launcher/resources/multimc/128x128/instances/meat.png b/launcher/resources/multimc/128x128/instances/meat_legacy.png similarity index 100% rename from launcher/resources/multimc/128x128/instances/meat.png rename to launcher/resources/multimc/128x128/instances/meat_legacy.png diff --git a/launcher/resources/multimc/128x128/instances/netherstar.png b/launcher/resources/multimc/128x128/instances/netherstar_legacy.png similarity index 100% rename from launcher/resources/multimc/128x128/instances/netherstar.png rename to launcher/resources/multimc/128x128/instances/netherstar_legacy.png diff --git a/launcher/resources/multimc/128x128/instances/skeleton.png b/launcher/resources/multimc/128x128/instances/skeleton_legacy.png similarity index 100% rename from launcher/resources/multimc/128x128/instances/skeleton.png rename to launcher/resources/multimc/128x128/instances/skeleton_legacy.png diff --git a/launcher/resources/multimc/128x128/instances/squarecreeper.png b/launcher/resources/multimc/128x128/instances/squarecreeper_legacy.png similarity index 100% rename from launcher/resources/multimc/128x128/instances/squarecreeper.png rename to launcher/resources/multimc/128x128/instances/squarecreeper_legacy.png diff --git a/launcher/resources/multimc/128x128/instances/steve.png b/launcher/resources/multimc/128x128/instances/steve_legacy.png similarity index 100% rename from launcher/resources/multimc/128x128/instances/steve.png rename to launcher/resources/multimc/128x128/instances/steve_legacy.png diff --git a/launcher/resources/multimc/32x32/instances/brick.png b/launcher/resources/multimc/32x32/instances/brick_legacy.png similarity index 100% rename from launcher/resources/multimc/32x32/instances/brick.png rename to launcher/resources/multimc/32x32/instances/brick_legacy.png diff --git a/launcher/resources/multimc/32x32/instances/chicken.png b/launcher/resources/multimc/32x32/instances/chicken_legacy.png similarity index 100% rename from launcher/resources/multimc/32x32/instances/chicken.png rename to launcher/resources/multimc/32x32/instances/chicken_legacy.png diff --git a/launcher/resources/multimc/32x32/instances/creeper.png b/launcher/resources/multimc/32x32/instances/creeper_legacy.png similarity index 100% rename from launcher/resources/multimc/32x32/instances/creeper.png rename to launcher/resources/multimc/32x32/instances/creeper_legacy.png diff --git a/launcher/resources/multimc/32x32/instances/diamond.png b/launcher/resources/multimc/32x32/instances/diamond_legacy.png similarity index 100% rename from launcher/resources/multimc/32x32/instances/diamond.png rename to launcher/resources/multimc/32x32/instances/diamond_legacy.png diff --git a/launcher/resources/multimc/32x32/instances/dirt.png b/launcher/resources/multimc/32x32/instances/dirt_legacy.png similarity index 100% rename from launcher/resources/multimc/32x32/instances/dirt.png rename to launcher/resources/multimc/32x32/instances/dirt_legacy.png diff --git a/launcher/resources/multimc/32x32/instances/enderpearl.png b/launcher/resources/multimc/32x32/instances/enderpearl_legacy.png similarity index 100% rename from launcher/resources/multimc/32x32/instances/enderpearl.png rename to launcher/resources/multimc/32x32/instances/enderpearl_legacy.png diff --git a/launcher/resources/multimc/32x32/instances/ftb_logo.png b/launcher/resources/multimc/32x32/instances/ftb_logo_legacy.png similarity index 100% rename from launcher/resources/multimc/32x32/instances/ftb_logo.png rename to launcher/resources/multimc/32x32/instances/ftb_logo_legacy.png diff --git a/launcher/resources/multimc/32x32/instances/gear.png b/launcher/resources/multimc/32x32/instances/gear_legacy.png similarity index 100% rename from launcher/resources/multimc/32x32/instances/gear.png rename to launcher/resources/multimc/32x32/instances/gear_legacy.png diff --git a/launcher/resources/multimc/32x32/instances/gold.png b/launcher/resources/multimc/32x32/instances/gold_legacy.png similarity index 100% rename from launcher/resources/multimc/32x32/instances/gold.png rename to launcher/resources/multimc/32x32/instances/gold_legacy.png diff --git a/launcher/resources/multimc/32x32/instances/grass.png b/launcher/resources/multimc/32x32/instances/grass_legacy.png similarity index 100% rename from launcher/resources/multimc/32x32/instances/grass.png rename to launcher/resources/multimc/32x32/instances/grass_legacy.png diff --git a/launcher/resources/multimc/32x32/instances/herobrine.png b/launcher/resources/multimc/32x32/instances/herobrine_legacy.png similarity index 100% rename from launcher/resources/multimc/32x32/instances/herobrine.png rename to launcher/resources/multimc/32x32/instances/herobrine_legacy.png diff --git a/launcher/resources/multimc/32x32/instances/infinity.png b/launcher/resources/multimc/32x32/instances/infinity_legacy.png similarity index 100% rename from launcher/resources/multimc/32x32/instances/infinity.png rename to launcher/resources/multimc/32x32/instances/infinity_legacy.png diff --git a/launcher/resources/multimc/32x32/instances/iron.png b/launcher/resources/multimc/32x32/instances/iron_legacy.png similarity index 100% rename from launcher/resources/multimc/32x32/instances/iron.png rename to launcher/resources/multimc/32x32/instances/iron_legacy.png diff --git a/launcher/resources/multimc/32x32/instances/magitech.png b/launcher/resources/multimc/32x32/instances/magitech_legacy.png similarity index 100% rename from launcher/resources/multimc/32x32/instances/magitech.png rename to launcher/resources/multimc/32x32/instances/magitech_legacy.png diff --git a/launcher/resources/multimc/32x32/instances/meat.png b/launcher/resources/multimc/32x32/instances/meat_legacy.png similarity index 100% rename from launcher/resources/multimc/32x32/instances/meat.png rename to launcher/resources/multimc/32x32/instances/meat_legacy.png diff --git a/launcher/resources/multimc/32x32/instances/netherstar.png b/launcher/resources/multimc/32x32/instances/netherstar_legacy.png similarity index 100% rename from launcher/resources/multimc/32x32/instances/netherstar.png rename to launcher/resources/multimc/32x32/instances/netherstar_legacy.png diff --git a/launcher/resources/multimc/32x32/instances/planks.png b/launcher/resources/multimc/32x32/instances/planks_legacy.png similarity index 100% rename from launcher/resources/multimc/32x32/instances/planks.png rename to launcher/resources/multimc/32x32/instances/planks_legacy.png diff --git a/launcher/resources/multimc/32x32/instances/skeleton.png b/launcher/resources/multimc/32x32/instances/skeleton_legacy.png similarity index 100% rename from launcher/resources/multimc/32x32/instances/skeleton.png rename to launcher/resources/multimc/32x32/instances/skeleton_legacy.png diff --git a/launcher/resources/multimc/32x32/instances/squarecreeper.png b/launcher/resources/multimc/32x32/instances/squarecreeper_legacy.png similarity index 100% rename from launcher/resources/multimc/32x32/instances/squarecreeper.png rename to launcher/resources/multimc/32x32/instances/squarecreeper_legacy.png diff --git a/launcher/resources/multimc/32x32/instances/steve.png b/launcher/resources/multimc/32x32/instances/steve_legacy.png similarity index 100% rename from launcher/resources/multimc/32x32/instances/steve.png rename to launcher/resources/multimc/32x32/instances/steve_legacy.png diff --git a/launcher/resources/multimc/32x32/instances/stone.png b/launcher/resources/multimc/32x32/instances/stone_legacy.png similarity index 100% rename from launcher/resources/multimc/32x32/instances/stone.png rename to launcher/resources/multimc/32x32/instances/stone_legacy.png diff --git a/launcher/resources/multimc/32x32/instances/tnt.png b/launcher/resources/multimc/32x32/instances/tnt_legacy.png similarity index 100% rename from launcher/resources/multimc/32x32/instances/tnt.png rename to launcher/resources/multimc/32x32/instances/tnt_legacy.png diff --git a/launcher/resources/multimc/50x50/instances/enderman.png b/launcher/resources/multimc/50x50/instances/enderman_legacy.png similarity index 100% rename from launcher/resources/multimc/50x50/instances/enderman.png rename to launcher/resources/multimc/50x50/instances/enderman_legacy.png diff --git a/launcher/resources/multimc/multimc.qrc b/launcher/resources/multimc/multimc.qrc index 3f3d22fc1..2c00f28fa 100644 --- a/launcher/resources/multimc/multimc.qrc +++ b/launcher/resources/multimc/multimc.qrc @@ -6,9 +6,6 @@ scalable/reddit-alien.svg - - 128x128/instances/flame.png - scalable/launcher.svg @@ -31,14 +28,14 @@ scalable/java.svg - + 16x16/star.png 24x24/star.png 32x32/star.png 48x48/star.png 64x64/star.png - + 16x16/worlds.png 22x22/worlds.png 32x32/worlds.png @@ -87,7 +84,7 @@ 48x48/cat.png 64x64/cat.png - + scalable/centralmods.svg 16x16/centralmods.png 22x22/centralmods.png @@ -165,25 +162,25 @@ 64x64/status-running.png scalable/status-running.svg - + 16x16/loadermods.png 24x24/loadermods.png 32x32/loadermods.png 64x64/loadermods.png - + 16x16/jarmods.png 24x24/jarmods.png 32x32/jarmods.png 64x64/jarmods.png - + 16x16/coremods.png 24x24/coremods.png 32x32/coremods.png 64x64/coremods.png - + 16x16/resourcepacks.png 24x24/resourcepacks.png 32x32/resourcepacks.png @@ -192,7 +189,7 @@ 128x128/shaderpacks.png - + 16x16/refresh.png 22x22/refresh.png 32x32/refresh.png @@ -254,63 +251,101 @@ scalable/discord.svg + + scalable/instances/flame.svg + scalable/instances/chicken.svg + scalable/instances/creeper.svg + scalable/instances/enderpearl.svg + scalable/instances/ftb_logo.svg + scalable/instances/flame.svg + scalable/instances/gear.svg + scalable/instances/herobrine.svg + scalable/instances/magitech.svg + scalable/instances/meat.svg + scalable/instances/netherstar.svg + scalable/instances/skeleton.svg + scalable/instances/squarecreeper.svg + scalable/instances/steve.svg + scalable/instances/diamond.svg + scalable/instances/dirt.svg + scalable/instances/grass.svg + scalable/instances/brick.svg + scalable/instances/gold.svg + scalable/instances/iron.svg + scalable/instances/planks.svg + scalable/instances/stone.svg + scalable/instances/tnt.svg + scalable/instances/enderman.svg + scalable/instances/fox.svg + scalable/instances/bee.svg + - 32x32/instances/chicken.png - 128x128/instances/chicken.png + 32x32/instances/chicken_legacy.png + 128x128/instances/chicken_legacy.png - 32x32/instances/creeper.png - 128x128/instances/creeper.png + 32x32/instances/creeper_legacy.png + 128x128/instances/creeper_legacy.png - 32x32/instances/enderpearl.png - 128x128/instances/enderpearl.png + 32x32/instances/enderpearl_legacy.png + 128x128/instances/enderpearl_legacy.png 32x32/instances/ftb_glow.png 128x128/instances/ftb_glow.png - 32x32/instances/ftb_logo.png - 128x128/instances/ftb_logo.png + 32x32/instances/ftb_logo_legacy.png + 128x128/instances/ftb_logo_legacy.png - 128x128/instances/flame.png + 128x128/instances/flame_legacy.png - 32x32/instances/gear.png - 128x128/instances/gear.png + 32x32/instances/gear_legacy.png + 128x128/instances/gear_legacy.png - 32x32/instances/herobrine.png - 128x128/instances/herobrine.png + 32x32/instances/herobrine_legacy.png + 128x128/instances/herobrine_legacy.png - 32x32/instances/magitech.png - 128x128/instances/magitech.png + 32x32/instances/magitech_legacy.png + 128x128/instances/magitech_legacy.png - 32x32/instances/meat.png - 128x128/instances/meat.png + 32x32/instances/meat_legacy.png + 128x128/instances/meat_legacy.png - 32x32/instances/netherstar.png - 128x128/instances/netherstar.png + 32x32/instances/netherstar_legacy.png + 128x128/instances/netherstar_legacy.png - 32x32/instances/skeleton.png - 128x128/instances/skeleton.png + 32x32/instances/skeleton_legacy.png + 128x128/instances/skeleton_legacy.png - 32x32/instances/squarecreeper.png - 128x128/instances/squarecreeper.png + 32x32/instances/squarecreeper_legacy.png + 128x128/instances/squarecreeper_legacy.png - 32x32/instances/steve.png - 128x128/instances/steve.png + 32x32/instances/steve_legacy.png + 128x128/instances/steve_legacy.png - 32x32/instances/brick.png - 32x32/instances/diamond.png - 32x32/instances/dirt.png - 32x32/instances/gold.png - 32x32/instances/grass.png - 32x32/instances/iron.png - 32x32/instances/planks.png - 32x32/instances/stone.png - 32x32/instances/tnt.png + 32x32/instances/brick_legacy.png + 32x32/instances/diamond_legacy.png + 32x32/instances/dirt_legacy.png + 32x32/instances/gold_legacy.png + 32x32/instances/grass_legacy.png + 32x32/instances/iron_legacy.png + 32x32/instances/planks_legacy.png + 32x32/instances/stone_legacy.png + 32x32/instances/tnt_legacy.png - 50x50/instances/enderman.png + 50x50/instances/enderman_legacy.png - scalable/instances/fox.svg - scalable/instances/bee.svg scalable/instances/prismlauncher.svg + scalable/instances/fox_legacy.svg + scalable/instances/bee_legacy.svg + + + scalable/delete.svg + scalable/tag.svg + scalable/rename.svg + scalable/shortcut.svg + + scalable/export.svg + scalable/launch.svg + scalable/server.svg diff --git a/launcher/resources/multimc/scalable/delete.svg b/launcher/resources/multimc/scalable/delete.svg new file mode 100644 index 000000000..414cbd5c6 --- /dev/null +++ b/launcher/resources/multimc/scalable/delete.svg @@ -0,0 +1,282 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/launcher/resources/multimc/scalable/export.svg b/launcher/resources/multimc/scalable/export.svg new file mode 100644 index 000000000..2605de14e --- /dev/null +++ b/launcher/resources/multimc/scalable/export.svg @@ -0,0 +1,466 @@ + + + + + + + + + + + unsorted + + + + + Open Clip Art Library, Source: Oxygen Icons, Source: Oxygen Icons, Source: Oxygen Icons, Source: Oxygen Icons + + + + + + + + + + + + + + image/svg+xml + + + en + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/launcher/resources/multimc/scalable/instances/bee.svg b/launcher/resources/multimc/scalable/instances/bee.svg index 49f216c8f..110b224ce 100644 --- a/launcher/resources/multimc/scalable/instances/bee.svg +++ b/launcher/resources/multimc/scalable/instances/bee.svg @@ -1,159 +1,136 @@ + + - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + viewBox="0 0 24 24" + id="svg168" + xml:space="preserve" + xmlns:xlink="http://www.w3.org/1999/xlink" + xmlns="http://www.w3.org/2000/svg" + xmlns:svg="http://www.w3.org/2000/svg" + xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#" + xmlns:cc="http://creativecommons.org/ns#" + xmlns:dc="http://purl.org/dc/elements/1.1/">Prism Launcher LogoPrism Launcher Logo19/10/2022Prism LauncherAutiOne, Boba, ely, Fulmine, gon sawa, Pankakes, tobimori, Zekehttps://github.com/PrismLauncher/PrismLauncherCC BY-SA 4.0Prism Launcher diff --git a/launcher/resources/multimc/scalable/instances/bee_legacy.svg b/launcher/resources/multimc/scalable/instances/bee_legacy.svg new file mode 100644 index 000000000..49f216c8f --- /dev/null +++ b/launcher/resources/multimc/scalable/instances/bee_legacy.svg @@ -0,0 +1,159 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/launcher/resources/multimc/scalable/instances/brick.svg b/launcher/resources/multimc/scalable/instances/brick.svg new file mode 100644 index 000000000..b600eba8b --- /dev/null +++ b/launcher/resources/multimc/scalable/instances/brick.svg @@ -0,0 +1,67 @@ + + + +Prism Launcher LogoPrism Launcher Logo19/10/2022Prism LauncherAutiOne, Boba, ely, Fulmine, gon sawa, Pankakes, tobimori, Zekehttps://github.com/PrismLauncher/PrismLauncherCC BY-SA 4.0Prism Launcher diff --git a/launcher/resources/multimc/scalable/instances/chicken.svg b/launcher/resources/multimc/scalable/instances/chicken.svg new file mode 100644 index 000000000..0b5bf017b --- /dev/null +++ b/launcher/resources/multimc/scalable/instances/chicken.svg @@ -0,0 +1,130 @@ + + + +Prism Launcher LogoPrism Launcher Logo19/10/2022Prism LauncherAutiOne, Boba, ely, Fulmine, gon sawa, Pankakes, tobimori, Zekehttps://github.com/PrismLauncher/PrismLauncherCC BY-SA 4.0Prism Launcher diff --git a/launcher/resources/multimc/scalable/instances/creeper.svg b/launcher/resources/multimc/scalable/instances/creeper.svg new file mode 100644 index 000000000..4a9fe380f --- /dev/null +++ b/launcher/resources/multimc/scalable/instances/creeper.svg @@ -0,0 +1,68 @@ + + + +Prism Launcher LogoPrism Launcher Logo19/10/2022Prism LauncherAutiOne, Boba, ely, Fulmine, gon sawa, Pankakes, tobimori, Zekehttps://github.com/PrismLauncher/PrismLauncherCC BY-SA 4.0Prism Launcher diff --git a/launcher/resources/multimc/scalable/instances/diamond.svg b/launcher/resources/multimc/scalable/instances/diamond.svg new file mode 100644 index 000000000..1d490b918 --- /dev/null +++ b/launcher/resources/multimc/scalable/instances/diamond.svg @@ -0,0 +1,62 @@ + + + +Prism Launcher LogoPrism Launcher Logo19/10/2022Prism LauncherAutiOne, Boba, ely, Fulmine, gon sawa, Pankakes, tobimori, Zekehttps://github.com/PrismLauncher/PrismLauncherCC BY-SA 4.0Prism Launcher diff --git a/launcher/resources/multimc/scalable/instances/dirt.svg b/launcher/resources/multimc/scalable/instances/dirt.svg new file mode 100644 index 000000000..df28ae920 --- /dev/null +++ b/launcher/resources/multimc/scalable/instances/dirt.svg @@ -0,0 +1,52 @@ + + + +Prism Launcher LogoPrism Launcher Logo19/10/2022Prism LauncherAutiOne, Boba, ely, Fulmine, gon sawa, Pankakes, tobimori, Zekehttps://github.com/PrismLauncher/PrismLauncherCC BY-SA 4.0Prism Launcher diff --git a/launcher/resources/multimc/scalable/instances/enderman.svg b/launcher/resources/multimc/scalable/instances/enderman.svg new file mode 100644 index 000000000..29f25a2f2 --- /dev/null +++ b/launcher/resources/multimc/scalable/instances/enderman.svg @@ -0,0 +1,96 @@ + + + +Prism Launcher LogoPrism Launcher Logo19/10/2022Prism LauncherAutiOne, Boba, ely, Fulmine, gon sawa, Pankakes, tobimori, Zekehttps://github.com/PrismLauncher/PrismLauncherCC BY-SA 4.0Prism Launcher diff --git a/launcher/resources/multimc/scalable/instances/enderpearl.svg b/launcher/resources/multimc/scalable/instances/enderpearl.svg new file mode 100644 index 000000000..e4c1e1041 --- /dev/null +++ b/launcher/resources/multimc/scalable/instances/enderpearl.svg @@ -0,0 +1,95 @@ + + + +Prism Launcher LogoPrism Launcher Logo19/10/2022Prism LauncherAutiOne, Boba, ely, Fulmine, gon sawa, Pankakes, tobimori, Zekehttps://github.com/PrismLauncher/PrismLauncherCC BY-SA 4.0Prism Launcher diff --git a/launcher/resources/multimc/scalable/instances/flame.svg b/launcher/resources/multimc/scalable/instances/flame.svg new file mode 100644 index 000000000..775914b89 --- /dev/null +++ b/launcher/resources/multimc/scalable/instances/flame.svg @@ -0,0 +1,49 @@ + + + +Prism Launcher LogoPrism Launcher Logo19/10/2022Prism LauncherAutiOne, Boba, ely, Fulmine, gon sawa, Pankakes, tobimori, Zekehttps://github.com/PrismLauncher/PrismLauncherCC BY-SA 4.0Prism Launcher diff --git a/launcher/resources/multimc/scalable/instances/fox.svg b/launcher/resources/multimc/scalable/instances/fox.svg index fcf16b2fb..95ca6ef91 100644 --- a/launcher/resources/multimc/scalable/instances/fox.svg +++ b/launcher/resources/multimc/scalable/instances/fox.svg @@ -1,290 +1,151 @@ + + - - - - - image/svg+xml - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + viewBox="0 0 24 24" + id="svg168" + xml:space="preserve" + xmlns:xlink="http://www.w3.org/1999/xlink" + xmlns="http://www.w3.org/2000/svg" + xmlns:svg="http://www.w3.org/2000/svg" + xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#" + xmlns:cc="http://creativecommons.org/ns#" + xmlns:dc="http://purl.org/dc/elements/1.1/">Prism Launcher LogoPrism Launcher Logo19/10/2022Prism LauncherAutiOne, Boba, ely, Fulmine, gon sawa, Pankakes, tobimori, Zekehttps://github.com/PrismLauncher/PrismLauncherCC BY-SA 4.0Prism Launcher diff --git a/launcher/resources/multimc/scalable/instances/fox_legacy.svg b/launcher/resources/multimc/scalable/instances/fox_legacy.svg new file mode 100644 index 000000000..fcf16b2fb --- /dev/null +++ b/launcher/resources/multimc/scalable/instances/fox_legacy.svg @@ -0,0 +1,290 @@ + + + + + + + image/svg+xml + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/launcher/resources/multimc/scalable/instances/ftb_logo.svg b/launcher/resources/multimc/scalable/instances/ftb_logo.svg new file mode 100644 index 000000000..85e8295eb --- /dev/null +++ b/launcher/resources/multimc/scalable/instances/ftb_logo.svg @@ -0,0 +1,82 @@ + + + +Prism Launcher LogoPrism Launcher Logo19/10/2022Prism LauncherAutiOne, Boba, ely, Fulmine, gon sawa, Pankakes, tobimori, Zekehttps://github.com/PrismLauncher/PrismLauncherCC BY-SA 4.0Prism Launcher diff --git a/launcher/resources/multimc/scalable/instances/gear.svg b/launcher/resources/multimc/scalable/instances/gear.svg new file mode 100644 index 000000000..b2923d676 --- /dev/null +++ b/launcher/resources/multimc/scalable/instances/gear.svg @@ -0,0 +1,68 @@ + + + +Prism Launcher LogoPrism Launcher Logo19/10/2022Prism LauncherAutiOne, Boba, ely, Fulmine, gon sawa, Pankakes, tobimori, Zekehttps://github.com/PrismLauncher/PrismLauncherCC BY-SA 4.0Prism Launcher diff --git a/launcher/resources/multimc/scalable/instances/gold.svg b/launcher/resources/multimc/scalable/instances/gold.svg new file mode 100644 index 000000000..f1513d70a --- /dev/null +++ b/launcher/resources/multimc/scalable/instances/gold.svg @@ -0,0 +1,63 @@ + + + +Prism Launcher LogoPrism Launcher Logo19/10/2022Prism LauncherAutiOne, Boba, ely, Fulmine, gon sawa, Pankakes, tobimori, Zekehttps://github.com/PrismLauncher/PrismLauncherCC BY-SA 4.0Prism Launcher diff --git a/launcher/resources/multimc/scalable/instances/grass.svg b/launcher/resources/multimc/scalable/instances/grass.svg new file mode 100644 index 000000000..cd29fd832 --- /dev/null +++ b/launcher/resources/multimc/scalable/instances/grass.svg @@ -0,0 +1,84 @@ + + + +Prism Launcher LogoPrism Launcher Logo19/10/2022Prism LauncherAutiOne, Boba, ely, Fulmine, gon sawa, Pankakes, tobimori, Zekehttps://github.com/PrismLauncher/PrismLauncherCC BY-SA 4.0Prism Launcher diff --git a/launcher/resources/multimc/scalable/instances/herobrine.svg b/launcher/resources/multimc/scalable/instances/herobrine.svg new file mode 100644 index 000000000..24f4d2c96 --- /dev/null +++ b/launcher/resources/multimc/scalable/instances/herobrine.svg @@ -0,0 +1,111 @@ + + + +Prism Launcher LogoPrism Launcher Logo19/10/2022Prism LauncherAutiOne, Boba, ely, Fulmine, gon sawa, Pankakes, tobimori, Zekehttps://github.com/PrismLauncher/PrismLauncherCC BY-SA 4.0Prism Launcher diff --git a/launcher/resources/multimc/scalable/instances/iron.svg b/launcher/resources/multimc/scalable/instances/iron.svg new file mode 100644 index 000000000..6a6faf77f --- /dev/null +++ b/launcher/resources/multimc/scalable/instances/iron.svg @@ -0,0 +1,178 @@ + + + +Prism Launcher LogoPrism Launcher Logo19/10/2022Prism LauncherAutiOne, Boba, ely, Fulmine, gon sawa, Pankakes, tobimori, Zekehttps://github.com/PrismLauncher/PrismLauncherCC BY-SA 4.0Prism Launcher diff --git a/launcher/resources/multimc/scalable/instances/magitech.svg b/launcher/resources/multimc/scalable/instances/magitech.svg new file mode 100644 index 000000000..57ef6df16 --- /dev/null +++ b/launcher/resources/multimc/scalable/instances/magitech.svg @@ -0,0 +1,85 @@ + + + +Prism Launcher LogoPrism Launcher Logo19/10/2022Prism LauncherAutiOne, Boba, ely, Fulmine, gon sawa, Pankakes, tobimori, Zekehttps://github.com/PrismLauncher/PrismLauncherCC BY-SA 4.0Prism Launcher diff --git a/launcher/resources/multimc/scalable/instances/meat.svg b/launcher/resources/multimc/scalable/instances/meat.svg new file mode 100644 index 000000000..36f0551b0 --- /dev/null +++ b/launcher/resources/multimc/scalable/instances/meat.svg @@ -0,0 +1,121 @@ + + + +Prism Launcher LogoPrism Launcher Logo19/10/2022Prism LauncherAutiOne, Boba, ely, Fulmine, gon sawa, Pankakes, tobimori, Zekehttps://github.com/PrismLauncher/PrismLauncherCC BY-SA 4.0Prism Launcher diff --git a/launcher/resources/multimc/scalable/instances/modrinth.svg b/launcher/resources/multimc/scalable/instances/modrinth.svg index a40f0e72b..029dc998f 100644 --- a/launcher/resources/multimc/scalable/instances/modrinth.svg +++ b/launcher/resources/multimc/scalable/instances/modrinth.svg @@ -1,4 +1,92 @@ - - - - + + + +Prism Launcher LogoPrism Launcher Logo19/10/2022Prism LauncherAutiOne, Boba, ely, Fulmine, gon sawa, Pankakes, tobimori, Zekehttps://github.com/PrismLauncher/PrismLauncherCC BY-SA 4.0Prism Launcher diff --git a/launcher/resources/multimc/scalable/instances/netherstar.svg b/launcher/resources/multimc/scalable/instances/netherstar.svg new file mode 100644 index 000000000..a5d9606eb --- /dev/null +++ b/launcher/resources/multimc/scalable/instances/netherstar.svg @@ -0,0 +1,81 @@ + + + +Prism Launcher LogoPrism Launcher Logo19/10/2022Prism LauncherAutiOne, Boba, ely, Fulmine, gon sawa, Pankakes, tobimori, Zekehttps://github.com/PrismLauncher/PrismLauncherCC BY-SA 4.0Prism Launcher diff --git a/launcher/resources/multimc/scalable/instances/planks.svg b/launcher/resources/multimc/scalable/instances/planks.svg new file mode 100644 index 000000000..8febfa6bd --- /dev/null +++ b/launcher/resources/multimc/scalable/instances/planks.svg @@ -0,0 +1,93 @@ + + + +Prism Launcher LogoPrism Launcher Logo19/10/2022Prism LauncherAutiOne, Boba, ely, Fulmine, gon sawa, Pankakes, tobimori, Zekehttps://github.com/PrismLauncher/PrismLauncherCC BY-SA 4.0Prism Launcher diff --git a/launcher/resources/multimc/scalable/instances/skeleton.svg b/launcher/resources/multimc/scalable/instances/skeleton.svg new file mode 100644 index 000000000..ca9e8dd4d --- /dev/null +++ b/launcher/resources/multimc/scalable/instances/skeleton.svg @@ -0,0 +1,134 @@ + + + +Prism Launcher LogoPrism Launcher Logo19/10/2022Prism LauncherAutiOne, Boba, ely, Fulmine, gon sawa, Pankakes, tobimori, Zekehttps://github.com/PrismLauncher/PrismLauncherCC BY-SA 4.0Prism Launcher diff --git a/launcher/resources/multimc/scalable/instances/squarecreeper.svg b/launcher/resources/multimc/scalable/instances/squarecreeper.svg new file mode 100644 index 000000000..ddb9aec8c --- /dev/null +++ b/launcher/resources/multimc/scalable/instances/squarecreeper.svg @@ -0,0 +1,81 @@ + + + +Prism Launcher LogoPrism Launcher Logo19/10/2022Prism LauncherAutiOne, Boba, ely, Fulmine, gon sawa, Pankakes, tobimori, Zekehttps://github.com/PrismLauncher/PrismLauncherCC BY-SA 4.0Prism Launcher diff --git a/launcher/resources/multimc/scalable/instances/steve.svg b/launcher/resources/multimc/scalable/instances/steve.svg new file mode 100644 index 000000000..9b6d2595c --- /dev/null +++ b/launcher/resources/multimc/scalable/instances/steve.svg @@ -0,0 +1,154 @@ + + + +Prism Launcher LogoPrism Launcher Logo19/10/2022Prism LauncherAutiOne, Boba, ely, Fulmine, gon sawa, Pankakes, tobimori, Zekehttps://github.com/PrismLauncher/PrismLauncherCC BY-SA 4.0Prism Launcher diff --git a/launcher/resources/multimc/scalable/instances/stone.svg b/launcher/resources/multimc/scalable/instances/stone.svg new file mode 100644 index 000000000..6df534d22 --- /dev/null +++ b/launcher/resources/multimc/scalable/instances/stone.svg @@ -0,0 +1,55 @@ + + + +Prism Launcher LogoPrism Launcher Logo19/10/2022Prism LauncherAutiOne, Boba, ely, Fulmine, gon sawa, Pankakes, tobimori, Zekehttps://github.com/PrismLauncher/PrismLauncherCC BY-SA 4.0Prism Launcher diff --git a/launcher/resources/multimc/scalable/instances/tnt.svg b/launcher/resources/multimc/scalable/instances/tnt.svg new file mode 100644 index 000000000..e876eba30 --- /dev/null +++ b/launcher/resources/multimc/scalable/instances/tnt.svg @@ -0,0 +1,126 @@ + + + +Prism Launcher LogoPrism Launcher Logo19/10/2022Prism LauncherAutiOne, Boba, ely, Fulmine, gon sawa, Pankakes, tobimori, Zekehttps://github.com/PrismLauncher/PrismLauncherCC BY-SA 4.0Prism Launcher diff --git a/launcher/resources/multimc/scalable/launch.svg b/launcher/resources/multimc/scalable/launch.svg new file mode 100644 index 000000000..321647a0b --- /dev/null +++ b/launcher/resources/multimc/scalable/launch.svg @@ -0,0 +1,96 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/launcher/resources/multimc/scalable/rename.svg b/launcher/resources/multimc/scalable/rename.svg new file mode 100644 index 000000000..a585e264b --- /dev/null +++ b/launcher/resources/multimc/scalable/rename.svg @@ -0,0 +1,437 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/launcher/resources/multimc/scalable/server.svg b/launcher/resources/multimc/scalable/server.svg new file mode 100644 index 000000000..c6a957b36 --- /dev/null +++ b/launcher/resources/multimc/scalable/server.svg @@ -0,0 +1,9764 @@ + + + + + + + + + + + + + + + + + + + + + + + +image/svg+xml + + + + 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 +byvXOWum0rOluGXuPT4pS3q2kZkyF8/TndQsijF6W5VXE8dqeuN8bT21Xewa1nt+93tv5mKzZL3L +d1vOXTWxeaF9Ts23ir9Ty59fe1OJcjI+tfxSeZhaVguXUwt7HegL+uIumTrKTyW2NltxWstnN7XV +WtxoQZMPfmiTSxcG/e31YBv3JbnayfzCb5etvkegf51coVFUF/J3s1IcxknB3Th1W9Z+7HzkoOZc +L7G1OzWX7qidY1Zsbjd1sz7zDn/u1uHdO4t1/KjTbq937ttP62en8mr6cgG3FVcKtZBqd5XH9O3n +0bJvpU8fxl5gpZr6tToVVOlb+3k1cQO1iNXSSk+MxZmrVv3Ir9LOzLNmBFW6t3WebVx7KkW14GrT +i7eJ9fLpsV+l7d7r2sJSfGPq1a9SuSBvbwRUqs3M5tbf8xjJPn1N3z3KhaJx7tvT6UJrLX5aO77w +rXR3pnnkqZSuF1zt/MFR+jhogK/aj2X1AFW62De8u9M3qXimOw+vpZt9c7o2C7NPqz2bn/fMarqY +3a3jSmE1vdlipU/tp+u3i4BK114yV5WPVbdSKc5V+7xSPAmsNLv6fjnvX+nG1GK7s5bo+Fd6ln2G +Wih++/ramd+6UwIqzXwuzhvVXf9K04tPiY31H66nOEmcO6ulzMyv1jv2q1QuHJ4bAZVqM/FMLrMd +UOndC3IlbBaLvn2d3p3dnDuqfl75Vrp7nrsOGt69uZWp5Cep1H78LgDG+AGeXuzM7yzjAV7qq3Tv +5Sfzu9SSodJsy1vp0f7JE630bmXB01Mpns0kk/dutUJf7w356Oc861/p/nQvd3RXzvlWelqv7rmV +wryI1R4mf1vLAZU+JOTLo9+ef6WHqeejQmF7yq9SmJfifm0/sK+Xx1rxNahSS76Wn/P+lR6tzBQr +L0vruFIp7u3r9fNmJ7DS64XqSyuo0mP5ZmXb8KtUikO1duJmJ9c2fQf4frn4HFjp13TxyAio9FGT +n0qvCVwpwpinrydXte+1+Mmyb6XPz7cvgZU2q9sLH36VSnFU7bH8ah9Z/gNcuFKmb7vn+36Vttsn +KzO00rfUomfRJPJLdgpXKsWV0nR3V6RKa+1eypRRpct9lZ5uLvw+27c7UOlm21NpYq15t0wr/c4v +uZUCTUbVxt8fpslWrhp3yr5IIC5k++RhF1W60r+nnqzMNlLrZ1Cp2fUOr21/JXGlaF4W7GUPKZyv +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 + + +