diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml
index 2cefa8100..feea08543 100644
--- a/.github/workflows/build.yml
+++ b/.github/workflows/build.yml
@@ -62,7 +62,7 @@ jobs:
qt_version: "5.15.2"
qt_modules: "qtnetworkauth"
- - os: ubuntu-20.04
+ - os: ubuntu-22.04
qt_ver: 6
qt_host: linux
qt_arch: ""
@@ -80,9 +80,9 @@ jobs:
architecture: "x64"
vcvars_arch: "amd64"
qt_ver: 6
- qt_host: windows
- qt_arch: ""
- qt_version: "6.7.3"
+ qt_host: "windows"
+ qt_arch: "win64_msvc2022_64"
+ qt_version: "6.8.1"
qt_modules: "qt5compat qtimageformats qtnetworkauth"
nscurl_tag: "v24.9.26.122"
nscurl_sha256: "AEE6C4BE3CB6455858E9C1EE4B3AFE0DB9960FA03FE99CCDEDC28390D57CCBB0"
@@ -93,9 +93,9 @@ jobs:
architecture: "arm64"
vcvars_arch: "amd64_arm64"
qt_ver: 6
- qt_host: windows
- qt_arch: "win64_msvc2019_arm64"
- qt_version: "6.7.3"
+ qt_host: "windows"
+ qt_arch: "win64_msvc2022_arm64_cross_compiled"
+ qt_version: "6.8.1"
qt_modules: "qt5compat qtimageformats qtnetworkauth"
nscurl_tag: "v24.9.26.122"
nscurl_sha256: "AEE6C4BE3CB6455858E9C1EE4B3AFE0DB9960FA03FE99CCDEDC28390D57CCBB0"
@@ -106,7 +106,7 @@ jobs:
qt_ver: 6
qt_host: mac
qt_arch: ""
- qt_version: "6.7.3"
+ qt_version: "6.8.1"
qt_modules: "qt5compat qtimageformats qtnetworkauth"
- os: macos-14
@@ -167,13 +167,13 @@ jobs:
- name: Setup ccache
if: (runner.os != 'Windows' || matrix.msystem == '') && inputs.build_type == 'Debug'
- uses: hendrikmuhs/ccache-action@v1.2.14
+ uses: hendrikmuhs/ccache-action@v1.2.16
with:
key: ${{ matrix.os }}-qt${{ matrix.qt_ver }}-${{ matrix.architecture }}
- name: Retrieve ccache cache (Windows MinGW-w64)
if: runner.os == 'Windows' && matrix.msystem != '' && inputs.build_type == 'Debug'
- uses: actions/cache@v4.1.2
+ uses: actions/cache@v4.2.0
with:
path: '${{ github.workspace }}\.ccache'
key: ${{ matrix.os }}-mingw-w64-ccache-${{ github.run_id }}
@@ -216,14 +216,14 @@ jobs:
- name: Install host Qt (Windows MSVC arm64)
if: runner.os == 'Windows' && matrix.architecture == 'arm64'
- uses: jurplel/install-qt-action@v3
+ uses: jurplel/install-qt-action@v4
with:
aqtversion: "==3.1.*"
py7zrversion: ">=0.20.2"
version: ${{ matrix.qt_version }}
host: "windows"
target: "desktop"
- arch: ""
+ arch: ${{ matrix.qt_arch }}
modules: ${{ matrix.qt_modules }}
cache: ${{ inputs.is_qt_cached }}
cache-key-prefix: host-qt-arm64-windows
@@ -232,7 +232,7 @@ jobs:
- name: Install Qt (macOS, Linux & Windows MSVC)
if: matrix.msystem == ''
- uses: jurplel/install-qt-action@v3
+ uses: jurplel/install-qt-action@v4
with:
aqtversion: "==3.1.*"
py7zrversion: ">=0.20.2"
@@ -259,12 +259,12 @@ jobs:
wget "https://github.com/AppImageCommunity/AppImageUpdate/releases/download/continuous/AppImageUpdate-x86_64.AppImage"
- sudo apt install libopengl0
+ sudo apt install libopengl0 libfuse2
- 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
+ echo "QT_HOST_PATH=${{ github.workspace }}\HostQt\Qt\${{ matrix.qt_version }}\msvc2022_64" >> $env:GITHUB_ENV
- name: Setup java (macOS)
if: runner.os == 'macOS'
@@ -380,11 +380,13 @@ jobs:
if [ -n '${{ secrets.APPLE_CODESIGN_ID }}' ]; then
APPLE_CODESIGN_ID='${{ secrets.APPLE_CODESIGN_ID }}'
+ ENTITLEMENTS_FILE='../program_info/App.entitlements'
else
APPLE_CODESIGN_ID='-'
+ ENTITLEMENTS_FILE='../program_info/AdhocSignedApp.entitlements'
fi
- sudo codesign --sign "$APPLE_CODESIGN_ID" --deep --force --entitlements "../program_info/App.entitlements" --options runtime "PrismLauncher.app/Contents/MacOS/prismlauncher"
+ sudo codesign --sign "$APPLE_CODESIGN_ID" --deep --force --entitlements "$ENTITLEMENTS_FILE" --options runtime "PrismLauncher.app/Contents/MacOS/prismlauncher"
mv "PrismLauncher.app" "Prism Launcher.app"
- name: Notarize (macOS)
@@ -519,8 +521,8 @@ jobs:
cp -r ${{ runner.workspace }}/Qt/${{ matrix.qt_version }}/gcc_64/plugins/iconengines/* ${{ env.INSTALL_APPIMAGE_DIR }}/usr/plugins/iconengines
- cp /usr/lib/x86_64-linux-gnu/libcrypto.so.1.1 ${{ env.INSTALL_APPIMAGE_DIR }}/usr/lib/
- cp /usr/lib/x86_64-linux-gnu/libssl.so.1.1 ${{ env.INSTALL_APPIMAGE_DIR }}/usr/lib/
+ cp /usr/lib/x86_64-linux-gnu/libcrypto.so.* ${{ env.INSTALL_APPIMAGE_DIR }}/usr/lib/
+ cp /usr/lib/x86_64-linux-gnu/libssl.so.* ${{ env.INSTALL_APPIMAGE_DIR }}/usr/lib/
cp /usr/lib/x86_64-linux-gnu/libOpenGL.so.0* ${{ env.INSTALL_APPIMAGE_DIR }}/usr/lib/
LD_LIBRARY_PATH="${LD_LIBRARY_PATH}:${{ env.INSTALL_APPIMAGE_DIR }}/usr/lib"
@@ -555,9 +557,9 @@ jobs:
mkdir ${{ env.INSTALL_PORTABLE_DIR }}/lib
cp /lib/x86_64-linux-gnu/libbz2.so.1.0 ${{ env.INSTALL_PORTABLE_DIR }}/lib
cp /usr/lib/x86_64-linux-gnu/libgobject-2.0.so.0 ${{ env.INSTALL_PORTABLE_DIR }}/lib
- cp /usr/lib/x86_64-linux-gnu/libcrypto.so.1.1 ${{ env.INSTALL_PORTABLE_DIR }}/lib
- cp /usr/lib/x86_64-linux-gnu/libssl.so.1.1 ${{ env.INSTALL_PORTABLE_DIR }}/lib
- cp /usr/lib/x86_64-linux-gnu/libffi.so.7 ${{ env.INSTALL_PORTABLE_DIR }}/lib
+ cp /usr/lib/x86_64-linux-gnu/libcrypto.so.* ${{ env.INSTALL_PORTABLE_DIR }}/lib
+ cp /usr/lib/x86_64-linux-gnu/libssl.so.* ${{ env.INSTALL_PORTABLE_DIR }}/lib
+ cp /usr/lib/x86_64-linux-gnu/libffi.so.*.* ${{ env.INSTALL_PORTABLE_DIR }}/lib
mv ${{ env.INSTALL_PORTABLE_DIR }}/bin/*.so* ${{ env.INSTALL_PORTABLE_DIR }}/lib
for l in $(find ${{ env.INSTALL_PORTABLE_DIR }} -type f); do l=${l#$(pwd)/}; l=${l#${{ env.INSTALL_PORTABLE_DIR }}/}; l=${l#./}; echo $l; done > ${{ env.INSTALL_PORTABLE_DIR }}/manifest.txt
@@ -631,22 +633,42 @@ jobs:
ccache -s
flatpak:
- runs-on: ubuntu-latest
+ name: Flatpak (${{ matrix.arch }})
+
+ strategy:
+ fail-fast: false
+ matrix:
+ include:
+ - os: ubuntu-22.04
+ arch: x86_64
+
+ - os: ubuntu-22.04-arm
+ arch: aarch64
+
+ runs-on: ${{ matrix.os }}
+
container:
- image: bilelmoussaoui/flatpak-github-actions:kde-6.7
+ image: ghcr.io/flathub-infra/flatpak-github-actions:kde-6.8
options: --privileged
+
steps:
- name: Checkout
uses: actions/checkout@v4
if: inputs.build_type == 'Debug'
with:
- submodules: "true"
+ submodules: true
+
+ - name: Set short version
+ shell: bash
+ run: echo "VERSION=${GITHUB_SHA::7}" >> $GITHUB_ENV
+
- name: Build Flatpak (Linux)
if: inputs.build_type == 'Debug'
uses: flatpak/flatpak-github-actions/flatpak-builder@v6
with:
- bundle: "Prism Launcher.flatpak"
+ bundle: PrismLauncher-${{ runner.os }}-${{ env.VERSION }}-Flatpak.flatpak
manifest-path: flatpak/org.prismlauncher.PrismLauncher.yml
+ arch: ${{ matrix.arch }}
nix:
name: Nix (${{ matrix.system }})
@@ -658,6 +680,9 @@ jobs:
- os: ubuntu-22.04
system: x86_64-linux
+ - os: ubuntu-22.04-arm
+ system: aarch64-linux
+
- os: macos-13
system: x86_64-darwin
diff --git a/CMakeLists.txt b/CMakeLists.txt
index 4063ff503..d61817ecf 100644
--- a/CMakeLists.txt
+++ b/CMakeLists.txt
@@ -78,6 +78,13 @@ else()
# ATL's pack list needs more than the default 1 Mib stack on windows
if(WIN32)
set(CMAKE_EXE_LINKER_FLAGS "-Wl,--stack,8388608 ${CMAKE_EXE_LINKER_FLAGS}")
+
+ # -ffunction-sections and -fdata-sections help reduce binary size
+ # -mguard=cf enables Control Flow Guard
+ # TODO: Look into -gc-sections to further reduce binary size
+ foreach(lang C CXX)
+ set("CMAKE_${lang}_FLAGS_RELEASE" "-ffunction-sections -fdata-sections -mguard=cf")
+ endforeach()
endif()
endif()
@@ -401,8 +408,8 @@ if(UNIX AND APPLE)
set(MACOSX_SPARKLE_UPDATE_PUBLIC_KEY "v55ZWWD6QlPoXGV6VLzOTZxZUggWeE51X8cRQyQh6vA=" CACHE STRING "Public key for Sparkle update feed")
set(MACOSX_SPARKLE_UPDATE_FEED_URL "https://prismlauncher.org/feed/appcast.xml" CACHE STRING "URL for Sparkle update feed")
- set(MACOSX_SPARKLE_DOWNLOAD_URL "https://github.com/sparkle-project/Sparkle/releases/download/2.5.2/Sparkle-2.5.2.tar.xz" CACHE STRING "URL to Sparkle release archive")
- set(MACOSX_SPARKLE_SHA256 "572dd67ae398a466f19f343a449e1890bac1ef74885b4739f68f979a8a89884b" CACHE STRING "SHA256 checksum for Sparkle release archive")
+ set(MACOSX_SPARKLE_DOWNLOAD_URL "https://github.com/sparkle-project/Sparkle/releases/download/2.6.4/Sparkle-2.6.4.tar.xz" CACHE STRING "URL to Sparkle release archive")
+ set(MACOSX_SPARKLE_SHA256 "50612a06038abc931f16011d7903b8326a362c1074dabccb718404ce8e585f0b" CACHE STRING "SHA256 checksum for Sparkle release archive")
set(MACOSX_SPARKLE_DIR "${CMAKE_BINARY_DIR}/frameworks/Sparkle")
# directories to look for dependencies
diff --git a/cmake/MacOSXBundleInfo.plist.in b/cmake/MacOSXBundleInfo.plist.in
index 6d3845dfc..3a8c8fbfe 100644
--- a/cmake/MacOSXBundleInfo.plist.in
+++ b/cmake/MacOSXBundleInfo.plist.in
@@ -8,6 +8,8 @@
A Minecraft mod wants to access your microphone.
NSDownloadsFolderUsageDescription
Prism uses access to your Downloads folder to help you more quickly add mods that can't be automatically downloaded to your instance. You can change where Prism scans for downloaded mods in Settings or the prompt that appears.
+ NSLocalNetworkUsageDescription
+ Minecraft uses the local network to find and connect to LAN servers.
NSPrincipalClass
NSApplication
NSHighResolutionCapable
diff --git a/flake.lock b/flake.lock
index ba9a56c33..01f66b974 100644
--- a/flake.lock
+++ b/flake.lock
@@ -3,11 +3,11 @@
"flake-compat": {
"flake": false,
"locked": {
- "lastModified": 1696426674,
- "narHash": "sha256-kvjfFW7WAETZlt09AgDn1MrtKzP7t90Vf7vypd3OL1U=",
+ "lastModified": 1733328505,
+ "narHash": "sha256-NeCCThCEP3eCl2l/+27kNNK7QrwZB1IJCrXfrbv5oqU=",
"owner": "edolstra",
"repo": "flake-compat",
- "rev": "0f9255e01c2351cc7d116c072cb317785dd33b33",
+ "rev": "ff81ac966bb2cae68946d5ed5fc4994f96d0ffec",
"type": "github"
},
"original": {
@@ -49,11 +49,11 @@
},
"nixpkgs": {
"locked": {
- "lastModified": 1732014248,
- "narHash": "sha256-y/MEyuJ5oBWrWAic/14LaIr/u5E0wRVzyYsouYY3W6w=",
+ "lastModified": 1737062831,
+ "narHash": "sha256-Tbk1MZbtV2s5aG+iM99U8FqwxU/YNArMcWAv6clcsBc=",
"owner": "NixOS",
"repo": "nixpkgs",
- "rev": "23e89b7da85c3640bbc2173fe04f4bd114342367",
+ "rev": "5df43628fdf08d642be8ba5b3625a6c70731c19c",
"type": "github"
},
"original": {
diff --git a/flatpak/org.prismlauncher.PrismLauncher.yml b/flatpak/org.prismlauncher.PrismLauncher.yml
index 7aaa4f379..136aef91a 100644
--- a/flatpak/org.prismlauncher.PrismLauncher.yml
+++ b/flatpak/org.prismlauncher.PrismLauncher.yml
@@ -1,6 +1,6 @@
id: org.prismlauncher.PrismLauncher
runtime: org.kde.Platform
-runtime-version: '6.7'
+runtime-version: '6.8'
sdk: org.kde.Sdk
sdk-extensions:
- org.freedesktop.Sdk.Extension.openjdk17
@@ -75,8 +75,8 @@ modules:
buildsystem: autotools
sources:
- type: archive
- url: https://xorg.freedesktop.org/archive/individual/app/xrandr-1.5.2.tar.xz
- sha256: c8bee4790d9058bacc4b6246456c58021db58a87ddda1a9d0139bf5f18f1f240
+ url: https://xorg.freedesktop.org/archive/individual/app/xrandr-1.5.3.tar.xz
+ sha256: f8dd7566adb74147fab9964680b6bbadee87cf406a7fcff51718a5e6949b841c
x-checker-data:
type: anitya
project-id: 14957
diff --git a/launcher/Application.cpp b/launcher/Application.cpp
index 777de6973..7ae043c16 100644
--- a/launcher/Application.cpp
+++ b/launcher/Application.cpp
@@ -614,6 +614,7 @@ Application::Application(int& argc, char** argv) : QApplication(argc, argv)
m_settings->registerSetting("IconsDir", "icons");
m_settings->registerSetting("DownloadsDir", QStandardPaths::writableLocation(QStandardPaths::DownloadLocation));
m_settings->registerSetting("DownloadsDirWatchRecursive", false);
+ m_settings->registerSetting("MoveModsFromDownloadsDir", false);
m_settings->registerSetting("SkinsDir", "skins");
m_settings->registerSetting("JavaDir", "java");
diff --git a/launcher/CMakeLists.txt b/launcher/CMakeLists.txt
index dedc0f332..a375e0bdf 100644
--- a/launcher/CMakeLists.txt
+++ b/launcher/CMakeLists.txt
@@ -1024,8 +1024,6 @@ SET(LAUNCHER_SOURCES
ui/dialogs/CopyInstanceDialog.h
ui/dialogs/CustomMessageBox.cpp
ui/dialogs/CustomMessageBox.h
- ui/dialogs/EditAccountDialog.cpp
- ui/dialogs/EditAccountDialog.h
ui/dialogs/ExportInstanceDialog.cpp
ui/dialogs/ExportInstanceDialog.h
ui/dialogs/ExportPackDialog.cpp
@@ -1079,8 +1077,6 @@ SET(LAUNCHER_SOURCES
ui/widgets/CustomCommands.h
ui/widgets/EnvironmentVariables.cpp
ui/widgets/EnvironmentVariables.h
- ui/widgets/DropLabel.cpp
- ui/widgets/DropLabel.h
ui/widgets/FocusLineEdit.cpp
ui/widgets/FocusLineEdit.h
ui/widgets/IconLabel.cpp
@@ -1215,7 +1211,6 @@ qt_wrap_ui(LAUNCHER_UI
ui/dialogs/MSALoginDialog.ui
ui/dialogs/OfflineLoginDialog.ui
ui/dialogs/AboutDialog.ui
- ui/dialogs/EditAccountDialog.ui
ui/dialogs/ReviewMessageBox.ui
ui/dialogs/ScrollMessageBox.ui
ui/dialogs/BlockedModsDialog.ui
diff --git a/launcher/icons/IconList.cpp b/launcher/icons/IconList.cpp
index e4157ea2d..f4022e0fb 100644
--- a/launcher/icons/IconList.cpp
+++ b/launcher/icons/IconList.cpp
@@ -47,24 +47,24 @@
#define MAX_SIZE 1024
-IconList::IconList(const QStringList& builtinPaths, QString path, QObject* parent) : QAbstractListModel(parent)
+IconList::IconList(const QStringList& builtinPaths, const QString& path, QObject* parent) : QAbstractListModel(parent)
{
QSet builtinNames;
// add builtin icons
- for (auto& builtinPath : builtinPaths) {
- QDir instance_icons(builtinPath);
- auto file_info_list = instance_icons.entryInfoList(QDir::Files, QDir::Name);
- for (auto file_info : file_info_list) {
- builtinNames.insert(file_info.completeBaseName());
+ for (const auto& builtinPath : builtinPaths) {
+ QDir instanceIcons(builtinPath);
+ auto fileInfoList = instanceIcons.entryInfoList(QDir::Files, QDir::Name);
+ for (const auto& fileInfo : fileInfoList) {
+ builtinNames.insert(fileInfo.baseName());
}
}
- for (auto& builtinName : builtinNames) {
+ for (const auto& builtinName : builtinNames) {
addThemeIcon(builtinName);
}
m_watcher.reset(new QFileSystemWatcher());
- is_watching = false;
+ m_isWatching = false;
connect(m_watcher.get(), &QFileSystemWatcher::directoryChanged, this, &IconList::directoryChanged);
connect(m_watcher.get(), &QFileSystemWatcher::fileChanged, this, &IconList::fileChanged);
@@ -77,91 +77,131 @@ IconList::IconList(const QStringList& builtinPaths, QString path, QObject* paren
void IconList::sortIconList()
{
qDebug() << "Sorting icon list...";
- std::sort(icons.begin(), icons.end(), [](const MMCIcon& a, const MMCIcon& b) { return a.m_key.localeAwareCompare(b.m_key) < 0; });
+ std::sort(m_icons.begin(), m_icons.end(), [](const MMCIcon& a, const MMCIcon& b) {
+ bool aIsSubdir = a.m_key.contains(QDir::separator());
+ bool bIsSubdir = b.m_key.contains(QDir::separator());
+ if (aIsSubdir != bIsSubdir) {
+ return !aIsSubdir; // root-level icons come first
+ }
+ return a.m_key.localeAwareCompare(b.m_key) < 0;
+ });
reindex();
}
+// Helper function to add directories recursively
+bool IconList::addPathRecursively(const QString& path)
+{
+ QDir dir(path);
+ if (!dir.exists())
+ return false;
+
+ // Add the directory itself
+ bool watching = m_watcher->addPath(path);
+
+ // Add all subdirectories
+ QFileInfoList entries = dir.entryInfoList(QDir::Dirs | QDir::NoDotAndDotDot);
+ for (const QFileInfo& entry : entries) {
+ if (addPathRecursively(entry.absoluteFilePath())) {
+ watching = true;
+ }
+ }
+ return watching;
+}
+
+QStringList IconList::getIconFilePaths() const
+{
+ QStringList iconFiles{};
+ QStringList directories{ m_dir.absolutePath() };
+ while (!directories.isEmpty()) {
+ QString first = directories.takeFirst();
+ QDir dir(first);
+ for (QFileInfo& fileInfo : dir.entryInfoList(QDir::AllDirs | QDir::Files | QDir::NoDotAndDotDot, QDir::Name)) {
+ if (fileInfo.isDir())
+ directories.push_back(fileInfo.absoluteFilePath());
+ else
+ iconFiles.push_back(fileInfo.absoluteFilePath());
+ }
+ }
+ return iconFiles;
+}
+
+QString formatName(const QDir& iconsDir, const QFileInfo& iconFile)
+{
+ if (iconFile.dir() == iconsDir)
+ return iconFile.baseName();
+
+ constexpr auto delimiter = " ยป ";
+ QString relativePathWithoutExtension = iconsDir.relativeFilePath(iconFile.dir().path()) + QDir::separator() + iconFile.baseName();
+ return relativePathWithoutExtension.replace(QDir::separator(), delimiter);
+}
+
+/// Split into a separate function because the preprocessing impedes readability
+QSet toStringSet(const QList& list)
+{
+#if QT_VERSION >= QT_VERSION_CHECK(5, 14, 0)
+ QSet set(list.begin(), list.end());
+#else
+ QSet set = list.toSet();
+#endif
+ return set;
+}
+
void IconList::directoryChanged(const QString& path)
{
- QDir new_dir(path);
- if (m_dir.absolutePath() != new_dir.absolutePath()) {
- m_dir.setPath(path);
+ QDir newDir(path);
+ if (m_dir.absolutePath() != newDir.absolutePath()) {
+ if (!path.startsWith(m_dir.absolutePath()))
+ m_dir.setPath(path);
m_dir.refresh();
- if (is_watching)
+ if (m_isWatching)
stopWatching();
startWatching();
}
- if (!m_dir.exists())
- if (!FS::ensureFolderPathExists(m_dir.absolutePath()))
- return;
+ if (!m_dir.exists() && !FS::ensureFolderPathExists(m_dir.absolutePath()))
+ return;
m_dir.refresh();
- auto new_list = m_dir.entryList(QDir::Files, QDir::Name);
- for (auto it = new_list.begin(); it != new_list.end(); it++) {
- QString& foo = (*it);
- foo = m_dir.filePath(foo);
- }
-#if QT_VERSION >= QT_VERSION_CHECK(5, 14, 0)
- QSet new_set(new_list.begin(), new_list.end());
-#else
- auto new_set = new_list.toSet();
-#endif
- QList current_list;
- for (auto& it : icons) {
+ const QStringList newFileNamesList = getIconFilePaths();
+ const QSet newSet = toStringSet(newFileNamesList);
+ QSet currentSet;
+ for (const MMCIcon& it : m_icons) {
if (!it.has(IconType::FileBased))
continue;
- current_list.push_back(it.m_images[IconType::FileBased].filename);
+ currentSet.insert(it.m_images[IconType::FileBased].filename);
}
-#if QT_VERSION >= QT_VERSION_CHECK(5, 14, 0)
- QSet current_set(current_list.begin(), current_list.end());
-#else
- QSet current_set = current_list.toSet();
-#endif
+ QSet toRemove = currentSet - newSet;
+ QSet toAdd = newSet - currentSet;
- QSet to_remove = current_set;
- to_remove -= new_set;
-
- QSet to_add = new_set;
- to_add -= current_set;
-
- for (auto remove : to_remove) {
- qDebug() << "Removing " << remove;
- QFileInfo rmfile(remove);
- QString key = rmfile.completeBaseName();
-
- QString suffix = rmfile.suffix();
- // The icon doesnt have a suffix, but it can have other .s in the name, so we account for those as well
- if (!IconUtils::isIconSuffix(suffix))
- key = rmfile.fileName();
+ for (const QString& removedPath : toRemove) {
+ qDebug() << "Removing icon " << removedPath;
+ QFileInfo removedFile(removedPath);
+ QString key = m_dir.relativeFilePath(removedFile.absoluteFilePath());
int idx = getIconIndex(key);
if (idx == -1)
continue;
- icons[idx].remove(IconType::FileBased);
- if (icons[idx].type() == IconType::ToBeDeleted) {
+ m_icons[idx].remove(FileBased);
+ if (m_icons[idx].type() == ToBeDeleted) {
beginRemoveRows(QModelIndex(), idx, idx);
- icons.remove(idx);
+ m_icons.remove(idx);
reindex();
endRemoveRows();
} else {
dataChanged(index(idx), index(idx));
}
- m_watcher->removePath(remove);
+ m_watcher->removePath(removedPath);
emit iconUpdated(key);
}
- for (auto add : to_add) {
- qDebug() << "Adding " << add;
+ for (const QString& addedPath : toAdd) {
+ qDebug() << "Adding icon " << addedPath;
- QFileInfo addfile(add);
- QString key = addfile.completeBaseName();
+ QFileInfo addfile(addedPath);
+ QString relativePath = m_dir.relativeFilePath(addfile.absoluteFilePath());
+ QString key = QFileInfo(relativePath).completeBaseName();
+ QString name = formatName(m_dir, addfile);
- QString suffix = addfile.suffix();
- // The icon doesnt have a suffix, but it can have other .s in the name, so we account for those as well
- if (!IconUtils::isIconSuffix(suffix))
- key = addfile.fileName();
-
- if (addIcon(key, QString(), addfile.filePath(), IconType::FileBased)) {
- m_watcher->addPath(add);
+ if (addIcon(key, name, addfile.filePath(), IconType::FileBased)) {
+ m_watcher->addPath(addedPath);
emit iconUpdated(key);
}
}
@@ -171,24 +211,24 @@ void IconList::directoryChanged(const QString& path)
void IconList::fileChanged(const QString& path)
{
- qDebug() << "Checking " << path;
+ qDebug() << "Checking icon " << path;
QFileInfo checkfile(path);
if (!checkfile.exists())
return;
- QString key = checkfile.completeBaseName();
+ QString key = m_dir.relativeFilePath(checkfile.absoluteFilePath());
int idx = getIconIndex(key);
if (idx == -1)
return;
QIcon icon(path);
- if (!icon.availableSizes().size())
+ if (icon.availableSizes().empty())
return;
- icons[idx].m_images[IconType::FileBased].icon = icon;
+ m_icons[idx].m_images[IconType::FileBased].icon = icon;
dataChanged(index(idx), index(idx));
emit iconUpdated(key);
}
-void IconList::SettingChanged(const Setting& setting, QVariant value)
+void IconList::SettingChanged(const Setting& setting, const QVariant& value)
{
if (setting.id() != "IconsDir")
return;
@@ -200,8 +240,8 @@ void IconList::startWatching()
{
auto abs_path = m_dir.absolutePath();
FS::ensureFolderPathExists(abs_path);
- is_watching = m_watcher->addPath(abs_path);
- if (is_watching) {
+ m_isWatching = addPathRecursively(abs_path);
+ if (m_isWatching) {
qDebug() << "Started watching " << abs_path;
} else {
qDebug() << "Failed to start watching " << abs_path;
@@ -212,7 +252,7 @@ void IconList::stopWatching()
{
m_watcher->removePaths(m_watcher->files());
m_watcher->removePaths(m_watcher->directories());
- is_watching = false;
+ m_isWatching = false;
}
QStringList IconList::mimeTypes() const
@@ -242,7 +282,7 @@ bool IconList::dropMimeData(const QMimeData* data,
if (data->hasUrls()) {
auto urls = data->urls();
QStringList iconFiles;
- for (auto url : urls) {
+ for (const auto& url : urls) {
// only local files may be dropped...
if (!url.isLocalFile())
continue;
@@ -263,33 +303,33 @@ Qt::ItemFlags IconList::flags(const QModelIndex& index) const
QVariant IconList::data(const QModelIndex& index, int role) const
{
if (!index.isValid())
- return QVariant();
+ return {};
int row = index.row();
- if (row < 0 || row >= icons.size())
- return QVariant();
+ if (row < 0 || row >= m_icons.size())
+ return {};
switch (role) {
case Qt::DecorationRole:
- return icons[row].icon();
+ return m_icons[row].icon();
case Qt::DisplayRole:
- return icons[row].name();
+ return m_icons[row].name();
case Qt::UserRole:
- return icons[row].m_key;
+ return m_icons[row].m_key;
default:
- return QVariant();
+ return {};
}
}
int IconList::rowCount(const QModelIndex& parent) const
{
- return parent.isValid() ? 0 : icons.size();
+ return parent.isValid() ? 0 : m_icons.size();
}
void IconList::installIcons(const QStringList& iconFiles)
{
- for (QString file : iconFiles)
+ for (const QString& file : iconFiles)
installIcon(file, {});
}
@@ -312,12 +352,13 @@ bool IconList::iconFileExists(const QString& key) const
return iconEntry && iconEntry->has(IconType::FileBased);
}
+/// Returns the icon with the given key or nullptr if it doesn't exist.
const MMCIcon* IconList::icon(const QString& key) const
{
int iconIdx = getIconIndex(key);
if (iconIdx == -1)
return nullptr;
- return &icons[iconIdx];
+ return &m_icons[iconIdx];
}
bool IconList::deleteIcon(const QString& key)
@@ -332,22 +373,22 @@ bool IconList::trashIcon(const QString& key)
bool IconList::addThemeIcon(const QString& key)
{
- auto iter = name_index.find(key);
- if (iter != name_index.end()) {
- auto& oldOne = icons[*iter];
+ auto iter = m_nameIndex.find(key);
+ if (iter != m_nameIndex.end()) {
+ auto& oldOne = m_icons[*iter];
oldOne.replace(Builtin, key);
dataChanged(index(*iter), index(*iter));
return true;
}
// add a new icon
- beginInsertRows(QModelIndex(), icons.size(), icons.size());
+ beginInsertRows(QModelIndex(), m_icons.size(), m_icons.size());
{
MMCIcon mmc_icon;
mmc_icon.m_name = key;
mmc_icon.m_key = key;
mmc_icon.replace(Builtin, key);
- icons.push_back(mmc_icon);
- name_index[key] = icons.size() - 1;
+ m_icons.push_back(mmc_icon);
+ m_nameIndex[key] = m_icons.size() - 1;
}
endInsertRows();
return true;
@@ -359,22 +400,22 @@ bool IconList::addIcon(const QString& key, const QString& name, const QString& p
QIcon icon(path);
if (icon.isNull())
return false;
- auto iter = name_index.find(key);
- if (iter != name_index.end()) {
- auto& oldOne = icons[*iter];
+ auto iter = m_nameIndex.find(key);
+ if (iter != m_nameIndex.end()) {
+ auto& oldOne = m_icons[*iter];
oldOne.replace(type, icon, path);
dataChanged(index(*iter), index(*iter));
return true;
}
// add a new icon
- beginInsertRows(QModelIndex(), icons.size(), icons.size());
+ beginInsertRows(QModelIndex(), m_icons.size(), m_icons.size());
{
MMCIcon mmc_icon;
mmc_icon.m_name = name;
mmc_icon.m_key = key;
mmc_icon.replace(type, icon, path);
- icons.push_back(mmc_icon);
- name_index[key] = icons.size() - 1;
+ m_icons.push_back(mmc_icon);
+ m_nameIndex[key] = m_icons.size() - 1;
}
endInsertRows();
return true;
@@ -389,33 +430,32 @@ void IconList::saveIcon(const QString& key, const QString& path, const char* for
void IconList::reindex()
{
- name_index.clear();
- int i = 0;
- for (auto& iter : icons) {
- name_index[iter.m_key] = i;
- i++;
+ m_nameIndex.clear();
+ for (int i = 0; i < m_icons.size(); i++) {
+ m_nameIndex[m_icons[i].m_key] = i;
+ emit iconUpdated(m_icons[i].m_key); // prevents incorrect indices with proxy model
}
}
QIcon IconList::getIcon(const QString& key) const
{
- int icon_index = getIconIndex(key);
+ int iconIndex = getIconIndex(key);
- if (icon_index != -1)
- return icons[icon_index].icon();
+ if (iconIndex != -1)
+ return m_icons[iconIndex].icon();
- // Fallback for icons that don't exist.
- icon_index = getIconIndex("grass");
+ // Fallback for icons that don't exist.b
+ iconIndex = getIconIndex("grass");
- if (icon_index != -1)
- return icons[icon_index].icon();
- return QIcon();
+ if (iconIndex != -1)
+ return m_icons[iconIndex].icon();
+ return {};
}
int IconList::getIconIndex(const QString& key) const
{
- auto iter = name_index.find(key == "default" ? "grass" : key);
- if (iter != name_index.end())
+ auto iter = m_nameIndex.find(key == "default" ? "grass" : key);
+ if (iter != m_nameIndex.end())
return *iter;
return -1;
@@ -425,3 +465,15 @@ QString IconList::getDirectory() const
{
return m_dir.absolutePath();
}
+
+/// Returns the directory of the icon with the given key or the default directory if it's a builtin icon.
+QString IconList::iconDirectory(const QString& key) const
+{
+ for (const auto& mmcIcon : m_icons) {
+ if (mmcIcon.m_key == key && mmcIcon.has(IconType::FileBased)) {
+ QFileInfo iconFile(mmcIcon.getFilePath());
+ return iconFile.dir().path();
+ }
+ }
+ return getDirectory();
+}
diff --git a/launcher/icons/IconList.h b/launcher/icons/IconList.h
index 553946c42..8936195c3 100644
--- a/launcher/icons/IconList.h
+++ b/launcher/icons/IconList.h
@@ -51,7 +51,7 @@ class QFileSystemWatcher;
class IconList : public QAbstractListModel {
Q_OBJECT
public:
- explicit IconList(const QStringList& builtinPaths, QString path, QObject* parent = 0);
+ explicit IconList(const QStringList& builtinPaths, const QString& path, QObject* parent = 0);
virtual ~IconList() {};
QIcon getIcon(const QString& key) const;
@@ -72,6 +72,7 @@ class IconList : public QAbstractListModel {
bool deleteIcon(const QString& key);
bool trashIcon(const QString& key);
bool iconFileExists(const QString& key) const;
+ QString iconDirectory(const QString& key) const;
void installIcons(const QStringList& iconFiles);
void installIcon(const QString& file, const QString& name);
@@ -91,18 +92,20 @@ class IconList : public QAbstractListModel {
IconList& operator=(const IconList&) = delete;
void reindex();
void sortIconList();
+ bool addPathRecursively(const QString& path);
+ QStringList getIconFilePaths() const;
public slots:
void directoryChanged(const QString& path);
protected slots:
void fileChanged(const QString& path);
- void SettingChanged(const Setting& setting, QVariant value);
+ void SettingChanged(const Setting& setting, const QVariant& value);
private:
shared_qobject_ptr m_watcher;
- bool is_watching;
- QMap name_index;
- QVector icons;
+ bool m_isWatching;
+ QMap m_nameIndex;
+ QVector m_icons;
QDir m_dir;
};
diff --git a/launcher/java/download/ManifestDownloadTask.cpp b/launcher/java/download/ManifestDownloadTask.cpp
index 836afeaac..20b39e751 100644
--- a/launcher/java/download/ManifestDownloadTask.cpp
+++ b/launcher/java/download/ManifestDownloadTask.cpp
@@ -86,11 +86,10 @@ void ManifestDownloadTask::downloadJava(const QJsonDocument& doc)
if (type == "directory") {
FS::ensureFolderPathExists(file);
} else if (type == "link") {
- // this is linux only !
+ // this is *nix only !
auto path = Json::ensureString(meta, "target");
if (!path.isEmpty()) {
- auto target = FS::PathCombine(file, "../" + path);
- QFile(target).link(file);
+ QFile::link(path, file);
}
} else if (type == "file") {
// TODO download compressed version if it exists ?
diff --git a/launcher/launch/LaunchTask.cpp b/launcher/launch/LaunchTask.cpp
index 0251b302d..4b93d2077 100644
--- a/launcher/launch/LaunchTask.cpp
+++ b/launcher/launch/LaunchTask.cpp
@@ -254,20 +254,60 @@ void LaunchTask::emitFailed(QString reason)
Task::emitFailed(reason);
}
-void LaunchTask::substituteVariables(QStringList& args) const
+QString expandVariables(const QString& input, QProcessEnvironment dict)
{
- auto env = m_instance->createEnvironment();
+ QString result = input;
- for (auto key : env.keys()) {
- args.replaceInStrings("$" + key, env.value(key));
+ enum { base, maybeBrace, variable, brace } state = base;
+ int startIdx = -1;
+ for (int i = 0; i < result.length();) {
+ QChar c = result.at(i++);
+ switch (state) {
+ case base:
+ if (c == '$')
+ state = maybeBrace;
+ break;
+ case maybeBrace:
+ if (c == '{') {
+ state = brace;
+ startIdx = i;
+ } else if (c.isLetterOrNumber() || c == '_') {
+ state = variable;
+ startIdx = i - 1;
+ } else {
+ state = base;
+ }
+ break;
+ case brace:
+ if (c == '}') {
+ const auto res = dict.value(result.mid(startIdx, i - 1 - startIdx), "");
+ if (!res.isEmpty()) {
+ result.replace(startIdx - 2, i - startIdx + 2, res);
+ i = startIdx - 2 + res.length();
+ }
+ state = base;
+ }
+ break;
+ case variable:
+ if (!c.isLetterOrNumber() && c != '_') {
+ const auto res = dict.value(result.mid(startIdx, i - startIdx - 1), "");
+ if (!res.isEmpty()) {
+ result.replace(startIdx - 1, i - startIdx, res);
+ i = startIdx - 1 + res.length();
+ }
+ state = base;
+ }
+ break;
+ }
}
+ if (state == variable) {
+ if (const auto res = dict.value(result.mid(startIdx), ""); !res.isEmpty())
+ result.replace(startIdx - 1, result.length() - startIdx + 1, res);
+ }
+ return result;
}
-void LaunchTask::substituteVariables(QString& cmd) const
+QString LaunchTask::substituteVariables(QString& cmd, bool isLaunch) const
{
- auto env = m_instance->createEnvironment();
-
- for (auto key : env.keys()) {
- cmd.replace("$" + key, env.value(key));
- }
+ return expandVariables(cmd, isLaunch ? m_instance->createLaunchEnvironment() : m_instance->createEnvironment());
}
diff --git a/launcher/launch/LaunchTask.h b/launcher/launch/LaunchTask.h
index 56065af5b..2e87ece95 100644
--- a/launcher/launch/LaunchTask.h
+++ b/launcher/launch/LaunchTask.h
@@ -87,8 +87,7 @@ class LaunchTask : public Task {
shared_qobject_ptr getLogModel();
public:
- void substituteVariables(QStringList& args) const;
- void substituteVariables(QString& cmd) const;
+ QString substituteVariables(QString& cmd, bool isLaunch = false) const;
QString censorPrivateInfo(QString in);
protected: /* methods */
diff --git a/launcher/launch/steps/PostLaunchCommand.cpp b/launcher/launch/steps/PostLaunchCommand.cpp
index b3985bbac..5d893c71f 100644
--- a/launcher/launch/steps/PostLaunchCommand.cpp
+++ b/launcher/launch/steps/PostLaunchCommand.cpp
@@ -47,19 +47,15 @@ PostLaunchCommand::PostLaunchCommand(LaunchTask* parent) : LaunchStep(parent)
void PostLaunchCommand::executeTask()
{
- // FIXME: where to put this?
+ auto cmd = m_parent->substituteVariables(m_command);
+ emit logLine(tr("Running Post-Launch command: %1").arg(cmd), MessageLevel::Launcher);
#if QT_VERSION >= QT_VERSION_CHECK(5, 15, 0)
- auto args = QProcess::splitCommand(m_command);
- m_parent->substituteVariables(args);
+ auto args = QProcess::splitCommand(cmd);
- emit logLine(tr("Running Post-Launch command: %1").arg(args.join(' ')), MessageLevel::Launcher);
const QString program = args.takeFirst();
m_process.start(program, args);
#else
- m_parent->substituteVariables(m_command);
-
- emit logLine(tr("Running Post-Launch command: %1").arg(m_command), MessageLevel::Launcher);
- m_process.start(m_command);
+ m_process.start(cmd);
#endif
}
diff --git a/launcher/launch/steps/PreLaunchCommand.cpp b/launcher/launch/steps/PreLaunchCommand.cpp
index 0c22d5c16..318237e99 100644
--- a/launcher/launch/steps/PreLaunchCommand.cpp
+++ b/launcher/launch/steps/PreLaunchCommand.cpp
@@ -47,19 +47,14 @@ PreLaunchCommand::PreLaunchCommand(LaunchTask* parent) : LaunchStep(parent)
void PreLaunchCommand::executeTask()
{
- // FIXME: where to put this?
+ auto cmd = m_parent->substituteVariables(m_command);
+ emit logLine(tr("Running Pre-Launch command: %1").arg(cmd), MessageLevel::Launcher);
#if QT_VERSION >= QT_VERSION_CHECK(5, 15, 0)
- auto args = QProcess::splitCommand(m_command);
- m_parent->substituteVariables(args);
-
- emit logLine(tr("Running Pre-Launch command: %1").arg(args.join(' ')), MessageLevel::Launcher);
+ auto args = QProcess::splitCommand(cmd);
const QString program = args.takeFirst();
m_process.start(program, args);
#else
- m_parent->substituteVariables(m_command);
-
- emit logLine(tr("Running Pre-Launch command: %1").arg(m_command), MessageLevel::Launcher);
- m_process.start(m_command);
+ m_process.start(cmd);
#endif
}
diff --git a/launcher/minecraft/MinecraftInstance.cpp b/launcher/minecraft/MinecraftInstance.cpp
index cb669a7f4..3ce563a4e 100644
--- a/launcher/minecraft/MinecraftInstance.cpp
+++ b/launcher/minecraft/MinecraftInstance.cpp
@@ -594,6 +594,13 @@ QMap MinecraftInstance::getVariables()
out.insert("INST_JAVA", settings()->get("JavaPath").toString());
out.insert("INST_JAVA_ARGS", javaArguments().join(' '));
out.insert("NO_COLOR", "1");
+#ifdef Q_OS_MACOS
+ // get library for Steam overlay support
+ QString steamDyldInsertLibraries = qEnvironmentVariable("STEAM_DYLD_INSERT_LIBRARIES");
+ if (!steamDyldInsertLibraries.isEmpty()) {
+ out.insert("DYLD_INSERT_LIBRARIES", steamDyldInsertLibraries);
+ }
+#endif
return out;
}
@@ -1151,13 +1158,6 @@ shared_qobject_ptr MinecraftInstance::createLaunchTask(AuthSessionPt
process->appendStep(step);
}
- // run pre-launch command if that's needed
- if (getPreLaunchCommand().size()) {
- auto step = makeShared(pptr);
- step->setWorkingDirectory(gameRoot());
- process->appendStep(step);
- }
-
// load meta
{
auto mode = session->status != AuthSession::PlayableOffline ? Net::Mode::Online : Net::Mode::Offline;
@@ -1170,6 +1170,13 @@ shared_qobject_ptr MinecraftInstance::createLaunchTask(AuthSessionPt
process->appendStep(makeShared(pptr));
}
+ // run pre-launch command if that's needed
+ if (getPreLaunchCommand().size()) {
+ auto step = makeShared(pptr);
+ step->setWorkingDirectory(gameRoot());
+ process->appendStep(step);
+ }
+
// if we aren't in offline mode,.
if (session->status != AuthSession::PlayableOffline) {
if (!session->demo) {
diff --git a/launcher/minecraft/MinecraftLoadAndCheck.cpp b/launcher/minecraft/MinecraftLoadAndCheck.cpp
index b9fb7eb0c..c0a82e61e 100644
--- a/launcher/minecraft/MinecraftLoadAndCheck.cpp
+++ b/launcher/minecraft/MinecraftLoadAndCheck.cpp
@@ -8,7 +8,10 @@ void MinecraftLoadAndCheck::executeTask()
{
// add offline metadata load task
auto components = m_inst->getPackProfile();
- components->reload(m_netmode);
+ if (auto result = components->reload(m_netmode); !result) {
+ emitFailed(result.error);
+ return;
+ }
m_task = components->getCurrentTask();
if (!m_task) {
diff --git a/launcher/minecraft/PackProfile.cpp b/launcher/minecraft/PackProfile.cpp
index 1acc87166..d6534b910 100644
--- a/launcher/minecraft/PackProfile.cpp
+++ b/launcher/minecraft/PackProfile.cpp
@@ -173,29 +173,32 @@ static bool savePackProfile(const QString& filename, const ComponentContainer& c
}
// Read the given file into component containers
-static bool loadPackProfile(PackProfile* parent,
- const QString& filename,
- const QString& componentJsonPattern,
- ComponentContainer& container)
+static PackProfile::Result loadPackProfile(PackProfile* parent,
+ const QString& filename,
+ const QString& componentJsonPattern,
+ ComponentContainer& container)
{
QFile componentsFile(filename);
if (!componentsFile.exists()) {
- qCWarning(instanceProfileC) << "Components file" << filename << "doesn't exist. This should never happen.";
- return false;
+ auto message = QObject::tr("Components file %1 doesn't exist. This should never happen.").arg(filename);
+ qCWarning(instanceProfileC) << message;
+ return PackProfile::Result::Error(message);
}
if (!componentsFile.open(QFile::ReadOnly)) {
- qCCritical(instanceProfileC) << "Couldn't open" << componentsFile.fileName() << " for reading:" << componentsFile.errorString();
+ auto message = QObject::tr("Couldn't open %1 for reading: %2").arg(componentsFile.fileName(), componentsFile.errorString());
+ qCCritical(instanceProfileC) << message;
qCWarning(instanceProfileC) << "Ignoring overridden order";
- return false;
+ return PackProfile::Result::Error(message);
}
// and it's valid JSON
QJsonParseError error;
QJsonDocument doc = QJsonDocument::fromJson(componentsFile.readAll(), &error);
if (error.error != QJsonParseError::NoError) {
- qCCritical(instanceProfileC) << "Couldn't parse" << componentsFile.fileName() << ":" << error.errorString();
+ auto message = QObject::tr("Couldn't parse %1 as json: %2").arg(componentsFile.fileName(), error.errorString());
+ qCCritical(instanceProfileC) << message;
qCWarning(instanceProfileC) << "Ignoring overridden order";
- return false;
+ return PackProfile::Result::Error(message);
}
// and then read it and process it if all above is true.
@@ -212,11 +215,13 @@ static bool loadPackProfile(PackProfile* parent,
container.append(componentFromJsonV1(parent, componentJsonPattern, comp_obj));
}
} catch ([[maybe_unused]] const JSONValidationError& err) {
- qCCritical(instanceProfileC) << "Couldn't parse" << componentsFile.fileName() << ": bad file format";
+ auto message = QObject::tr("Couldn't parse %1 : bad file format").arg(componentsFile.fileName());
+ qCCritical(instanceProfileC) << message;
+ qCWarning(instanceProfileC) << "error:" << err.what();
container.clear();
- return false;
+ return PackProfile::Result::Error(message);
}
- return true;
+ return PackProfile::Result::Success();
}
// END: component file format
@@ -283,44 +288,43 @@ void PackProfile::save_internal()
d->dirty = false;
}
-bool PackProfile::load()
+PackProfile::Result PackProfile::load()
{
auto filename = componentsFilePath();
// load the new component list and swap it with the current one...
ComponentContainer newComponents;
- if (!loadPackProfile(this, filename, patchesPattern(), newComponents)) {
+ if (auto result = loadPackProfile(this, filename, patchesPattern(), newComponents); !result) {
qCritical() << d->m_instance->name() << "|" << "Failed to load the component config";
- return false;
- } else {
- // FIXME: actually use fine-grained updates, not this...
- beginResetModel();
- // disconnect all the old components
- for (auto component : d->components) {
- disconnect(component.get(), &Component::dataChanged, this, &PackProfile::componentDataChanged);
- }
- d->components.clear();
- d->componentIndex.clear();
- for (auto component : newComponents) {
- if (d->componentIndex.contains(component->m_uid)) {
- qWarning() << d->m_instance->name() << "|" << "Ignoring duplicate component entry" << component->m_uid;
- continue;
- }
- connect(component.get(), &Component::dataChanged, this, &PackProfile::componentDataChanged);
- d->components.append(component);
- d->componentIndex[component->m_uid] = component;
- }
- endResetModel();
- d->loaded = true;
- return true;
+ return result;
}
+ // FIXME: actually use fine-grained updates, not this...
+ beginResetModel();
+ // disconnect all the old components
+ for (auto component : d->components) {
+ disconnect(component.get(), &Component::dataChanged, this, &PackProfile::componentDataChanged);
+ }
+ d->components.clear();
+ d->componentIndex.clear();
+ for (auto component : newComponents) {
+ if (d->componentIndex.contains(component->m_uid)) {
+ qWarning() << d->m_instance->name() << "|" << "Ignoring duplicate component entry" << component->m_uid;
+ continue;
+ }
+ connect(component.get(), &Component::dataChanged, this, &PackProfile::componentDataChanged);
+ d->components.append(component);
+ d->componentIndex[component->m_uid] = component;
+ }
+ endResetModel();
+ d->loaded = true;
+ return Result::Success();
}
-void PackProfile::reload(Net::Mode netmode)
+PackProfile::Result PackProfile::reload(Net::Mode netmode)
{
// Do not reload when the update/resolve task is running. It is in control.
if (d->m_updateTask) {
- return;
+ return Result::Success();
}
// flush any scheduled saves to not lose state
@@ -329,9 +333,11 @@ void PackProfile::reload(Net::Mode netmode)
// FIXME: differentiate when a reapply is required by propagating state from components
invalidateLaunchProfile();
- if (load()) {
- resolve(netmode);
+ if (auto result = load(); !result) {
+ return result;
}
+ resolve(netmode);
+ return Result::Success();
}
Task::Ptr PackProfile::getCurrentTask()
diff --git a/launcher/minecraft/PackProfile.h b/launcher/minecraft/PackProfile.h
index b2de26ea0..d812dfa48 100644
--- a/launcher/minecraft/PackProfile.h
+++ b/launcher/minecraft/PackProfile.h
@@ -62,6 +62,19 @@ class PackProfile : public QAbstractListModel {
public:
enum Columns { NameColumn = 0, VersionColumn, NUM_COLUMNS };
+ struct Result {
+ bool success;
+ QString error;
+
+ // Implicit conversion to bool
+ operator bool() const { return success; }
+
+ // Factory methods for convenience
+ static Result Success() { return { true, "" }; }
+
+ static Result Error(const QString& errorMessage) { return { false, errorMessage }; }
+ };
+
explicit PackProfile(MinecraftInstance* instance);
virtual ~PackProfile();
@@ -102,7 +115,7 @@ class PackProfile : public QAbstractListModel {
bool revertToBase(int index);
/// reload the list, reload all components, resolve dependencies
- void reload(Net::Mode netmode);
+ Result reload(Net::Mode netmode);
// reload all components, resolve dependencies
void resolve(Net::Mode netmode);
@@ -169,7 +182,7 @@ class PackProfile : public QAbstractListModel {
void disableInteraction(bool disable);
private:
- bool load();
+ Result load();
bool installJarMods_internal(QStringList filepaths);
bool installCustomJar_internal(QString filepath);
bool installAgents_internal(QStringList filepaths);
diff --git a/launcher/minecraft/launch/LauncherPartLaunch.cpp b/launcher/minecraft/launch/LauncherPartLaunch.cpp
index 7f0edbdab..7a086cf84 100644
--- a/launcher/minecraft/launch/LauncherPartLaunch.cpp
+++ b/launcher/minecraft/launch/LauncherPartLaunch.cpp
@@ -131,6 +131,7 @@ void LauncherPartLaunch::executeTask()
QString wrapperCommandStr = instance->getWrapperCommand().trimmed();
if (!wrapperCommandStr.isEmpty()) {
+ wrapperCommandStr = m_parent->substituteVariables(wrapperCommandStr);
auto wrapperArgs = Commandline::splitArgs(wrapperCommandStr);
auto wrapperCommand = wrapperArgs.takeFirst();
auto realWrapperCommand = QStandardPaths::findExecutable(wrapperCommand);
diff --git a/launcher/minecraft/mod/Mod.cpp b/launcher/minecraft/mod/Mod.cpp
index 4986d8ee1..50fb45d77 100644
--- a/launcher/minecraft/mod/Mod.cpp
+++ b/launcher/minecraft/mod/Mod.cpp
@@ -48,6 +48,7 @@
#include "Version.h"
#include "minecraft/mod/ModDetails.h"
#include "minecraft/mod/tasks/LocalModParseTask.h"
+#include "modplatform/ModIndex.h"
Mod::Mod(const QFileInfo& file) : Resource(file), m_local_details()
{
@@ -157,11 +158,8 @@ auto Mod::loaders() const -> QString
if (metadata()) {
QStringList loaders;
auto modLoaders = metadata()->loaders;
- for (auto loader : { ModPlatform::NeoForge, ModPlatform::Forge, ModPlatform::Cauldron, ModPlatform::LiteLoader, ModPlatform::Fabric,
- ModPlatform::Quilt }) {
- if (modLoaders & loader) {
- loaders << getModLoaderAsString(loader);
- }
+ for (auto loader : ModPlatform::modLoaderTypesToList(modLoaders)) {
+ loaders << getModLoaderAsString(loader);
}
return loaders.join(", ");
}
diff --git a/launcher/modplatform/EnsureMetadataTask.cpp b/launcher/modplatform/EnsureMetadataTask.cpp
index 9d8d75b09..8e910e984 100644
--- a/launcher/modplatform/EnsureMetadataTask.cpp
+++ b/launcher/modplatform/EnsureMetadataTask.cpp
@@ -19,27 +19,28 @@ static ModrinthAPI modrinth_api;
static FlameAPI flame_api;
EnsureMetadataTask::EnsureMetadataTask(Resource* resource, QDir dir, ModPlatform::ResourceProvider prov)
- : Task(), m_index_dir(dir), m_provider(prov), m_hashing_task(nullptr), m_current_task(nullptr)
+ : Task(), m_index_dir(dir), m_provider(prov), m_hashingTask(nullptr), m_current_task(nullptr)
{
- auto hash_task = createNewHash(resource);
- if (!hash_task)
+ auto hashTask = createNewHash(resource);
+ if (!hashTask)
return;
- connect(hash_task.get(), &Hashing::Hasher::resultsReady, [this, resource](QString hash) { m_resources.insert(hash, resource); });
- connect(hash_task.get(), &Task::failed, [this, resource] { emitFail(resource, "", RemoveFromList::No); });
- hash_task->start();
+ connect(hashTask.get(), &Hashing::Hasher::resultsReady, [this, resource](QString hash) { m_resources.insert(hash, resource); });
+ connect(hashTask.get(), &Task::failed, [this, resource] { emitFail(resource, "", RemoveFromList::No); });
+ m_hashingTask = hashTask;
}
EnsureMetadataTask::EnsureMetadataTask(QList& resources, QDir dir, ModPlatform::ResourceProvider prov)
: Task(), m_index_dir(dir), m_provider(prov), m_current_task(nullptr)
{
- m_hashing_task.reset(new ConcurrentTask("MakeHashesTask", APPLICATION->settings()->get("NumberOfConcurrentTasks").toInt()));
+ auto hashTask = makeShared("MakeHashesTask", APPLICATION->settings()->get("NumberOfConcurrentTasks").toInt());
+ m_hashingTask = hashTask;
for (auto* resource : resources) {
auto hash_task = createNewHash(resource);
if (!hash_task)
continue;
connect(hash_task.get(), &Hashing::Hasher::resultsReady, [this, resource](QString hash) { m_resources.insert(hash, resource); });
connect(hash_task.get(), &Task::failed, [this, resource] { emitFail(resource, "", RemoveFromList::No); });
- m_hashing_task->addTask(hash_task);
+ hashTask->addTask(hash_task);
}
}
diff --git a/launcher/modplatform/EnsureMetadataTask.h b/launcher/modplatform/EnsureMetadataTask.h
index a78ffc0c5..4e5787841 100644
--- a/launcher/modplatform/EnsureMetadataTask.h
+++ b/launcher/modplatform/EnsureMetadataTask.h
@@ -21,7 +21,7 @@ class EnsureMetadataTask : public Task {
~EnsureMetadataTask() = default;
- Task::Ptr getHashingTask() { return m_hashing_task; }
+ Task::Ptr getHashingTask() { return m_hashingTask; }
public slots:
bool abort() override;
@@ -59,6 +59,6 @@ class EnsureMetadataTask : public Task {
ModPlatform::ResourceProvider m_provider;
QHash m_temp_versions;
- ConcurrentTask::Ptr m_hashing_task;
+ Task::Ptr m_hashingTask;
Task::Ptr m_current_task;
};
diff --git a/launcher/modplatform/ModIndex.cpp b/launcher/modplatform/ModIndex.cpp
index 8c85ae122..c3ecccf8e 100644
--- a/launcher/modplatform/ModIndex.cpp
+++ b/launcher/modplatform/ModIndex.cpp
@@ -31,6 +31,19 @@ static const QMap s_indexed_version_ty
{ "alpha", IndexedVersionType::VersionType::Alpha }
};
+static const QList loaderList = { NeoForge, Forge, Cauldron, LiteLoader, Quilt, Fabric };
+
+QList modLoaderTypesToList(ModLoaderTypes flags)
+{
+ QList flagList;
+ for (auto flag : loaderList) {
+ if (flags.testFlag(flag)) {
+ flagList.append(flag);
+ }
+ }
+ return flagList;
+}
+
IndexedVersionType::IndexedVersionType(const QString& type) : IndexedVersionType(enumFromString(type)) {}
IndexedVersionType::IndexedVersionType(const IndexedVersionType::VersionType& type)
diff --git a/launcher/modplatform/ModIndex.h b/launcher/modplatform/ModIndex.h
index d5ee12473..8fae1bf6c 100644
--- a/launcher/modplatform/ModIndex.h
+++ b/launcher/modplatform/ModIndex.h
@@ -32,6 +32,7 @@ namespace ModPlatform {
enum ModLoaderType { NeoForge = 1 << 0, Forge = 1 << 1, Cauldron = 1 << 2, LiteLoader = 1 << 3, Fabric = 1 << 4, Quilt = 1 << 5 };
Q_DECLARE_FLAGS(ModLoaderTypes, ModLoaderType)
+QList modLoaderTypesToList(ModLoaderTypes flags);
enum class ResourceProvider { MODRINTH, FLAME };
diff --git a/launcher/modplatform/flame/FileResolvingTask.cpp b/launcher/modplatform/flame/FileResolvingTask.cpp
index 4c2f3d69e..7ff38d57e 100644
--- a/launcher/modplatform/flame/FileResolvingTask.cpp
+++ b/launcher/modplatform/flame/FileResolvingTask.cpp
@@ -87,6 +87,30 @@ void Flame::FileResolvingTask::executeTask()
m_task->start();
}
+PackedResourceType getResourceType(int classId)
+{
+ switch (classId) {
+ case 17: // Worlds
+ return PackedResourceType::WorldSave;
+ case 6: // Mods
+ return PackedResourceType::Mod;
+ case 12: // Resource Packs
+ // return PackedResourceType::ResourcePack; // not really a resourcepack
+ /* fallthrough */
+ case 4546: // Customization
+ // return PackedResourceType::ShaderPack; // not really a shaderPack
+ /* fallthrough */
+ case 4471: // Modpacks
+ /* fallthrough */
+ case 5: // Bukkit Plugins
+ /* fallthrough */
+ case 4559: // Addons
+ /* fallthrough */
+ default:
+ return PackedResourceType::UNKNOWN;
+ }
+}
+
void Flame::FileResolvingTask::netJobFinished()
{
setProgress(1, 3);
@@ -144,7 +168,7 @@ void Flame::FileResolvingTask::netJobFinished()
<< " reason: " << parse_error.errorString();
qWarning() << *m_result;
- failed(parse_error.errorString());
+ getFlameProjects();
return;
}
@@ -232,6 +256,10 @@ void Flame::FileResolvingTask::getFlameProjects()
setStatus(tr("Parsing API response from CurseForge for '%1'...").arg(file->version.fileName));
FlameMod::loadIndexedPack(file->pack, entry_obj);
+ file->resourceType = getResourceType(Json::requireInteger(entry_obj, "classId", "modClassId"));
+ if (file->resourceType == PackedResourceType::WorldSave) {
+ file->targetFolder = "saves";
+ }
}
} catch (Json::JsonException& e) {
qDebug() << e.cause();
diff --git a/launcher/modplatform/flame/FlameAPI.cpp b/launcher/modplatform/flame/FlameAPI.cpp
index ddd48c2b1..699eb792a 100644
--- a/launcher/modplatform/flame/FlameAPI.cpp
+++ b/launcher/modplatform/flame/FlameAPI.cpp
@@ -270,21 +270,44 @@ std::optional FlameAPI::getLatestVersion(QList instanceLoaders,
ModPlatform::ModLoaderTypes modLoaders)
{
- // edge case: mod has installed for forge but the instance is fabric => fabric version will be prioritizated on update
- auto bestVersion = [&versions](ModPlatform::ModLoaderTypes loader) {
- std::optional ver;
- for (auto file_tmp : versions) {
- if (file_tmp.loaders & loader && (!ver.has_value() || file_tmp.date > ver->date)) {
- ver = file_tmp;
+ static const auto noLoader = ModPlatform::ModLoaderType(0);
+ QHash bestMatch;
+ auto checkVersion = [&bestMatch](const ModPlatform::IndexedVersion& version, const ModPlatform::ModLoaderType& loader) {
+ if (bestMatch.contains(loader)) {
+ auto best = bestMatch.value(loader);
+ if (version.date > best.date) {
+ bestMatch[loader] = version;
+ }
+ } else {
+ bestMatch[loader] = version;
+ }
+ };
+ for (auto file_tmp : versions) {
+ auto loaders = ModPlatform::modLoaderTypesToList(file_tmp.loaders);
+ if (loaders.isEmpty()) {
+ checkVersion(file_tmp, noLoader);
+ } else {
+ for (auto loader : loaders) {
+ checkVersion(file_tmp, loader);
}
}
- return ver;
- };
- for (auto l : instanceLoaders) {
- auto ver = bestVersion(l);
- if (ver.has_value()) {
- return ver;
+ }
+ // edge case: mod has installed for forge but the instance is fabric => fabric version will be prioritizated on update
+ auto currentLoaders = instanceLoaders + ModPlatform::modLoaderTypesToList(modLoaders);
+ currentLoaders.append(noLoader); // add a fallback in case the versions do not define a loader
+
+ for (auto loader : currentLoaders) {
+ if (bestMatch.contains(loader)) {
+ auto bestForLoader = bestMatch.value(loader);
+ // awkward case where the mod has only two loaders and one of them is not specified
+ if (loader != noLoader && bestMatch.contains(noLoader) && bestMatch.size() == 2) {
+ auto bestForNoLoader = bestMatch.value(noLoader);
+ if (bestForNoLoader.date > bestForLoader.date) {
+ return bestForNoLoader;
+ }
+ }
+ return bestForLoader;
}
}
- return bestVersion(modLoaders);
+ return {};
}
diff --git a/launcher/modplatform/flame/FlameCheckUpdate.h b/launcher/modplatform/flame/FlameCheckUpdate.h
index 2b5c6ac17..6543a0e04 100644
--- a/launcher/modplatform/flame/FlameCheckUpdate.h
+++ b/launcher/modplatform/flame/FlameCheckUpdate.h
@@ -1,6 +1,5 @@
#pragma once
-#include "Application.h"
#include "modplatform/CheckUpdateTask.h"
#include "net/NetJob.h"
diff --git a/launcher/modplatform/flame/FlameInstanceCreationTask.cpp b/launcher/modplatform/flame/FlameInstanceCreationTask.cpp
index 3fbe1aced..6073e90a2 100644
--- a/launcher/modplatform/flame/FlameInstanceCreationTask.cpp
+++ b/launcher/modplatform/flame/FlameInstanceCreationTask.cpp
@@ -75,12 +75,12 @@ bool FlameCreationTask::abort()
return false;
m_abort = true;
- if (m_process_update_file_info_job)
- m_process_update_file_info_job->abort();
- if (m_files_job)
- m_files_job->abort();
- if (m_mod_id_resolver)
- m_mod_id_resolver->abort();
+ if (m_processUpdateFileInfoJob)
+ m_processUpdateFileInfoJob->abort();
+ if (m_filesJob)
+ m_filesJob->abort();
+ if (m_modIdResolver)
+ m_modIdResolver->abort();
return Task::abort();
}
@@ -232,12 +232,12 @@ bool FlameCreationTask::updateInstance()
connect(job.get(), &Task::failed, this, [](QString reason) { qCritical() << "Failed to get files: " << reason; });
connect(job.get(), &Task::finished, &loop, &QEventLoop::quit);
- m_process_update_file_info_job = job;
+ m_processUpdateFileInfoJob = job;
job->start();
loop.exec();
- m_process_update_file_info_job = nullptr;
+ m_processUpdateFileInfoJob = 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."),
@@ -430,26 +430,26 @@ bool FlameCreationTask::createInstance()
}
// 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);
+ if (!m_managedId.isEmpty())
+ instance.setManagedPack("flame", m_managedId, m_pack.name, m_managedVersionId, m_pack.version);
else
instance.setManagedPack("flame", "", name(), "", "");
instance.setName(name());
- m_mod_id_resolver.reset(new Flame::FileResolvingTask(APPLICATION->network(), m_pack));
- connect(m_mod_id_resolver.get(), &Flame::FileResolvingTask::succeeded, this, [this, &loop] { idResolverSucceeded(loop); });
- connect(m_mod_id_resolver.get(), &Flame::FileResolvingTask::failed, [this, &loop](QString reason) {
- m_mod_id_resolver.reset();
+ m_modIdResolver.reset(new Flame::FileResolvingTask(APPLICATION->network(), m_pack));
+ connect(m_modIdResolver.get(), &Flame::FileResolvingTask::succeeded, this, [this, &loop] { idResolverSucceeded(loop); });
+ connect(m_modIdResolver.get(), &Flame::FileResolvingTask::failed, [this, &loop](QString reason) {
+ m_modIdResolver.reset();
setError(tr("Unable to resolve mod IDs:\n") + reason);
loop.quit();
});
- connect(m_mod_id_resolver.get(), &Flame::FileResolvingTask::aborted, &loop, &QEventLoop::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);
- connect(m_mod_id_resolver.get(), &Flame::FileResolvingTask::stepProgress, this, &FlameCreationTask::propagateStepProgress);
- connect(m_mod_id_resolver.get(), &Flame::FileResolvingTask::details, this, &FlameCreationTask::setDetails);
- m_mod_id_resolver->start();
+ connect(m_modIdResolver.get(), &Flame::FileResolvingTask::aborted, &loop, &QEventLoop::quit);
+ connect(m_modIdResolver.get(), &Flame::FileResolvingTask::progress, this, &FlameCreationTask::setProgress);
+ connect(m_modIdResolver.get(), &Flame::FileResolvingTask::status, this, &FlameCreationTask::setStatus);
+ connect(m_modIdResolver.get(), &Flame::FileResolvingTask::stepProgress, this, &FlameCreationTask::propagateStepProgress);
+ connect(m_modIdResolver.get(), &Flame::FileResolvingTask::details, this, &FlameCreationTask::setDetails);
+ m_modIdResolver->start();
loop.exec();
@@ -468,14 +468,14 @@ bool FlameCreationTask::createInstance()
void FlameCreationTask::idResolverSucceeded(QEventLoop& loop)
{
- auto results = m_mod_id_resolver->getResults();
+ auto results = m_modIdResolver->getResults();
// first check for blocked mods
QList blocked_mods;
auto anyBlocked = false;
for (const auto& result : results.files.values()) {
- if (result.version.fileName.endsWith(".zip")) {
- m_ZIP_resources.append(std::make_pair(result.version.fileName, result.targetFolder));
+ if (result.resourceType != PackedResourceType::Mod) {
+ m_otherResources.append(std::make_pair(result.version.fileName, result.targetFolder));
}
if (result.version.downloadUrl.isEmpty()) {
@@ -507,7 +507,7 @@ void FlameCreationTask::idResolverSucceeded(QEventLoop& loop)
copyBlockedMods(blocked_mods);
setupDownloadJob(loop);
} else {
- m_mod_id_resolver.reset();
+ m_modIdResolver.reset();
setError("Canceled");
loop.quit();
}
@@ -518,8 +518,8 @@ void FlameCreationTask::idResolverSucceeded(QEventLoop& loop)
void FlameCreationTask::setupDownloadJob(QEventLoop& loop)
{
- m_files_job.reset(new NetJob(tr("Mod Download Flame"), APPLICATION->network()));
- auto results = m_mod_id_resolver->getResults().files;
+ m_filesJob.reset(new NetJob(tr("Mod Download Flame"), APPLICATION->network()));
+ auto results = m_modIdResolver->getResults().files;
QStringList optionalFiles;
for (auto& result : results) {
@@ -554,26 +554,26 @@ void FlameCreationTask::setupDownloadJob(QEventLoop& loop)
if (!result.version.downloadUrl.isEmpty()) {
qDebug() << "Will download" << result.version.downloadUrl << "to" << path;
auto dl = Net::ApiDownload::makeFile(result.version.downloadUrl, path);
- m_files_job->addNetAction(dl);
+ m_filesJob->addNetAction(dl);
}
}
- connect(m_files_job.get(), &NetJob::finished, this, [this, &loop]() {
- m_files_job.reset();
- validateZIPResources(loop);
+ connect(m_filesJob.get(), &NetJob::finished, this, [this, &loop]() {
+ m_filesJob.reset();
+ validateOtherResources(loop);
});
- connect(m_files_job.get(), &NetJob::failed, [this](QString reason) {
- m_files_job.reset();
+ connect(m_filesJob.get(), &NetJob::failed, [this](QString reason) {
+ m_filesJob.reset();
setError(reason);
});
- connect(m_files_job.get(), &NetJob::progress, this, [this](qint64 current, qint64 total) {
+ connect(m_filesJob.get(), &NetJob::progress, this, [this](qint64 current, qint64 total) {
setDetails(tr("%1 out of %2 complete").arg(current).arg(total));
setProgress(current, total);
});
- connect(m_files_job.get(), &NetJob::stepProgress, this, &FlameCreationTask::propagateStepProgress);
+ connect(m_filesJob.get(), &NetJob::stepProgress, this, &FlameCreationTask::propagateStepProgress);
setStatus(tr("Downloading mods..."));
- m_files_job->start();
+ m_filesJob->start();
}
/// @brief copy the matched blocked mods to the instance staging area
@@ -597,8 +597,14 @@ void FlameCreationTask::copyBlockedMods(QList const& blocked_mods)
qDebug() << "Will try to copy" << mod.localPath << "to" << destPath;
- if (!FS::copy(mod.localPath, destPath)()) {
- qDebug() << "Copy of" << mod.localPath << "to" << destPath << "Failed";
+ if (mod.move) {
+ if (!FS::move(mod.localPath, destPath)) {
+ qDebug() << "Move of" << mod.localPath << "to" << destPath << "Failed";
+ }
+ } else {
+ if (!FS::copy(mod.localPath, destPath)()) {
+ qDebug() << "Copy of" << mod.localPath << "to" << destPath << "Failed";
+ }
}
i++;
@@ -608,11 +614,11 @@ void FlameCreationTask::copyBlockedMods(QList const& blocked_mods)
setAbortable(true);
}
-void FlameCreationTask::validateZIPResources(QEventLoop& loop)
+void FlameCreationTask::validateOtherResources(QEventLoop& loop)
{
- qDebug() << "Validating whether resources stored as .zip are in the right place";
+ qDebug() << "Validating whether other resources are in the right place";
QStringList zipMods;
- for (auto [fileName, targetFolder] : m_ZIP_resources) {
+ for (auto [fileName, targetFolder] : m_otherResources) {
qDebug() << "Checking" << fileName << "...";
auto localPath = FS::PathCombine(m_stagingPath, "minecraft", targetFolder, fileName);
@@ -672,6 +678,7 @@ void FlameCreationTask::validateZIPResources(QEventLoop& loop)
installWorld(worldPath);
break;
case PackedResourceType::UNKNOWN:
+ /* fallthrough */
default:
qDebug() << "Can't Identify" << fileName << "at" << localPath << ", leaving it where it is.";
break;
@@ -679,7 +686,7 @@ void FlameCreationTask::validateZIPResources(QEventLoop& loop)
}
// TODO make this work with other sorts of resource
auto task = makeShared("CreateModMetadata", APPLICATION->settings()->get("NumberOfConcurrentTasks").toInt());
- auto results = m_mod_id_resolver->getResults().files;
+ auto results = m_modIdResolver->getResults().files;
auto folder = FS::PathCombine(m_stagingPath, "minecraft", "mods", ".index");
for (auto file : results) {
if (file.targetFolder != "mods" || (file.version.fileName.endsWith(".zip") && !zipMods.contains(file.version.fileName))) {
@@ -688,6 +695,6 @@ void FlameCreationTask::validateZIPResources(QEventLoop& loop)
task->addTask(makeShared(folder, file.pack, file.version));
}
connect(task.get(), &Task::finished, &loop, &QEventLoop::quit);
- m_process_update_file_info_job = task;
+ m_processUpdateFileInfoJob = task;
task->start();
}
diff --git a/launcher/modplatform/flame/FlameInstanceCreationTask.h b/launcher/modplatform/flame/FlameInstanceCreationTask.h
index 28ab176c2..3e586a416 100644
--- a/launcher/modplatform/flame/FlameInstanceCreationTask.h
+++ b/launcher/modplatform/flame/FlameInstanceCreationTask.h
@@ -57,7 +57,7 @@ class FlameCreationTask final : public InstanceCreationTask {
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))
+ : InstanceCreationTask(), m_parent(parent), m_managedId(std::move(id)), m_managedVersionId(std::move(version_id))
{
setStagingPath(staging_path);
setParentSettings(global_settings);
@@ -74,22 +74,22 @@ class FlameCreationTask final : public InstanceCreationTask {
void idResolverSucceeded(QEventLoop&);
void setupDownloadJob(QEventLoop&);
void copyBlockedMods(QList const& blocked_mods);
- void validateZIPResources(QEventLoop& loop);
+ void validateOtherResources(QEventLoop& loop);
QString getVersionForLoader(QString uid, QString loaderType, QString version, QString mcVersion);
private:
QWidget* m_parent = nullptr;
- shared_qobject_ptr m_mod_id_resolver;
+ shared_qobject_ptr m_modIdResolver;
Flame::Manifest m_pack;
// Handle to allow aborting
- Task::Ptr m_process_update_file_info_job = nullptr;
- NetJob::Ptr m_files_job = nullptr;
+ Task::Ptr m_processUpdateFileInfoJob = nullptr;
+ NetJob::Ptr m_filesJob = nullptr;
- QString m_managed_id, m_managed_version_id;
+ QString m_managedId, m_managedVersionId;
- QList> m_ZIP_resources;
+ QList> m_otherResources;
std::optional m_instance;
};
diff --git a/launcher/modplatform/flame/PackManifest.h b/launcher/modplatform/flame/PackManifest.h
index 49a0b2d68..7af3b9d6b 100644
--- a/launcher/modplatform/flame/PackManifest.h
+++ b/launcher/modplatform/flame/PackManifest.h
@@ -40,6 +40,7 @@
#include
#include
#include
+#include "minecraft/mod/tasks/LocalResourceParse.h"
#include "modplatform/ModIndex.h"
namespace Flame {
@@ -54,6 +55,7 @@ struct File {
// our
QString targetFolder = QStringLiteral("mods");
+ PackedResourceType resourceType;
};
struct Modloader {
diff --git a/launcher/modplatform/modrinth/ModrinthAPI.h b/launcher/modplatform/modrinth/ModrinthAPI.h
index 3a5c21ed1..ae74545e2 100644
--- a/launcher/modplatform/modrinth/ModrinthAPI.h
+++ b/launcher/modplatform/modrinth/ModrinthAPI.h
@@ -71,13 +71,15 @@ class ModrinthAPI : public NetworkResourceAPI {
static auto getSideFilters(QString side) -> const QString
{
- if (side.isEmpty() || side == "both") {
+ if (side.isEmpty()) {
return {};
}
+ if (side == "both")
+ return QString("\"client_side:required\"],[\"server_side:required\"");
if (side == "client")
- return QString("\"client_side:required\",\"client_side:optional\"");
+ return QString("\"client_side:required\",\"client_side:optional\"],[\"server_side:optional\",\"server_side:unsupported\"");
if (side == "server")
- return QString("\"server_side:required\",\"server_side:optional\"");
+ return QString("\"server_side:required\",\"server_side:optional\"],[\"client_side:optional\",\"client_side:unsupported\"");
return {};
}
diff --git a/launcher/modplatform/modrinth/ModrinthCheckUpdate.cpp b/launcher/modplatform/modrinth/ModrinthCheckUpdate.cpp
index b76883e65..aa371f280 100644
--- a/launcher/modplatform/modrinth/ModrinthCheckUpdate.cpp
+++ b/launcher/modplatform/modrinth/ModrinthCheckUpdate.cpp
@@ -8,6 +8,7 @@
#include "QObjectPtr.h"
#include "ResourceDownloadTask.h"
+#include "modplatform/ModIndex.h"
#include "modplatform/helpers/HashUtils.h"
#include "tasks/ConcurrentTask.h"
@@ -107,10 +108,8 @@ void ModrinthCheckUpdate::checkVersionsResponse(std::shared_ptr resp
// Sometimes a version may have multiple files, one with "forge" and one with "fabric",
// so we may want to filter it
QString loader_filter;
- static auto flags = { ModPlatform::ModLoaderType::NeoForge, ModPlatform::ModLoaderType::Forge,
- ModPlatform::ModLoaderType::Quilt, ModPlatform::ModLoaderType::Fabric };
- for (auto flag : flags) {
- if (loader.has_value() && loader->testFlag(flag)) {
+ if (loader.has_value()) {
+ for (auto flag : ModPlatform::modLoaderTypesToList(*loader)) {
loader_filter = ModPlatform::getModLoaderAsString(flag);
break;
}
diff --git a/launcher/modplatform/modrinth/ModrinthInstanceCreationTask.cpp b/launcher/modplatform/modrinth/ModrinthInstanceCreationTask.cpp
index bfececb8d..2cc8767a4 100644
--- a/launcher/modplatform/modrinth/ModrinthInstanceCreationTask.cpp
+++ b/launcher/modplatform/modrinth/ModrinthInstanceCreationTask.cpp
@@ -262,12 +262,14 @@ bool ModrinthCreationTask::createInstance()
mod->setDetails(d);
resources[file.hash.toHex()] = mod;
}
-
+ if (file.downloads.empty()) {
+ setError(tr("The file '%1' is missing a download link. This is invalid in the pack format.").arg(fileName));
+ return false;
+ }
qDebug() << "Will try to download" << file.downloads.front() << "to" << file_path;
auto dl = Net::ApiDownload::makeFile(file.downloads.dequeue(), file_path);
dl->addValidator(new Net::ChecksumValidator(file.hashAlgorithm, file.hash));
downloadMods->addNetAction(dl);
-
if (!file.downloads.empty()) {
// FIXME: This really needs to be put into a ConcurrentTask of
// MultipleOptionsTask's , once those exist :)
diff --git a/launcher/modplatform/packwiz/Packwiz.cpp b/launcher/modplatform/packwiz/Packwiz.cpp
index bbccb72b8..a3bb74399 100644
--- a/launcher/modplatform/packwiz/Packwiz.cpp
+++ b/launcher/modplatform/packwiz/Packwiz.cpp
@@ -190,11 +190,8 @@ void V1::updateModIndex(const QDir& index_dir, Mod& mod)
}
toml::array loaders;
- for (auto loader : { ModPlatform::NeoForge, ModPlatform::Forge, ModPlatform::Cauldron, ModPlatform::LiteLoader, ModPlatform::Fabric,
- ModPlatform::Quilt }) {
- if (mod.loaders & loader) {
- loaders.push_back(getModLoaderAsString(loader).toStdString());
- }
+ for (auto loader : ModPlatform::modLoaderTypesToList(mod.loaders)) {
+ loaders.push_back(getModLoaderAsString(loader).toStdString());
}
toml::array mcVersions;
for (auto version : mod.mcVersions) {
diff --git a/launcher/resources/multimc/index.theme b/launcher/resources/multimc/index.theme
index 4da8072d9..497106d6f 100644
--- a/launcher/resources/multimc/index.theme
+++ b/launcher/resources/multimc/index.theme
@@ -1,7 +1,6 @@
[Icon Theme]
Name=Legacy
Comment=Default Icons
-Inherits=default
Directories=8x8,16x16,22x22,24x24,32x32,32x32/instances,48x48,50x50/instances,64x64,128x128/instances,256x256,scalable,scalable/instances
[8x8]
diff --git a/launcher/ui/GuiUtil.cpp b/launcher/ui/GuiUtil.cpp
index 93b9a452b..d53ade86d 100644
--- a/launcher/ui/GuiUtil.cpp
+++ b/launcher/ui/GuiUtil.cpp
@@ -51,11 +51,35 @@
#include
#include "Application.h"
+constexpr int MaxMclogsLines = 25000;
+constexpr int InitialMclogsLines = 10000;
+constexpr int FinalMclogsLines = 14900;
+
+QString truncateLogForMclogs(const QString& logContent)
+{
+ QStringList lines = logContent.split("\n");
+ if (lines.size() > MaxMclogsLines) {
+ QString truncatedLog = lines.mid(0, InitialMclogsLines).join("\n");
+ truncatedLog +=
+ "\n\n\n\n\n\n\n\n\n\n"
+ "------------------------------------------------------------\n"
+ "----------------------- Log truncated ----------------------\n"
+ "------------------------------------------------------------\n"
+ "----- Middle portion omitted to fit mclo.gs size limits ----\n"
+ "------------------------------------------------------------\n"
+ "\n\n\n\n\n\n\n\n\n\n";
+ truncatedLog += lines.mid(lines.size() - FinalMclogsLines - 1).join("\n");
+ return truncatedLog;
+ }
+ return logContent;
+}
+
std::optional GuiUtil::uploadPaste(const QString& name, const QString& text, QWidget* parentWidget)
{
ProgressDialog dialog(parentWidget);
auto pasteTypeSetting = static_cast(APPLICATION->settings()->get("PastebinType").toInt());
auto pasteCustomAPIBaseSetting = APPLICATION->settings()->get("PastebinCustomAPIBase").toString();
+ bool shouldTruncate = false;
{
QUrl baseUrl;
@@ -75,10 +99,36 @@ std::optional GuiUtil::uploadPaste(const QString& name, const QString&
if (response != QMessageBox::Yes)
return {};
+
+ if (baseUrl.toString() == "https://api.mclo.gs" && text.count("\n") > MaxMclogsLines) {
+ auto truncateResponse = CustomMessageBox::selectable(
+ parentWidget, QObject::tr("Confirm Truncation"),
+ QObject::tr("The log has %1 lines, exceeding mclo.gs' limit of %2.\n"
+ "The launcher can keep the first %3 and last %4 lines, trimming the middle.\n\n"
+ "If you choose 'No', mclo.gs will only keep the first %2 lines, cutting off "
+ "potentially useful info like crashes at the end.\n\n"
+ "Proceed with truncation?")
+ .arg(text.count("\n"))
+ .arg(MaxMclogsLines)
+ .arg(InitialMclogsLines)
+ .arg(FinalMclogsLines),
+ QMessageBox::Warning, QMessageBox::Yes | QMessageBox::No | QMessageBox::Cancel, QMessageBox::No)
+ ->exec();
+
+ if (truncateResponse == QMessageBox::Cancel) {
+ return {};
+ }
+ shouldTruncate = truncateResponse == QMessageBox::Yes;
+ }
}
}
- std::unique_ptr paste(new PasteUpload(parentWidget, text, pasteCustomAPIBaseSetting, pasteTypeSetting));
+ QString textToUpload = text;
+ if (shouldTruncate) {
+ textToUpload = truncateLogForMclogs(text);
+ }
+
+ std::unique_ptr paste(new PasteUpload(parentWidget, textToUpload, pasteCustomAPIBaseSetting, pasteTypeSetting));
dialog.execWithTask(paste.get());
if (!paste->wasSuccessful()) {
diff --git a/launcher/ui/dialogs/BlockedModsDialog.cpp b/launcher/ui/dialogs/BlockedModsDialog.cpp
index b3b6d2bcc..0095f7af9 100644
--- a/launcher/ui/dialogs/BlockedModsDialog.cpp
+++ b/launcher/ui/dialogs/BlockedModsDialog.cpp
@@ -288,6 +288,8 @@ void BlockedModsDialog::checkMatchHash(QString hash, QString path)
qDebug() << "[Blocked Mods Dialog] Checking for match on hash: " << hash << "| From path:" << path;
+ auto downloadDir = QFileInfo(APPLICATION->settings()->get("DownloadsDir").toString()).absoluteFilePath();
+ auto moveFiles = APPLICATION->settings()->get("MoveModsFromDownloadsDir").toBool();
for (auto& mod : m_mods) {
if (mod.matched) {
continue;
@@ -295,6 +297,9 @@ void BlockedModsDialog::checkMatchHash(QString hash, QString path)
if (mod.hash.compare(hash, Qt::CaseInsensitive) == 0) {
mod.matched = true;
mod.localPath = path;
+ if (moveFiles) {
+ mod.move = QFileInfo(path).absoluteFilePath().startsWith(downloadDir);
+ }
match = true;
qDebug() << "[Blocked Mods Dialog] Hash match found:" << mod.name << hash << "| From path:" << path;
@@ -346,6 +351,8 @@ bool BlockedModsDialog::checkValidPath(QString path)
return fsName.compare(metaName) == 0;
};
+ auto downloadDir = QFileInfo(APPLICATION->settings()->get("DownloadsDir").toString()).absoluteFilePath();
+ auto moveFiles = APPLICATION->settings()->get("MoveModsFromDownloadsDir").toBool();
for (auto& mod : m_mods) {
if (compare(filename, mod.name)) {
// if the mod is not yet matched and doesn't have a hash then
@@ -353,6 +360,9 @@ bool BlockedModsDialog::checkValidPath(QString path)
if (!mod.matched && mod.hash.isEmpty()) {
mod.matched = true;
mod.localPath = path;
+ if (moveFiles) {
+ mod.move = QFileInfo(path).absoluteFilePath().startsWith(downloadDir);
+ }
return false;
}
qDebug() << "[Blocked Mods Dialog] Name match found:" << mod.name << "| From path:" << path;
diff --git a/launcher/ui/dialogs/BlockedModsDialog.h b/launcher/ui/dialogs/BlockedModsDialog.h
index 09722bce9..b2d2c0374 100644
--- a/launcher/ui/dialogs/BlockedModsDialog.h
+++ b/launcher/ui/dialogs/BlockedModsDialog.h
@@ -42,6 +42,7 @@ struct BlockedMod {
bool matched;
QString localPath;
QString targetFolder;
+ bool move = false;
};
QT_BEGIN_NAMESPACE
diff --git a/launcher/ui/dialogs/EditAccountDialog.cpp b/launcher/ui/dialogs/EditAccountDialog.cpp
deleted file mode 100644
index 9d0175bbc..000000000
--- a/launcher/ui/dialogs/EditAccountDialog.cpp
+++ /dev/null
@@ -1,64 +0,0 @@
-/* Copyright 2013-2021 MultiMC Contributors
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-#include "EditAccountDialog.h"
-#include
-#include
-#include
-#include "ui_EditAccountDialog.h"
-
-EditAccountDialog::EditAccountDialog(const QString& text, QWidget* parent, int flags) : QDialog(parent), ui(new Ui::EditAccountDialog)
-{
- ui->setupUi(this);
-
- ui->label->setText(text);
- ui->label->setVisible(!text.isEmpty());
-
- ui->userTextBox->setEnabled(flags & UsernameField);
- ui->passTextBox->setEnabled(flags & PasswordField);
-
- ui->buttonBox->button(QDialogButtonBox::Cancel)->setText(tr("Cancel"));
- ui->buttonBox->button(QDialogButtonBox::Ok)->setText(tr("OK"));
-}
-
-EditAccountDialog::~EditAccountDialog()
-{
- delete ui;
-}
-
-void EditAccountDialog::on_label_linkActivated(const QString& link)
-{
- DesktopServices::openUrl(QUrl(link));
-}
-
-void EditAccountDialog::setUsername(const QString& user) const
-{
- ui->userTextBox->setText(user);
-}
-
-QString EditAccountDialog::username() const
-{
- return ui->userTextBox->text();
-}
-
-void EditAccountDialog::setPassword(const QString& pass) const
-{
- ui->passTextBox->setText(pass);
-}
-
-QString EditAccountDialog::password() const
-{
- return ui->passTextBox->text();
-}
diff --git a/launcher/ui/dialogs/EditAccountDialog.h b/launcher/ui/dialogs/EditAccountDialog.h
deleted file mode 100644
index 7a9ccba79..000000000
--- a/launcher/ui/dialogs/EditAccountDialog.h
+++ /dev/null
@@ -1,52 +0,0 @@
-/* Copyright 2013-2021 MultiMC Contributors
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-#pragma once
-
-#include
-
-namespace Ui {
-class EditAccountDialog;
-}
-
-class EditAccountDialog : public QDialog {
- Q_OBJECT
-
- public:
- explicit EditAccountDialog(const QString& text = "", QWidget* parent = 0, int flags = UsernameField | PasswordField);
- ~EditAccountDialog();
-
- void setUsername(const QString& user) const;
- void setPassword(const QString& pass) const;
-
- QString username() const;
- QString password() const;
-
- enum Flags {
- NoFlags = 0,
-
- //! Specifies that the dialog should have a username field.
- UsernameField,
-
- //! Specifies that the dialog should have a password field.
- PasswordField,
- };
-
- private slots:
- void on_label_linkActivated(const QString& link);
-
- private:
- Ui::EditAccountDialog* ui;
-};
diff --git a/launcher/ui/dialogs/EditAccountDialog.ui b/launcher/ui/dialogs/EditAccountDialog.ui
deleted file mode 100644
index e87509bcb..000000000
--- a/launcher/ui/dialogs/EditAccountDialog.ui
+++ /dev/null
@@ -1,94 +0,0 @@
-
-
- EditAccountDialog
-
-
-
- 0
- 0
- 400
- 148
-
-
-
- Login
-
-
- -
-
-
- Message label placeholder.
-
-
- Qt::RichText
-
-
- Qt::LinksAccessibleByKeyboard|Qt::LinksAccessibleByMouse|Qt::TextBrowserInteraction|Qt::TextSelectableByKeyboard|Qt::TextSelectableByMouse
-
-
-
- -
-
-
- Email
-
-
-
- -
-
-
- QLineEdit::Password
-
-
- Password
-
-
-
- -
-
-
- Qt::Horizontal
-
-
- QDialogButtonBox::Cancel|QDialogButtonBox::Ok
-
-
-
-
-
-
-
-
- buttonBox
- accepted()
- EditAccountDialog
- accept()
-
-
- 248
- 254
-
-
- 157
- 274
-
-
-
-
- buttonBox
- rejected()
- EditAccountDialog
- reject()
-
-
- 316
- 260
-
-
- 286
- 274
-
-
-
-
-
diff --git a/launcher/ui/dialogs/IconPickerDialog.cpp b/launcher/ui/dialogs/IconPickerDialog.cpp
index a7b44eab0..b6e928a3d 100644
--- a/launcher/ui/dialogs/IconPickerDialog.cpp
+++ b/launcher/ui/dialogs/IconPickerDialog.cpp
@@ -15,7 +15,9 @@
#include
#include
+#include
#include
+#include
#include "Application.h"
@@ -33,6 +35,15 @@ IconPickerDialog::IconPickerDialog(QWidget* parent) : QDialog(parent), ui(new Ui
ui->setupUi(this);
setWindowModality(Qt::WindowModal);
+ searchBar = new QLineEdit(this);
+ searchBar->setPlaceholderText(tr("Search..."));
+ ui->verticalLayout->insertWidget(0, searchBar);
+
+ proxyModel = new QSortFilterProxyModel(this);
+ proxyModel->setSourceModel(APPLICATION->icons().get());
+ proxyModel->setFilterCaseSensitivity(Qt::CaseInsensitive);
+ ui->iconView->setModel(proxyModel);
+
auto contentsWidget = ui->iconView;
contentsWidget->setViewMode(QListView::IconMode);
contentsWidget->setFlow(QListView::LeftToRight);
@@ -57,7 +68,7 @@ IconPickerDialog::IconPickerDialog(QWidget* parent) : QDialog(parent), ui(new Ui
contentsWidget->installEventFilter(this);
- contentsWidget->setModel(APPLICATION->icons().get());
+ contentsWidget->setModel(proxyModel);
// NOTE: ResetRole forces the button to be on the left, while the OK/Cancel ones are on the right. We win.
auto buttonAdd = ui->buttonBox->addButton(tr("Add Icon"), QDialogButtonBox::ResetRole);
@@ -76,6 +87,9 @@ IconPickerDialog::IconPickerDialog(QWidget* parent) : QDialog(parent), ui(new Ui
auto buttonFolder = ui->buttonBox->addButton(tr("Open Folder"), QDialogButtonBox::ResetRole);
connect(buttonFolder, &QPushButton::clicked, this, &IconPickerDialog::openFolder);
+ connect(searchBar, &QLineEdit::textChanged, this, &IconPickerDialog::filterIcons);
+ // Prevent incorrect indices from e.g. filesystem changes
+ connect(APPLICATION->icons().get(), &IconList::iconUpdated, this, [this]() { proxyModel->invalidate(); });
}
bool IconPickerDialog::eventFilter(QObject* obj, QEvent* evt)
@@ -162,5 +176,10 @@ IconPickerDialog::~IconPickerDialog()
void IconPickerDialog::openFolder()
{
- DesktopServices::openPath(APPLICATION->icons()->getDirectory(), true);
+ DesktopServices::openPath(APPLICATION->icons()->iconDirectory(selectedIconKey), true);
}
+
+void IconPickerDialog::filterIcons(const QString& query)
+{
+ proxyModel->setFilterFixedString(query);
+}
\ No newline at end of file
diff --git a/launcher/ui/dialogs/IconPickerDialog.h b/launcher/ui/dialogs/IconPickerDialog.h
index 37e53dcce..db1315338 100644
--- a/launcher/ui/dialogs/IconPickerDialog.h
+++ b/launcher/ui/dialogs/IconPickerDialog.h
@@ -16,6 +16,8 @@
#pragma once
#include
#include
+#include
+#include
namespace Ui {
class IconPickerDialog;
@@ -36,6 +38,8 @@ class IconPickerDialog : public QDialog {
private:
Ui::IconPickerDialog* ui;
QPushButton* buttonRemove;
+ QLineEdit* searchBar;
+ QSortFilterProxyModel* proxyModel;
private slots:
void selectionChanged(QItemSelection, QItemSelection);
@@ -44,4 +48,5 @@ class IconPickerDialog : public QDialog {
void addNewIcon();
void removeSelectedIcon();
void openFolder();
+ void filterIcons(const QString& text);
};
diff --git a/launcher/ui/dialogs/ProfileSetupDialog.ui b/launcher/ui/dialogs/ProfileSetupDialog.ui
index 9dbabb4b3..947110da7 100644
--- a/launcher/ui/dialogs/ProfileSetupDialog.ui
+++ b/launcher/ui/dialogs/ProfileSetupDialog.ui
@@ -30,6 +30,9 @@ Choose your name carefully:
true
+
+ Qt::LinksAccessibleByKeyboard|Qt::LinksAccessibleByMouse|Qt::TextBrowserInteraction|Qt::TextSelectableByKeyboard|Qt::TextSelectableByMouse
+
nameEdit
diff --git a/launcher/ui/dialogs/ResourceUpdateDialog.cpp b/launcher/ui/dialogs/ResourceUpdateDialog.cpp
index 78cf0e44a..7e29e1192 100644
--- a/launcher/ui/dialogs/ResourceUpdateDialog.cpp
+++ b/launcher/ui/dialogs/ResourceUpdateDialog.cpp
@@ -1,4 +1,5 @@
#include "ResourceUpdateDialog.h"
+#include "Application.h"
#include "ChooseProviderDialog.h"
#include "CustomMessageBox.h"
#include "ProgressDialog.h"
@@ -7,6 +8,7 @@
#include "minecraft/mod/tasks/GetModDependenciesTask.h"
#include "modplatform/ModIndex.h"
#include "modplatform/flame/FlameAPI.h"
+#include "tasks/SequentialTask.h"
#include "ui_ReviewMessageBox.h"
#include "Markdown.h"
@@ -411,8 +413,14 @@ void ResourceUpdateDialog::onMetadataFailed(Resource* resource, bool try_others,
connect(task.get(), &EnsureMetadataTask::metadataFailed, [this](Resource* candidate) { onMetadataFailed(candidate, false); });
connect(task.get(), &EnsureMetadataTask::failed,
[this](const QString& reason) { CustomMessageBox::selectable(this, tr("Error"), reason, QMessageBox::Critical)->exec(); });
-
- m_second_try_metadata->addTask(task);
+ if (task->getHashingTask()) {
+ auto seq = makeShared();
+ seq->addTask(task->getHashingTask());
+ seq->addTask(task);
+ m_second_try_metadata->addTask(seq);
+ } else {
+ m_second_try_metadata->addTask(task);
+ }
} else {
QString reason{ tr("Couldn't find a valid version on the selected mod provider(s)") };
diff --git a/launcher/ui/pages/global/APIPage.ui b/launcher/ui/pages/global/APIPage.ui
index 9c713aa79..05c256bb2 100644
--- a/launcher/ui/pages/global/APIPage.ui
+++ b/launcher/ui/pages/global/APIPage.ui
@@ -207,7 +207,7 @@
-
- <html><head/><body><p>Note: you only need to set this to access private data. Read the <a href="https://docs.modrinth.com/#section/Authentication">documentation</a> for more information.</p></body></html>
+ <html><head/><body><p>Note: you only need to set this to access private data. Read the <a href="https://docs.modrinth.com/api/#authentication">documentation</a> for more information.</p></body></html>
true
diff --git a/launcher/ui/pages/global/LauncherPage.cpp b/launcher/ui/pages/global/LauncherPage.cpp
index f7d7da568..04ee01b00 100644
--- a/launcher/ui/pages/global/LauncherPage.cpp
+++ b/launcher/ui/pages/global/LauncherPage.cpp
@@ -232,6 +232,7 @@ void LauncherPage::applySettings()
s->set("SkinsDir", ui->skinsDirTextBox->text());
s->set("JavaDir", ui->javaDirTextBox->text());
s->set("DownloadsDirWatchRecursive", ui->downloadsDirWatchRecursiveCheckBox->isChecked());
+ s->set("MoveModsFromDownloadsDir", ui->downloadsDirMoveCheckBox->isChecked());
auto sortMode = (InstSortMode)ui->sortingModeGroup->checkedId();
switch (sortMode) {
@@ -296,6 +297,7 @@ void LauncherPage::loadSettings()
ui->skinsDirTextBox->setText(s->get("SkinsDir").toString());
ui->javaDirTextBox->setText(s->get("JavaDir").toString());
ui->downloadsDirWatchRecursiveCheckBox->setChecked(s->get("DownloadsDirWatchRecursive").toBool());
+ ui->downloadsDirMoveCheckBox->setChecked(s->get("MoveModsFromDownloadsDir").toBool());
QString sortMode = s->get("InstSortMode").toString();
diff --git a/launcher/ui/pages/global/LauncherPage.ui b/launcher/ui/pages/global/LauncherPage.ui
index 87c9dad78..c36dfdfc6 100644
--- a/launcher/ui/pages/global/LauncherPage.ui
+++ b/launcher/ui/pages/global/LauncherPage.ui
@@ -6,7 +6,7 @@
0
0
- 511
+ 562
726
@@ -38,7 +38,7 @@
QTabWidget::Rounded
- 0
+ 2
@@ -48,7 +48,7 @@
-
- Qt::ScrollBarAlwaysOff
+ Qt::ScrollBarAsNeeded
true
@@ -58,8 +58,8 @@
0
0
- 473
- 690
+ 570
+ 692
@@ -156,14 +156,28 @@
-
-
-
- When enabled, in addition to the downloads folder, its sub folders will also be searched when looking for resources (e.g. when looking for blocked mods on CurseForge).
-
-
- Check downloads folder recursively
-
-
+
+
-
+
+
+ When enabled, in addition to the downloads folder, its sub folders will also be searched when looking for resources (e.g. when looking for blocked mods on CurseForge).
+
+
+ Check downloads folder recursively
+
+
+
+ -
+
+
+ When enabled, it will move blocked resources instead of copying them.
+
+
+ Move blocked resources
+
+
+
+
-
@@ -585,7 +599,7 @@
- Qt::ScrollBarAlwaysOff
+ Qt::ScrollBarAsNeeded
false
@@ -637,15 +651,33 @@
tabWidget
+ scrollArea
autoUpdateCheckBox
+ updateIntervalSpinBox
instDirTextBox
instDirBrowseBtn
modsDirTextBox
modsDirBrowseBtn
iconsDirTextBox
iconsDirBrowseBtn
+ javaDirTextBox
+ javaDirBrowseBtn
+ skinsDirTextBox
+ skinsDirBrowseBtn
+ downloadsDirTextBox
+ downloadsDirBrowseBtn
+ downloadsDirWatchRecursiveCheckBox
+ metadataDisableBtn
+ dependenciesDisableBtn
+ skipModpackUpdatePromptBtn
+ numberOfConcurrentTasksSpinBox
+ numberOfConcurrentDownloadsSpinBox
+ numberOfManualRetriesSpinBox
+ timeoutSecondsSpinBox
sortLastLaunchedBtn
sortByNameBtn
+ catOpacitySpinBox
+ preferMenuBarCheckBox
lineLimitSpinBox
checkStopLogging
consoleFont
diff --git a/launcher/ui/pages/instance/ExternalResourcesPage.ui b/launcher/ui/pages/instance/ExternalResourcesPage.ui
index 9c41f3a71..5df8aafa2 100644
--- a/launcher/ui/pages/instance/ExternalResourcesPage.ui
+++ b/launcher/ui/pages/instance/ExternalResourcesPage.ui
@@ -60,7 +60,7 @@
true
- QAbstractItemView::NoDragDrop
+ QAbstractItemView::DropOnly
true
diff --git a/launcher/ui/pages/instance/VersionPage.cpp b/launcher/ui/pages/instance/VersionPage.cpp
index ab1c48ed4..975c44de2 100644
--- a/launcher/ui/pages/instance/VersionPage.cpp
+++ b/launcher/ui/pages/instance/VersionPage.cpp
@@ -252,8 +252,11 @@ void VersionPage::updateButtons(int row)
bool VersionPage::reloadPackProfile()
{
try {
- m_profile->reload(Net::Mode::Online);
- return true;
+ auto result = m_profile->reload(Net::Mode::Online);
+ if (!result) {
+ QMessageBox::critical(this, tr("Error"), result.error);
+ }
+ return result;
} catch (const Exception& e) {
QMessageBox::critical(this, tr("Error"), e.cause());
return false;
diff --git a/launcher/ui/pages/modplatform/import_ftb/ImportFTBPage.ui b/launcher/ui/pages/modplatform/import_ftb/ImportFTBPage.ui
index 18c604ca4..337c3e474 100644
--- a/launcher/ui/pages/modplatform/import_ftb/ImportFTBPage.ui
+++ b/launcher/ui/pages/modplatform/import_ftb/ImportFTBPage.ui
@@ -13,6 +13,11 @@
-
+
+
+ true
+
+
Note: If your FTB instances are not in the default location, select it using the button next to search.
diff --git a/launcher/ui/themes/ThemeManager.cpp b/launcher/ui/themes/ThemeManager.cpp
index 691a51668..30a1fe7be 100644
--- a/launcher/ui/themes/ThemeManager.cpp
+++ b/launcher/ui/themes/ThemeManager.cpp
@@ -36,6 +36,9 @@
ThemeManager::ThemeManager()
{
+ QIcon::setFallbackThemeName(QIcon::themeName());
+ QIcon::setThemeSearchPaths(QIcon::themeSearchPaths() << m_iconThemeFolder.path());
+
themeDebugLog() << "Determining System Widget Theme...";
const auto& style = QApplication::style();
m_defaultStyle = style->objectName();
@@ -93,10 +96,6 @@ void ThemeManager::initializeIcons()
// set icon theme search path!
themeDebugLog() << "<> Initializing Icon Themes";
- auto searchPaths = QIcon::themeSearchPaths();
- searchPaths.append(m_iconThemeFolder.path());
- QIcon::setThemeSearchPaths(searchPaths);
-
for (const QString& id : builtinIcons) {
IconTheme theme(id, QString(":/icons/%1").arg(id));
if (!theme.load()) {
@@ -348,4 +347,4 @@ void ThemeManager::refresh()
initializeThemes();
initializeCatPacks();
-}
\ No newline at end of file
+}
diff --git a/launcher/ui/widgets/DropLabel.cpp b/launcher/ui/widgets/DropLabel.cpp
deleted file mode 100644
index b1473b358..000000000
--- a/launcher/ui/widgets/DropLabel.cpp
+++ /dev/null
@@ -1,40 +0,0 @@
-#include "DropLabel.h"
-
-#include
-#include
-
-DropLabel::DropLabel(QWidget* parent) : QLabel(parent)
-{
- setAcceptDrops(true);
-}
-
-void DropLabel::dragEnterEvent(QDragEnterEvent* event)
-{
- event->acceptProposedAction();
-}
-
-void DropLabel::dragMoveEvent(QDragMoveEvent* event)
-{
- event->acceptProposedAction();
-}
-
-void DropLabel::dragLeaveEvent(QDragLeaveEvent* event)
-{
- event->accept();
-}
-
-void DropLabel::dropEvent(QDropEvent* event)
-{
- const QMimeData* mimeData = event->mimeData();
-
- if (!mimeData) {
- return;
- }
-
- if (mimeData->hasUrls()) {
- auto urls = mimeData->urls();
- emit droppedURLs(urls);
- }
-
- event->acceptProposedAction();
-}
diff --git a/launcher/ui/widgets/DropLabel.h b/launcher/ui/widgets/DropLabel.h
deleted file mode 100644
index 0027f48b1..000000000
--- a/launcher/ui/widgets/DropLabel.h
+++ /dev/null
@@ -1,19 +0,0 @@
-#pragma once
-
-#include
-
-class DropLabel : public QLabel {
- Q_OBJECT
-
- public:
- explicit DropLabel(QWidget* parent = nullptr);
-
- signals:
- void droppedURLs(QList urls);
-
- protected:
- void dropEvent(QDropEvent* event) override;
- void dragEnterEvent(QDragEnterEvent* event) override;
- void dragMoveEvent(QDragMoveEvent* event) override;
- void dragLeaveEvent(QDragLeaveEvent* event) override;
-};
diff --git a/launcher/ui/widgets/ModFilterWidget.cpp b/launcher/ui/widgets/ModFilterWidget.cpp
index 5ae49d3a5..37211693f 100644
--- a/launcher/ui/widgets/ModFilterWidget.cpp
+++ b/launcher/ui/widgets/ModFilterWidget.cpp
@@ -279,16 +279,17 @@ void ModFilterWidget::onSideFilterChanged()
{
QString side;
- if (ui->clientSide->isChecked() != ui->serverSide->isChecked()) {
- if (ui->clientSide->isChecked())
- side = "client";
- else
- side = "server";
+ if (ui->clientSide->isChecked() && !ui->serverSide->isChecked()) {
+ side = "client";
+ } else if (!ui->clientSide->isChecked() && ui->serverSide->isChecked()) {
+ side = "server";
+ } else if (ui->clientSide->isChecked() && ui->serverSide->isChecked()) {
+ side = "both";
} else {
- // both are checked or none are checked; in either case no filtering will happen
side = "";
}
+
m_filter_changed = side != m_filter->side;
m_filter->side = side;
if (m_filter_changed)
diff --git a/launcher/ui/widgets/SubTaskProgressBar.ui b/launcher/ui/widgets/SubTaskProgressBar.ui
index 5431eab60..aabb68329 100644
--- a/launcher/ui/widgets/SubTaskProgressBar.ui
+++ b/launcher/ui/widgets/SubTaskProgressBar.ui
@@ -47,6 +47,9 @@
true
+
+ Qt::LinksAccessibleByKeyboard|Qt::LinksAccessibleByMouse|Qt::TextBrowserInteraction|Qt::TextSelectableByKeyboard|Qt::TextSelectableByMouse
+
-
@@ -68,6 +71,9 @@
Qt::AlignRight|Qt::AlignTrailing|Qt::AlignVCenter
+
+ Qt::LinksAccessibleByKeyboard|Qt::LinksAccessibleByMouse|Qt::TextBrowserInteraction|Qt::TextSelectableByKeyboard|Qt::TextSelectableByMouse
+
diff --git a/program_info/AdhocSignedApp.entitlements b/program_info/AdhocSignedApp.entitlements
new file mode 100644
index 000000000..032308a18
--- /dev/null
+++ b/program_info/AdhocSignedApp.entitlements
@@ -0,0 +1,12 @@
+
+
+
+
+ com.apple.security.cs.disable-library-validation
+
+ com.apple.security.device.audio-input
+
+ com.apple.security.device.camera
+
+
+
diff --git a/program_info/App.entitlements b/program_info/App.entitlements
index b46e8ff2a..73bf832c7 100644
--- a/program_info/App.entitlements
+++ b/program_info/App.entitlements
@@ -2,10 +2,6 @@
- com.apple.security.cs.disable-library-validation
-
- com.apple.security.cs.allow-dyld-environment-variables
-
com.apple.security.device.audio-input
com.apple.security.device.camera