aboutsummaryrefslogtreecommitdiff
path: root/launcher
diff options
context:
space:
mode:
Diffstat (limited to 'launcher')
-rw-r--r--launcher/Application.cpp5
-rw-r--r--launcher/CMakeLists.txt22
-rw-r--r--launcher/FileSystem.cpp227
-rw-r--r--launcher/FileSystem.h51
-rw-r--r--launcher/InstanceImportTask.h6
-rw-r--r--launcher/InstanceList.cpp428
-rw-r--r--launcher/InstanceList.h13
-rw-r--r--launcher/LaunchController.cpp20
-rwxr-xr-xlauncher/Launcher.in18
-rw-r--r--launcher/MMCZip.cpp47
-rw-r--r--launcher/MMCZip.h10
-rw-r--r--launcher/ModDownloadTask.cpp14
-rw-r--r--launcher/ModDownloadTask.h7
-rw-r--r--launcher/QObjectPtr.h6
-rw-r--r--launcher/java/JavaUtils.cpp79
-rw-r--r--launcher/java/JavaUtils.h1
-rw-r--r--launcher/launch/LaunchTask.cpp33
-rw-r--r--launcher/launch/LaunchTask.h4
-rw-r--r--launcher/launch/steps/PostLaunchCommand.cpp7
-rw-r--r--launcher/launch/steps/PreLaunchCommand.cpp7
-rw-r--r--launcher/minecraft/MinecraftInstance.cpp24
-rw-r--r--launcher/minecraft/MinecraftInstance.h2
-rw-r--r--launcher/minecraft/World.cpp8
-rw-r--r--launcher/minecraft/World.h6
-rw-r--r--launcher/minecraft/auth/AccountList.cpp11
-rw-r--r--launcher/minecraft/auth/steps/LauncherLoginStep.cpp17
-rw-r--r--launcher/minecraft/auth/steps/MinecraftProfileStep.cpp17
-rw-r--r--launcher/minecraft/auth/steps/MinecraftProfileStepMojang.cpp17
-rw-r--r--launcher/minecraft/auth/steps/XboxAuthorizationStep.cpp21
-rw-r--r--launcher/minecraft/auth/steps/XboxProfileStep.cpp17
-rw-r--r--launcher/minecraft/auth/steps/XboxUserStep.cpp13
-rw-r--r--launcher/minecraft/mod/MetadataHandler.h22
-rw-r--r--launcher/minecraft/mod/Mod.cpp39
-rw-r--r--launcher/minecraft/mod/Mod.h12
-rw-r--r--launcher/minecraft/mod/ModFolderModel.cpp124
-rw-r--r--launcher/minecraft/mod/ModFolderModel.h18
-rw-r--r--launcher/minecraft/mod/tasks/LocalModParseTask.cpp9
-rw-r--r--launcher/minecraft/mod/tasks/LocalModUpdateTask.cpp17
-rw-r--r--launcher/minecraft/mod/tasks/LocalModUpdateTask.h3
-rw-r--r--launcher/minecraft/mod/tasks/ModFolderLoadTask.cpp44
-rw-r--r--launcher/minecraft/mod/tasks/ModFolderLoadTask.h2
-rw-r--r--launcher/modplatform/CheckUpdateTask.h51
-rw-r--r--launcher/modplatform/EnsureMetadataTask.cpp534
-rw-r--r--launcher/modplatform/EnsureMetadataTask.h54
-rw-r--r--launcher/modplatform/ModAPI.h4
-rw-r--r--launcher/modplatform/ModIndex.h8
-rw-r--r--launcher/modplatform/atlauncher/ATLPackInstallTask.h6
-rw-r--r--launcher/modplatform/flame/FileResolvingTask.cpp7
-rw-r--r--launcher/modplatform/flame/FileResolvingTask.h3
-rw-r--r--launcher/modplatform/flame/FlameAPI.cpp148
-rw-r--r--launcher/modplatform/flame/FlameAPI.h8
-rw-r--r--launcher/modplatform/flame/FlameCheckUpdate.cpp179
-rw-r--r--launcher/modplatform/flame/FlameCheckUpdate.h25
-rw-r--r--launcher/modplatform/flame/FlameModIndex.cpp18
-rw-r--r--launcher/modplatform/flame/FlameModIndex.h2
-rw-r--r--launcher/modplatform/helpers/NetworkModAPI.cpp29
-rw-r--r--launcher/modplatform/helpers/NetworkModAPI.h2
-rw-r--r--launcher/modplatform/legacy_ftb/PackInstallTask.h6
-rw-r--r--launcher/modplatform/modpacksch/FTBPackInstallTask.cpp279
-rw-r--r--launcher/modplatform/modpacksch/FTBPackInstallTask.h80
-rw-r--r--launcher/modplatform/modpacksch/FTBPackManifest.cpp46
-rw-r--r--launcher/modplatform/modpacksch/FTBPackManifest.h48
-rw-r--r--launcher/modplatform/modrinth/ModrinthAPI.cpp108
-rw-r--r--launcher/modplatform/modrinth/ModrinthAPI.h28
-rw-r--r--launcher/modplatform/modrinth/ModrinthCheckUpdate.cpp174
-rw-r--r--launcher/modplatform/modrinth/ModrinthCheckUpdate.h23
-rw-r--r--launcher/modplatform/modrinth/ModrinthPackIndex.cpp41
-rw-r--r--launcher/modplatform/modrinth/ModrinthPackIndex.h2
-rw-r--r--launcher/modplatform/packwiz/Packwiz.cpp69
-rw-r--r--launcher/modplatform/packwiz/Packwiz.h23
-rw-r--r--launcher/modplatform/packwiz/Packwiz_test.cpp7
-rw-r--r--launcher/modplatform/technic/SingleZipPackInstallTask.h6
-rw-r--r--launcher/net/NetUtils.h44
-rw-r--r--launcher/net/Upload.cpp10
-rw-r--r--launcher/net/Upload.h2
-rw-r--r--launcher/tasks/MultipleOptionsTask.cpp48
-rw-r--r--launcher/tasks/MultipleOptionsTask.h19
-rw-r--r--launcher/tasks/SequentialTask.cpp20
-rw-r--r--launcher/tasks/SequentialTask.h16
-rw-r--r--launcher/ui/MainWindow.cpp32
-rw-r--r--launcher/ui/MainWindow.h1
-rw-r--r--launcher/ui/dialogs/ChooseProviderDialog.cpp96
-rw-r--r--launcher/ui/dialogs/ChooseProviderDialog.h56
-rw-r--r--launcher/ui/dialogs/ChooseProviderDialog.ui89
-rw-r--r--launcher/ui/dialogs/ModUpdateDialog.cpp409
-rw-r--r--launcher/ui/dialogs/ModUpdateDialog.h62
-rw-r--r--launcher/ui/dialogs/ProgressDialog.cpp38
-rw-r--r--launcher/ui/dialogs/ReviewMessageBox.cpp2
-rw-r--r--launcher/ui/dialogs/ScrollMessageBox.ui2
-rw-r--r--launcher/ui/dialogs/SkinUploadDialog.cpp90
-rw-r--r--launcher/ui/dialogs/SkinUploadDialog.ui6
-rw-r--r--launcher/ui/pages/instance/ExternalResourcesPage.cpp8
-rw-r--r--launcher/ui/pages/instance/ExternalResourcesPage.ui11
-rw-r--r--launcher/ui/pages/instance/InstanceSettingsPage.cpp2
-rw-r--r--launcher/ui/pages/instance/ModFolderPage.cpp83
-rw-r--r--launcher/ui/pages/instance/ModFolderPage.h1
-rw-r--r--launcher/ui/pages/modplatform/flame/FlameModPage.cpp2
-rw-r--r--launcher/ui/pages/modplatform/ftb/FtbPage.cpp3
-rw-r--r--launcher/ui/themes/DarkTheme.cpp1
-rw-r--r--launcher/ui/widgets/WideBar.cpp49
-rw-r--r--launcher/ui/widgets/WideBar.h31
101 files changed, 3728 insertions, 971 deletions
diff --git a/launcher/Application.cpp b/launcher/Application.cpp
index 41be6a5b..cb8088be 100644
--- a/launcher/Application.cpp
+++ b/launcher/Application.cpp
@@ -321,7 +321,7 @@ Application::Application(int &argc, char **argv) : QApplication(argc, argv)
{
// Root path is used for updates and portable data
-#if defined(Q_OS_LINUX) || defined(Q_OS_FREEBSD)
+#if defined(Q_OS_LINUX) || defined(Q_OS_FREEBSD) || defined(Q_OS_OPENBSD)
QDir foo(FS::PathCombine(binPath, "..")); // typically portable-root or /usr
m_rootPath = foo.absolutePath();
#elif defined(Q_OS_WIN32)
@@ -1259,6 +1259,9 @@ bool Application::launch(
}
connect(controller.get(), &LaunchController::succeeded, this, &Application::controllerSucceeded);
connect(controller.get(), &LaunchController::failed, this, &Application::controllerFailed);
+ connect(controller.get(), &LaunchController::aborted, this, [this] {
+ controllerFailed(tr("Aborted"));
+ });
addRunningInstance();
controller->start();
return true;
diff --git a/launcher/CMakeLists.txt b/launcher/CMakeLists.txt
index ecdeaac0..9c5c2ea8 100644
--- a/launcher/CMakeLists.txt
+++ b/launcher/CMakeLists.txt
@@ -117,6 +117,7 @@ set(NET_SOURCES
net/NetAction.h
net/NetJob.cpp
net/NetJob.h
+ net/NetUtils.h
net/PasteUpload.cpp
net/PasteUpload.h
net/Sink.h
@@ -397,6 +398,8 @@ set(TASKS_SOURCES
tasks/ConcurrentTask.cpp
tasks/SequentialTask.h
tasks/SequentialTask.cpp
+ tasks/MultipleOptionsTask.h
+ tasks/MultipleOptionsTask.cpp
)
ecm_add_test(tasks/Task_test.cpp LINK_LIBRARIES Launcher_logic Qt${QT_VERSION_MAJOR}::Test
@@ -480,9 +483,15 @@ set(API_SOURCES
modplatform/ModAPI.h
+ modplatform/EnsureMetadataTask.h
+ modplatform/EnsureMetadataTask.cpp
+
+ modplatform/CheckUpdateTask.h
+
modplatform/flame/FlameAPI.h
+ modplatform/flame/FlameAPI.cpp
modplatform/modrinth/ModrinthAPI.h
-
+ modplatform/modrinth/ModrinthAPI.cpp
modplatform/helpers/NetworkModAPI.h
modplatform/helpers/NetworkModAPI.cpp
)
@@ -508,6 +517,8 @@ set(FLAME_SOURCES
modplatform/flame/PackManifest.cpp
modplatform/flame/FileResolvingTask.h
modplatform/flame/FileResolvingTask.cpp
+ modplatform/flame/FlameCheckUpdate.cpp
+ modplatform/flame/FlameCheckUpdate.h
)
set(MODRINTH_SOURCES
@@ -515,6 +526,8 @@ set(MODRINTH_SOURCES
modplatform/modrinth/ModrinthPackIndex.h
modplatform/modrinth/ModrinthPackManifest.cpp
modplatform/modrinth/ModrinthPackManifest.h
+ modplatform/modrinth/ModrinthCheckUpdate.cpp
+ modplatform/modrinth/ModrinthCheckUpdate.h
)
set(MODPACKSCH_SOURCES
@@ -836,6 +849,10 @@ SET(LAUNCHER_SOURCES
ui/dialogs/ModDownloadDialog.h
ui/dialogs/ScrollMessageBox.cpp
ui/dialogs/ScrollMessageBox.h
+ ui/dialogs/ChooseProviderDialog.h
+ ui/dialogs/ChooseProviderDialog.cpp
+ ui/dialogs/ModUpdateDialog.cpp
+ ui/dialogs/ModUpdateDialog.h
# GUI - widgets
ui/widgets/Common.cpp
@@ -941,6 +958,7 @@ qt_wrap_ui(LAUNCHER_UI
ui/dialogs/EditAccountDialog.ui
ui/dialogs/ReviewMessageBox.ui
ui/dialogs/ScrollMessageBox.ui
+ ui/dialogs/ChooseProviderDialog.ui
)
qt_add_resources(LAUNCHER_RESOURCES
@@ -967,9 +985,9 @@ add_library(Launcher_logic STATIC ${LOGIC_SOURCES} ${LAUNCHER_SOURCES} ${LAUNCHE
target_link_libraries(Launcher_logic
systeminfo
Launcher_classparser
+ Launcher_murmur2
nbt++
${ZLIB_LIBRARIES}
- optional-bare
tomlc99
BuildConfig
Katabasis
diff --git a/launcher/FileSystem.cpp b/launcher/FileSystem.cpp
index ebb4460d..21edbb48 100644
--- a/launcher/FileSystem.cpp
+++ b/launcher/FileSystem.cpp
@@ -35,76 +35,64 @@
#include "FileSystem.h"
+#include <QDebug>
#include <QDir>
#include <QFile>
-#include <QSaveFile>
#include <QFileInfo>
-#include <QDebug>
-#include <QUrl>
+#include <QSaveFile>
#include <QStandardPaths>
#include <QTextStream>
+#include <QUrl>
#if defined Q_OS_WIN32
- #include <windows.h>
- #include <string>
- #include <sys/utime.h>
- #include <winnls.h>
- #include <shobjidl.h>
- #include <objbase.h>
- #include <objidl.h>
- #include <shlguid.h>
- #include <shlobj.h>
+#include <objbase.h>
+#include <objidl.h>
+#include <shlguid.h>
+#include <shlobj.h>
+#include <shobjidl.h>
+#include <sys/utime.h>
+#include <windows.h>
+#include <winnls.h>
+#include <string>
#else
- #include <utime.h>
+#include <utime.h>
#endif
namespace FS {
-void ensureExists(const QDir &dir)
+void ensureExists(const QDir& dir)
{
- if (!QDir().mkpath(dir.absolutePath()))
- {
- throw FileSystemException("Unable to create folder " + dir.dirName() + " (" +
- dir.absolutePath() + ")");
+ if (!QDir().mkpath(dir.absolutePath())) {
+ throw FileSystemException("Unable to create folder " + dir.dirName() + " (" + dir.absolutePath() + ")");
}
}
-void write(const QString &filename, const QByteArray &data)
+void write(const QString& filename, const QByteArray& data)
{
ensureExists(QFileInfo(filename).dir());
QSaveFile file(filename);
- if (!file.open(QSaveFile::WriteOnly))
- {
- throw FileSystemException("Couldn't open " + filename + " for writing: " +
- file.errorString());
+ if (!file.open(QSaveFile::WriteOnly)) {
+ throw FileSystemException("Couldn't open " + filename + " for writing: " + file.errorString());
}
- if (data.size() != file.write(data))
- {
- throw FileSystemException("Error writing data to " + filename + ": " +
- file.errorString());
+ if (data.size() != file.write(data)) {
+ throw FileSystemException("Error writing data to " + filename + ": " + file.errorString());
}
- if (!file.commit())
- {
- throw FileSystemException("Error while committing data to " + filename + ": " +
- file.errorString());
+ if (!file.commit()) {
+ throw FileSystemException("Error while committing data to " + filename + ": " + file.errorString());
}
}
-QByteArray read(const QString &filename)
+QByteArray read(const QString& filename)
{
QFile file(filename);
- if (!file.open(QFile::ReadOnly))
- {
- throw FileSystemException("Unable to open " + filename + " for reading: " +
- file.errorString());
+ if (!file.open(QFile::ReadOnly)) {
+ throw FileSystemException("Unable to open " + filename + " for reading: " + file.errorString());
}
const qint64 size = file.size();
QByteArray data(int(size), 0);
const qint64 ret = file.read(data.data(), size);
- if (ret == -1 || ret != size)
- {
- throw FileSystemException("Error reading data from " + filename + ": " +
- file.errorString());
+ if (ret == -1 || ret != size) {
+ throw FileSystemException("Error reading data from " + filename + ": " + file.errorString());
}
return data;
}
@@ -138,12 +126,12 @@ bool ensureFolderPathExists(QString foldernamepath)
return success;
}
-bool copy::operator()(const QString &offset)
+bool copy::operator()(const QString& offset)
{
- //NOTE always deep copy on windows. the alternatives are too messy.
- #if defined Q_OS_WIN32
+// NOTE always deep copy on windows. the alternatives are too messy.
+#if defined Q_OS_WIN32
m_followSymlinks = true;
- #endif
+#endif
auto src = PathCombine(m_src.absolutePath(), offset);
auto dst = PathCombine(m_dst.absolutePath(), offset);
@@ -152,52 +140,39 @@ bool copy::operator()(const QString &offset)
if (!currentSrc.exists())
return false;
- if(!m_followSymlinks && currentSrc.isSymLink())
- {
+ if (!m_followSymlinks && currentSrc.isSymLink()) {
qDebug() << "creating symlink" << src << " - " << dst;
- if (!ensureFilePathExists(dst))
- {
+ if (!ensureFilePathExists(dst)) {
qWarning() << "Cannot create path!";
return false;
}
return QFile::link(currentSrc.symLinkTarget(), dst);
- }
- else if(currentSrc.isFile())
- {
+ } else if (currentSrc.isFile()) {
qDebug() << "copying file" << src << " - " << dst;
- if (!ensureFilePathExists(dst))
- {
+ if (!ensureFilePathExists(dst)) {
qWarning() << "Cannot create path!";
return false;
}
return QFile::copy(src, dst);
- }
- else if(currentSrc.isDir())
- {
+ } else if (currentSrc.isDir()) {
qDebug() << "recursing" << offset;
- if (!ensureFolderPathExists(dst))
- {
+ if (!ensureFolderPathExists(dst)) {
qWarning() << "Cannot create path!";
return false;
}
QDir currentDir(src);
- for(auto & f : currentDir.entryList(QDir::Files | QDir::Dirs | QDir::NoDotAndDotDot | QDir::Hidden | QDir::System))
- {
+ for (auto& f : currentDir.entryList(QDir::Files | QDir::Dirs | QDir::NoDotAndDotDot | QDir::Hidden | QDir::System)) {
auto inner_offset = PathCombine(offset, f);
// ignore and skip stuff that matches the blacklist.
- if(m_blacklist && m_blacklist->matches(inner_offset))
- {
+ if (m_blacklist && m_blacklist->matches(inner_offset)) {
continue;
}
- if(!operator()(inner_offset))
- {
+ if (!operator()(inner_offset)) {
qWarning() << "Failed to copy" << inner_offset;
return false;
}
}
- }
- else
- {
+ } else {
qCritical() << "Copy ERROR: Unknown filesystem object:" << src;
return false;
}
@@ -208,55 +183,41 @@ bool deletePath(QString path)
{
bool OK = true;
QFileInfo finfo(path);
- if(finfo.isFile()) {
+ if (finfo.isFile()) {
return QFile::remove(path);
}
QDir dir(path);
- if (!dir.exists())
- {
+ if (!dir.exists()) {
return OK;
}
- auto allEntries = dir.entryInfoList(QDir::NoDotAndDotDot | QDir::System | QDir::Hidden |
- QDir::AllDirs | QDir::Files,
- QDir::DirsFirst);
+ auto allEntries = dir.entryInfoList(QDir::NoDotAndDotDot | QDir::System | QDir::Hidden | QDir::AllDirs | QDir::Files, QDir::DirsFirst);
- for(auto & info: allEntries)
- {
+ for (auto& info : allEntries) {
#if defined Q_OS_WIN32
QString nativePath = QDir::toNativeSeparators(info.absoluteFilePath());
auto wString = nativePath.toStdWString();
DWORD dwAttrs = GetFileAttributesW(wString.c_str());
// Windows: check for junctions, reparse points and other nasty things of that sort
- if(dwAttrs & FILE_ATTRIBUTE_REPARSE_POINT)
- {
- if (info.isFile())
- {
+ if (dwAttrs & FILE_ATTRIBUTE_REPARSE_POINT) {
+ if (info.isFile()) {
OK &= QFile::remove(info.absoluteFilePath());
- }
- else if (info.isDir())
- {
+ } else if (info.isDir()) {
OK &= dir.rmdir(info.absoluteFilePath());
}
}
#else
// We do not trust Qt with reparse points, but do trust it with unix symlinks.
- if(info.isSymLink())
- {
+ if (info.isSymLink()) {
OK &= QFile::remove(info.absoluteFilePath());
}
#endif
- else if (info.isDir())
- {
+ else if (info.isDir()) {
OK &= deletePath(info.absoluteFilePath());
- }
- else if (info.isFile())
- {
+ } else if (info.isFile()) {
OK &= QFile::remove(info.absoluteFilePath());
- }
- else
- {
+ } else {
OK = false;
qCritical() << "Delete ERROR: Unknown filesystem object:" << info.absoluteFilePath();
}
@@ -265,22 +226,30 @@ bool deletePath(QString path)
return OK;
}
+bool trash(QString path, QString *pathInTrash = nullptr)
+{
+#if QT_VERSION < QT_VERSION_CHECK(5, 15, 0)
+ return false;
+#else
+ return QFile::moveToTrash(path, pathInTrash);
+#endif
+}
-QString PathCombine(const QString & path1, const QString & path2)
+QString PathCombine(const QString& path1, const QString& path2)
{
- if(!path1.size())
+ if (!path1.size())
return path2;
- if(!path2.size())
+ if (!path2.size())
return path1;
return QDir::cleanPath(path1 + QDir::separator() + path2);
}
-QString PathCombine(const QString & path1, const QString & path2, const QString & path3)
+QString PathCombine(const QString& path1, const QString& path2, const QString& path3)
{
return PathCombine(PathCombine(path1, path2), path3);
}
-QString PathCombine(const QString & path1, const QString & path2, const QString & path3, const QString & path4)
+QString PathCombine(const QString& path1, const QString& path2, const QString& path3, const QString& path4)
{
return PathCombine(PathCombine(path1, path2, path3), path4);
}
@@ -292,17 +261,14 @@ QString AbsolutePath(QString path)
QString ResolveExecutable(QString path)
{
- if (path.isEmpty())
- {
+ if (path.isEmpty()) {
return QString();
}
- if(!path.contains('/'))
- {
+ if (!path.contains('/')) {
path = QStandardPaths::findExecutable(path);
}
QFileInfo pathInfo(path);
- if(!pathInfo.exists() || !pathInfo.isExecutable())
- {
+ if (!pathInfo.exists() || !pathInfo.isExecutable()) {
return QString();
}
return pathInfo.absoluteFilePath();
@@ -322,12 +288,9 @@ QString NormalizePath(QString path)
QDir b(path);
QString newAbsolute = b.absolutePath();
- if (newAbsolute.startsWith(currentAbsolute))
- {
+ if (newAbsolute.startsWith(currentAbsolute)) {
return a.relativeFilePath(newAbsolute);
- }
- else
- {
+ } else {
return newAbsolute;
}
}
@@ -336,10 +299,8 @@ QString badFilenameChars = "\"\\/?<>:;*|!+\r\n";
QString RemoveInvalidFilenameChars(QString string, QChar replaceWith)
{
- for (int i = 0; i < string.length(); i++)
- {
- if (badFilenameChars.contains(string[i]))
- {
+ for (int i = 0; i < string.length(); i++) {
+ if (badFilenameChars.contains(string[i])) {
string[i] = replaceWith;
}
}
@@ -351,15 +312,12 @@ QString DirNameFromString(QString string, QString inDir)
int num = 0;
QString baseName = RemoveInvalidFilenameChars(string, '-');
QString dirName;
- do
- {
- if(num == 0)
- {
+ do {
+ if (num == 0) {
dirName = baseName;
- }
- else
- {
- dirName = baseName + QString::number(num);;
+ } else {
+ dirName = baseName + QString::number(num);
+ ;
}
// If it's over 9000
@@ -383,36 +341,31 @@ bool checkProblemticPathJava(QDir folder)
bool called_coinit = false;
-HRESULT CreateLink(LPCSTR linkPath, LPCSTR targetPath, LPCSTR args)
+HRESULT CreateLink(LPCCH linkPath, LPCWSTR targetPath, LPCWSTR args)
{
HRESULT hres;
- if (!called_coinit)
- {
+ if (!called_coinit) {
hres = CoInitialize(NULL);
called_coinit = true;
- if (!SUCCEEDED(hres))
- {
+ if (!SUCCEEDED(hres)) {
qWarning("Failed to initialize COM. Error 0x%08lX", hres);
return hres;
}
}
- IShellLinkA *link;
- hres = CoCreateInstance(CLSID_ShellLink, NULL, CLSCTX_INPROC_SERVER, IID_IShellLink,
- (LPVOID *)&link);
+ IShellLink* link;
+ hres = CoCreateInstance(CLSID_ShellLink, NULL, CLSCTX_INPROC_SERVER, IID_IShellLink, (LPVOID*)&link);
- if (SUCCEEDED(hres))
- {
- IPersistFile *persistFile;
+ if (SUCCEEDED(hres)) {
+ IPersistFile* persistFile;
link->SetPath(targetPath);
link->SetArguments(args);
- hres = link->QueryInterface(IID_IPersistFile, (LPVOID *)&persistFile);
- if (SUCCEEDED(hres))
- {
+ hres = link->QueryInterface(IID_IPersistFile, (LPVOID*)&persistFile);
+ if (SUCCEEDED(hres)) {
WCHAR wstr[MAX_PATH];
MultiByteToWideChar(CP_ACP, 0, linkPath, -1, wstr, MAX_PATH);
@@ -433,8 +386,7 @@ QString getDesktopDir()
}
// Cross-platform Shortcut creation
-bool createShortCut(QString location, QString dest, QStringList args, QString name,
- QString icon)
+bool createShortCut(QString location, QString dest, QStringList args, QString name, QString icon)
{
#if defined(Q_OS_LINUX) || defined(Q_OS_FREEBSD)
location = PathCombine(location, name + ".desktop");
@@ -459,8 +411,7 @@ bool createShortCut(QString location, QString dest, QStringList args, QString na
stream.flush();
f.close();
- f.setPermissions(f.permissions() | QFileDevice::ExeOwner | QFileDevice::ExeGroup |
- QFileDevice::ExeOther);
+ f.setPermissions(f.permissions() | QFileDevice::ExeOwner | QFileDevice::ExeGroup | QFileDevice::ExeOther);
return true;
#elif defined Q_OS_WIN
diff --git a/launcher/FileSystem.h b/launcher/FileSystem.h
index fd305b01..b46f3281 100644
--- a/launcher/FileSystem.h
+++ b/launcher/FileSystem.h
@@ -41,29 +41,27 @@
#include <QDir>
#include <QFlags>
-namespace FS
-{
+namespace FS {
-class FileSystemException : public ::Exception
-{
-public:
- FileSystemException(const QString &message) : Exception(message) {}
+class FileSystemException : public ::Exception {
+ public:
+ FileSystemException(const QString& message) : Exception(message) {}
};
/**
* write data to a file safely
*/
-void write(const QString &filename, const QByteArray &data);
+void write(const QString& filename, const QByteArray& data);
/**
* read data from a file safely\
*/
-QByteArray read(const QString &filename);
+QByteArray read(const QString& filename);
/**
* Update the last changed timestamp of an existing file
*/
-bool updateTimestamp(const QString & filename);
+bool updateTimestamp(const QString& filename);
/**
* Creates all the folders in a path for the specified path
@@ -77,35 +75,31 @@ bool ensureFilePathExists(QString filenamepath);
*/
bool ensureFolderPathExists(QString filenamepath);
-class copy
-{
-public:
- copy(const QString & src, const QString & dst)
+class copy {
+ public:
+ copy(const QString& src, const QString& dst)
{
m_src.setPath(src);
m_dst.setPath(dst);
}
- copy & followSymlinks(const bool follow)
+ copy& followSymlinks(const bool follow)
{
m_followSymlinks = follow;
return *this;
}
- copy & blacklist(const IPathMatcher * filter)
+ copy& blacklist(const IPathMatcher* filter)
{
m_blacklist = filter;
return *this;
}
- bool operator()()
- {
- return operator()(QString());
- }
+ bool operator()() { return operator()(QString()); }
-private:
- bool operator()(const QString &offset);
+ private:
+ bool operator()(const QString& offset);
-private:
+ private:
bool m_followSymlinks = true;
- const IPathMatcher * m_blacklist = nullptr;
+ const IPathMatcher* m_blacklist = nullptr;
QDir m_src;
QDir m_dst;
};
@@ -115,9 +109,14 @@ private:
*/
bool deletePath(QString path);
-QString PathCombine(const QString &path1, const QString &path2);
-QString PathCombine(const QString &path1, const QString &path2, const QString &path3);
-QString PathCombine(const QString &path1, const QString &path2, const QString &path3, const QString &path4);
+/**
+ * Trash a folder / file
+ */
+bool trash(QString path, QString *pathInTrash);
+
+QString PathCombine(const QString& path1, const QString& path2);
+QString PathCombine(const QString& path1, const QString& path2, const QString& path3);
+QString PathCombine(const QString& path1, const QString& path2, const QString& path3, const QString& path4);
QString AbsolutePath(QString path);
diff --git a/launcher/InstanceImportTask.h b/launcher/InstanceImportTask.h
index b67d48f3..48ba2161 100644
--- a/launcher/InstanceImportTask.h
+++ b/launcher/InstanceImportTask.h
@@ -44,7 +44,7 @@
#include "QObjectPtr.h"
#include "modplatform/flame/PackManifest.h"
-#include <nonstd/optional>
+#include <optional>
class QuaZip;
namespace Flame
@@ -90,8 +90,8 @@ private: /* data */
QString m_archivePath;
bool m_downloadRequired = false;
std::unique_ptr<QuaZip> m_packZip;
- QFuture<nonstd::optional<QStringList>> m_extractFuture;
- QFutureWatcher<nonstd::optional<QStringList>> m_extractFutureWatcher;
+ QFuture<std::optional<QStringList>> m_extractFuture;
+ QFutureWatcher<std::optional<QStringList>> m_extractFutureWatcher;
QVector<Flame::File> m_blockedMods;
enum class ModpackType{
Unknown,
diff --git a/launcher/InstanceList.cpp b/launcher/InstanceList.cpp
index fb7103dd..4447a17c 100644
--- a/launcher/InstanceList.cpp
+++ b/launcher/InstanceList.cpp
@@ -33,30 +33,32 @@
* limitations under the License.
*/
+#include <QDebug>
#include <QDir>
#include <QDirIterator>
-#include <QSet>
#include <QFile>
-#include <QThread>
-#include <QTextStream>
-#include <QXmlStreamReader>
-#include <QTimer>
-#include <QDebug>
#include <QFileSystemWatcher>
-#include <QUuid>
#include <QJsonArray>
#include <QJsonDocument>
#include <QMimeData>
+#include <QSet>
+#include <QStack>
+#include <QPair>
+#include <QTextStream>
+#include <QThread>
+#include <QTimer>
+#include <QUuid>
+#include <QXmlStreamReader>
-#include "InstanceList.h"
#include "BaseInstance.h"
+#include "ExponentialSeries.h"
+#include "FileSystem.h"
+#include "InstanceList.h"
#include "InstanceTask.h"
-#include "settings/INISettingsObject.h"
#include "NullInstance.h"
-#include "minecraft/MinecraftInstance.h"
-#include "FileSystem.h"
-#include "ExponentialSeries.h"
#include "WatchLock.h"
+#include "minecraft/MinecraftInstance.h"
+#include "settings/INISettingsObject.h"
#ifdef Q_OS_WIN32
#include <Windows.h>
@@ -64,13 +66,12 @@
const static int GROUP_FILE_FORMAT_VERSION = 1;
-InstanceList::InstanceList(SettingsObjectPtr settings, const QString & instDir, QObject *parent)
+InstanceList::InstanceList(SettingsObjectPtr settings, const QString& instDir, QObject* parent)
: QAbstractListModel(parent), m_globalSettings(settings)
{
resumeWatch();
// Create aand normalize path
- if (!QDir::current().exists(instDir))
- {
+ if (!QDir::current().exists(instDir)) {
QDir::current().mkpath(instDir);
}
@@ -83,9 +84,7 @@ InstanceList::InstanceList(SettingsObjectPtr settings, const QString & instDir,
m_watcher->addPath(m_instDir);
}
-InstanceList::~InstanceList()
-{
-}
+InstanceList::~InstanceList() {}
Qt::DropActions InstanceList::supportedDragActions() const
{
@@ -99,7 +98,7 @@ Qt::DropActions InstanceList::supportedDropActions() const
bool InstanceList::canDropMimeData(const QMimeData* data, Qt::DropAction action, int row, int column, const QModelIndex& parent) const
{
- if(data && data->hasFormat("application/x-instanceid")) {
+ if (data && data->hasFormat("application/x-instanceid")) {
return true;
}
return false;
@@ -107,7 +106,7 @@ bool InstanceList::canDropMimeData(const QMimeData* data, Qt::DropAction action,
bool InstanceList::dropMimeData(const QMimeData* data, Qt::DropAction action, int row, int column, const QModelIndex& parent)
{
- if(data && data->hasFormat("application/x-instanceid")) {
+ if (data && data->hasFormat("application/x-instanceid")) {
return true;
}
return false;
@@ -120,35 +119,33 @@ QStringList InstanceList::mimeTypes() const
return types;
}
-QMimeData * InstanceList::mimeData(const QModelIndexList& indexes) const
+QMimeData* InstanceList::mimeData(const QModelIndexList& indexes) const
{
auto mimeData = QAbstractListModel::mimeData(indexes);
- if(indexes.size() == 1) {
+ if (indexes.size() == 1) {
auto instanceId = data(indexes[0], InstanceIDRole).toString();
mimeData->setData("application/x-instanceid", instanceId.toUtf8());
}
return mimeData;
}
-
-int InstanceList::rowCount(const QModelIndex &parent) const
+int InstanceList::rowCount(const QModelIndex& parent) const
{
Q_UNUSED(parent);
return m_instances.count();
}
-QModelIndex InstanceList::index(int row, int column, const QModelIndex &parent) const
+QModelIndex InstanceList::index(int row, int column, const QModelIndex& parent) const
{
Q_UNUSED(parent);
if (row < 0 || row >= m_instances.size())
return QModelIndex();
- return createIndex(row, column, (void *)m_instances.at(row).get());
+ return createIndex(row, column, (void*)m_instances.at(row).get());
}
-QVariant InstanceList::data(const QModelIndex &index, int role) const
+QVariant InstanceList::data(const QModelIndex& index, int role) const
{
- if (!index.isValid())
- {
+ if (!index.isValid()) {
return QVariant();
}
BaseInstance *pdata = static_cast<BaseInstance *>(index.internalPointer());
@@ -193,29 +190,25 @@ QVariant InstanceList::data(const QModelIndex &index, int role) const
bool InstanceList::setData(const QModelIndex& index, const QVariant& value, int role)
{
- if (!index.isValid())
- {
+ if (!index.isValid()) {
return false;
}
- if(role != Qt::EditRole)
- {
+ if (role != Qt::EditRole) {
return false;
}
- BaseInstance *pdata = static_cast<BaseInstance *>(index.internalPointer());
+ BaseInstance* pdata = static_cast<BaseInstance*>(index.internalPointer());
auto newName = value.toString();
- if(pdata->name() == newName)
- {
+ if (pdata->name() == newName) {
return true;
}
pdata->setName(newName);
return true;
}
-Qt::ItemFlags InstanceList::flags(const QModelIndex &index) const
+Qt::ItemFlags InstanceList::flags(const QModelIndex& index) const
{
Qt::ItemFlags f;
- if (index.isValid())
- {
+ if (index.isValid()) {
f |= (Qt::ItemIsEnabled | Qt::ItemIsSelectable | Qt::ItemIsEditable);
}
return f;
@@ -224,13 +217,11 @@ Qt::ItemFlags InstanceList::flags(const QModelIndex &index) const
GroupId InstanceList::getInstanceGroup(const InstanceId& id) const
{
auto inst = getInstanceById(id);
- if(!inst)
- {
+ if (!inst) {
return GroupId();
}
auto iter = m_instanceGroupIndex.find(inst->id());
- if(iter != m_instanceGroupIndex.end())
- {
+ if (iter != m_instanceGroupIndex.end()) {
return *iter;
}
return GroupId();
@@ -239,33 +230,27 @@ GroupId InstanceList::getInstanceGroup(const InstanceId& id) const
void InstanceList::setInstanceGroup(const InstanceId& id, const GroupId& name)
{
auto inst = getInstanceById(id);
- if(!inst)
- {
+ if (!inst) {
qDebug() << "Attempt to set a null instance's group";
return;
}
bool changed = false;
auto iter = m_instanceGroupIndex.find(inst->id());
- if(iter != m_instanceGroupIndex.end())
- {
- if(*iter != name)
- {
+ if (iter != m_instanceGroupIndex.end()) {
+ if (*iter != name) {
*iter = name;
changed = true;
}
- }
- else
- {
+ } else {
changed = true;
m_instanceGroupIndex[id] = name;
}
- if(changed)
- {
+ if (changed) {
m_groupNameCache.insert(name);
auto idx = getInstIndex(inst.get());
- emit dataChanged(index(idx), index(idx), {GroupRole});
+ emit dataChanged(index(idx), index(idx), { GroupRole });
saveGroupList();
}
}
@@ -279,24 +264,20 @@ void InstanceList::deleteGroup(const QString& name)
{
bool removed = false;
qDebug() << "Delete group" << name;
- for(auto & instance: m_instances)
- {
- const auto & instID = instance->id();
+ for (auto& instance : m_instances) {
+ const auto& instID = instance->id();
auto instGroupName = getInstanceGroup(instID);
- if(instGroupName == name)
- {
+ if (instGroupName == name) {
m_instanceGroupIndex.remove(instID);
qDebug() << "Remove" << instID << "from group" << name;
removed = true;
auto idx = getInstIndex(instance.get());
- if(idx > 0)
- {
- emit dataChanged(index(idx), index(idx), {GroupRole});
+ if (idx > 0) {
+ emit dataChanged(index(idx), index(idx), { GroupRole });
}
}
}
- if(removed)
- {
+ if (removed) {
saveGroupList();
}
}
@@ -306,23 +287,75 @@ bool InstanceList::isGroupCollapsed(const QString& group)
return m_collapsedGroups.contains(group);
}
+bool InstanceList::trashInstance(const InstanceId& id)
+{
+ auto inst = getInstanceById(id);
+ if (!inst) {
+ qDebug() << "Cannot trash instance" << id << ". No such instance is present (deleted externally?).";
+ return false;
+ }
+
+ auto cachedGroupId = m_instanceGroupIndex[id];
+
+ qDebug() << "Will trash instance" << id;
+ QString trashedLoc;
+
+ if (m_instanceGroupIndex.remove(id)) {
+ saveGroupList();
+ }
+
+ if (!FS::trash(inst->instanceRoot(), &trashedLoc)) {
+ qDebug() << "Trash of instance" << id << "has not been completely successfully...";
+ return false;
+ }
+
+ qDebug() << "Instance" << id << "has been trashed by the launcher.";
+ m_trashHistory.push({id, inst->instanceRoot(), trashedLoc, cachedGroupId});
+
+ return true;
+}
+
+bool InstanceList::trashedSomething() {
+ return !m_trashHistory.empty();
+}
+
+void InstanceList::undoTrashInstance() {
+ if (m_trashHistory.empty()) {
+ qWarning() << "Nothing to recover from trash.";
+ return;
+ }
+
+ auto top = m_trashHistory.pop();
+
+ while (QDir(top.polyPath).exists()) {
+ top.id += "1";
+ top.polyPath += "1";
+ }
+
+ qDebug() << "Moving" << top.trashPath << "back to" << top.polyPath;
+ QFile(top.trashPath).rename(top.polyPath);
+
+ m_instanceGroupIndex[top.id] = top.groupName;
+ m_groupNameCache.insert(top.groupName);
+
+ saveGroupList();
+ emit instancesChanged();
+}
+
void InstanceList::deleteInstance(const InstanceId& id)
{
auto inst = getInstanceById(id);
- if(!inst)
- {
+ if (!inst) {
qDebug() << "Cannot delete instance" << id << ". No such instance is present (deleted externally?).";
return;
}
- if(m_instanceGroupIndex.remove(id))
- {
+ if (m_instanceGroupIndex.remove(id)) {
saveGroupList();
}
qDebug() << "Will delete instance" << id;
- if(!FS::deletePath(inst->instanceRoot()))
- {
+ if (!FS::deletePath(inst->instanceRoot())) {
qWarning() << "Deletion of instance" << id << "has not been completely successful ...";
return;
}
@@ -330,15 +363,13 @@ void InstanceList::deleteInstance(const InstanceId& id)
qDebug() << "Instance" << id << "has been deleted by the launcher.";
}
-static QMap<InstanceId, InstanceLocator> getIdMapping(const QList<InstancePtr> &list)
+static QMap<InstanceId, InstanceLocator> getIdMapping(const QList<InstancePtr>& list)
{
QMap<InstanceId, InstanceLocator> out;
int i = 0;
- for(auto & item: list)
- {
+ for (auto& item : list) {
auto id = item->id();
- if(out.contains(id))
- {
+ if (out.contains(id)) {
qWarning() << "Duplicate ID" << id << "in instance list";
}
out[id] = std::make_pair(item, i);
@@ -347,24 +378,21 @@ static QMap<InstanceId, InstanceLocator> getIdMapping(const QList<InstancePtr> &
return out;
}
-QList< InstanceId > InstanceList::discoverInstances()
+QList<InstanceId> InstanceList::discoverInstances()
{
qDebug() << "Discovering instances in" << m_instDir;
QList<InstanceId> out;
QDirIterator iter(m_instDir, QDir::Dirs | QDir::NoDot | QDir::NoDotDot | QDir::Readable | QDir::Hidden, QDirIterator::FollowSymlinks);
- while (iter.hasNext())
- {
+ while (iter.hasNext()) {
QString subDir = iter.next();
QFileInfo dirInfo(subDir);
if (!QFileInfo(FS::PathCombine(subDir, "instance.cfg")).exists())
continue;
// if it is a symlink, ignore it if it goes to the instance folder
- if(dirInfo.isSymLink())
- {
+ if (dirInfo.isSymLink()) {
QFileInfo targetInfo(dirInfo.symLinkTarget());
QFileInfo instDirInfo(m_instDir);
- if(targetInfo.canonicalPath() == instDirInfo.canonicalFilePath())
- {
+ if (targetInfo.canonicalPath() == instDirInfo.canonicalFilePath()) {
qDebug() << "Ignoring symlink" << subDir << "that leads into the instances folder";
continue;
}
@@ -388,74 +416,56 @@ InstanceList::InstListError InstanceList::loadList()
QList<InstancePtr> newList;
- for(auto & id: discoverInstances())
- {
- if(existingIds.contains(id))
- {
+ for (auto& id : discoverInstances()) {
+ if (existingIds.contains(id)) {
auto instPair = existingIds[id];
existingIds.remove(id);
qDebug() << "Should keep and soft-reload" << id;
- }
- else
- {
+ } else {
InstancePtr instPtr = loadInstance(id);
- if(instPtr)
- {
+ if (instPtr) {
newList.append(instPtr);
}
}
}
// TODO: looks like a general algorithm with a few specifics inserted. Do something about it.
- if(!existingIds.isEmpty())
- {
+ if (!existingIds.isEmpty()) {
// get the list of removed instances and sort it by their original index, from last to first
auto deadList = existingIds.values();
- auto orderSortPredicate = [](const InstanceLocator & a, const InstanceLocator & b) -> bool
- {
- return a.second > b.second;
- };
+ auto orderSortPredicate = [](const InstanceLocator& a, const InstanceLocator& b) -> bool { return a.second > b.second; };
std::sort(deadList.begin(), deadList.end(), orderSortPredicate);
// remove the contiguous ranges of rows
int front_bookmark = -1;
int back_bookmark = -1;
int currentItem = -1;
- auto removeNow = [&]()
- {
+ auto removeNow = [&]() {
beginRemoveRows(QModelIndex(), front_bookmark, back_bookmark);
m_instances.erase(m_instances.begin() + front_bookmark, m_instances.begin() + back_bookmark + 1);
endRemoveRows();
front_bookmark = -1;
back_bookmark = currentItem;
};
- for(auto & removedItem: deadList)
- {
+ for (auto& removedItem : deadList) {
auto instPtr = removedItem.first;
instPtr->invalidate();
currentItem = removedItem.second;
- if(back_bookmark == -1)
- {
+ if (back_bookmark == -1) {
// no bookmark yet
back_bookmark = currentItem;
- }
- else if(currentItem == front_bookmark - 1)
- {
+ } else if (currentItem == front_bookmark - 1) {
// part of contiguous sequence, continue
- }
- else
- {
+ } else {
// seam between previous and current item
removeNow();
}
front_bookmark = currentItem;
}
- if(back_bookmark != -1)
- {
+ if (back_bookmark != -1) {
removeNow();
}
}
- if(newList.size())
- {
+ if (newList.size()) {
add(newList);
}
m_dirty = false;
@@ -466,26 +476,23 @@ InstanceList::InstListError InstanceList::loadList()
void InstanceList::updateTotalPlayTime()
{
totalPlayTime = 0;
- for(auto const& itr : m_instances)
- {
+ for (auto const& itr : m_instances) {
totalPlayTime += itr.get()->totalTimePlayed();
}
}
void InstanceList::saveNow()
{
- for(auto & item: m_instances)
- {
+ for (auto& item : m_instances) {
item->saveNow();
}
}
-void InstanceList::add(const QList<InstancePtr> &t)
+void InstanceList::add(const QList<InstancePtr>& t)
{
beginInsertRows(QModelIndex(), m_instances.count(), m_instances.count() + t.size() - 1);
m_instances.append(t);
- for(auto & ptr : t)
- {
+ for (auto& ptr : t) {
connect(ptr.get(), &BaseInstance::propertiesChanged, this, &InstanceList::propertiesChanged);
}
endInsertRows();
@@ -493,69 +500,61 @@ void InstanceList::add(const QList<InstancePtr> &t)
void InstanceList::resumeWatch()
{
- if(m_watchLevel > 0)
- {
+ if (m_watchLevel > 0) {
qWarning() << "Bad suspend level resume in instance list";
return;
}
m_watchLevel++;
- if(m_watchLevel > 0 && m_dirty)
- {
+ if (m_watchLevel > 0 && m_dirty) {
loadList();
}
}
void InstanceList::suspendWatch()
{
- m_watchLevel --;
+ m_watchLevel--;
}
void InstanceList::providerUpdated()
{
m_dirty = true;
- if(m_watchLevel == 1)
- {
+ if (m_watchLevel == 1) {
loadList();
}
}
InstancePtr InstanceList::getInstanceById(QString instId) const
{
- if(instId.isEmpty())
+ if (instId.isEmpty())
return InstancePtr();
- for(auto & inst: m_instances)
- {
- if (inst->id() == instId)
- {
+ for (auto& inst : m_instances) {
+ if (inst->id() == instId) {
return inst;
}
}
return InstancePtr();
}
-QModelIndex InstanceList::getInstanceIndexById(const QString &id) const
+QModelIndex InstanceList::getInstanceIndexById(const QString& id) const
{
return index(getInstIndex(getInstanceById(id).get()));
}
-int InstanceList::getInstIndex(BaseInstance *inst) const
+int InstanceList::getInstIndex(BaseInstance* inst) const
{
int count = m_instances.count();
- for (int i = 0; i < count; i++)
- {
- if (inst == m_instances[i].get())
- {
+ for (int i = 0; i < count; i++) {
+ if (inst == m_instances[i].get()) {
return i;
}
}
return -1;
}
-void InstanceList::propertiesChanged(BaseInstance *inst)
+void InstanceList::propertiesChanged(BaseInstance* inst)
{
int i = getInstIndex(inst);
- if (i != -1)
- {
+ if (i != -1) {
emit dataChanged(index(i), index(i));
updateTotalPlayTime();
}
@@ -563,8 +562,7 @@ void InstanceList::propertiesChanged(BaseInstance *inst)
InstancePtr InstanceList::loadInstance(const InstanceId& id)
{
- if(!m_groupsLoaded)
- {
+ if (!m_groupsLoaded) {
loadGroupList();
}
@@ -592,50 +590,42 @@ InstancePtr InstanceList::loadInstance(const InstanceId& id)
void InstanceList::saveGroupList()
{
qDebug() << "Will save group list now.";
- if(!m_instancesProbed)
- {
+ if (!m_instancesProbed) {
qDebug() << "Group saving prevented because we don't know the full list of instances yet.";
return;
}
WatchLock foo(m_watcher, m_instDir);
QString groupFileName = m_instDir + "/instgroups.json";
QMap<QString, QSet<QString>> reverseGroupMap;
- for (auto iter = m_instanceGroupIndex.begin(); iter != m_instanceGroupIndex.end(); iter++)
- {
+ for (auto iter = m_instanceGroupIndex.begin(); iter != m_instanceGroupIndex.end(); iter++) {
QString id = iter.key();
QString group = iter.value();
if (group.isEmpty())
continue;
- if(!instanceSet.contains(id))
- {
+ if (!instanceSet.contains(id)) {
qDebug() << "Skipping saving missing instance" << id << "to groups list.";
continue;
}
- if (!reverseGroupMap.count(group))
- {
+ if (!reverseGroupMap.count(group)) {
QSet<QString> set;
set.insert(id);
reverseGroupMap[group] = set;
- }
- else
- {
- QSet<QString> &set = reverseGroupMap[group];
+ } else {
+ QSet<QString>& set = reverseGroupMap[group];
set.insert(id);
}
}
QJsonObject toplevel;
toplevel.insert("formatVersion", QJsonValue(QString("1")));
QJsonObject groupsArr;
- for (auto iter = reverseGroupMap.begin(); iter != reverseGroupMap.end(); iter++)
- {
+ for (auto iter = reverseGroupMap.begin(); iter != reverseGroupMap.end(); iter++) {
auto list = iter.value();
auto name = iter.key();
QJsonObject groupObj;
QJsonArray instanceArr;
groupObj.insert("hidden", QJsonValue(m_collapsedGroups.contains(name)));
- for (auto item : list)
- {
+ for (auto item : list) {
instanceArr.append(QJsonValue(item));
}
groupObj.insert("instances", instanceArr);
@@ -643,13 +633,10 @@ void InstanceList::saveGroupList()
}
toplevel.insert("groups", groupsArr);
QJsonDocument doc(toplevel);
- try
- {
+ try {
FS::write(groupFileName, doc.toJson());
qDebug() << "Group list saved.";
- }
- catch (const FS::FileSystemException &e)
- {
+ } catch (const FS::FileSystemException& e) {
qCritical() << "Failed to write instance group file :" << e.cause();
}
}
@@ -665,12 +652,9 @@ void InstanceList::loadGroupList()
return;
QByteArray jsonData;
- try
- {
+ try {
jsonData = FS::read(groupFileName);
- }
- catch (const FS::FileSystemException &e)
- {
+ } catch (const FS::FileSystemException& e) {
qCritical() << "Failed to read instance group file :" << e.cause();
return;
}
@@ -679,17 +663,15 @@ void InstanceList::loadGroupList()
QJsonDocument jsonDoc = QJsonDocument::fromJson(jsonData, &error);
// if the json was bad, fail
- if (error.error != QJsonParseError::NoError)
- {
+ if (error.error != QJsonParseError::NoError) {
qCritical() << QString("Failed to parse instance group file: %1 at offset %2")
- .arg(error.errorString(), QString::number(error.offset))
- .toUtf8();
+ .arg(error.errorString(), QString::number(error.offset))
+ .toUtf8();
return;
}
// if the root of the json wasn't an object, fail
- if (!jsonDoc.isObject())
- {
+ if (!jsonDoc.isObject()) {
qWarning() << "Invalid group file. Root entry should be an object.";
return;
}
@@ -701,8 +683,7 @@ void InstanceList::loadGroupList()
return;
// Get the groups. if it's not an object, fail
- if (!rootObj.value("groups").isObject())
- {
+ if (!rootObj.value("groups").isObject()) {
qWarning() << "Invalid group list JSON: 'groups' should be an object.";
return;
}
@@ -712,21 +693,20 @@ void InstanceList::loadGroupList()
// Iterate through all the groups.
QJsonObject groupMapping = rootObj.value("groups").toObject();
- for (QJsonObject::iterator iter = groupMapping.begin(); iter != groupMapping.end(); iter++)
- {
+ for (QJsonObject::iterator iter = groupMapping.begin(); iter != groupMapping.end(); iter++) {
QString groupName = iter.key();
// If not an object, complain and skip to the next one.
- if (!iter.value().isObject())
- {
+ if (!iter.value().isObject()) {
qWarning() << QString("Group '%1' in the group list should be an object.").arg(groupName).toUtf8();
continue;
}
QJsonObject groupObj = iter.value().toObject();
- if (!groupObj.value("instances").isArray())
- {
- qWarning() << QString("Group '%1' in the group list is invalid. It should contain an array called 'instances'.").arg(groupName).toUtf8();
+ if (!groupObj.value("instances").isArray()) {
+ qWarning() << QString("Group '%1' in the group list is invalid. It should contain an array called 'instances'.")
+ .arg(groupName)
+ .toUtf8();
continue;
}
@@ -734,15 +714,14 @@ void InstanceList::loadGroupList()
groupSet.insert(groupName);
auto hidden = groupObj.value("hidden").toBool(false);
- if(hidden) {
+ if (hidden) {
m_collapsedGroups.insert(groupName);
}
// Iterate through the list of instances in the group.
QJsonArray instancesArray = groupObj.value("instances").toArray();
- for (QJsonArray::iterator iter2 = instancesArray.begin(); iter2 != instancesArray.end(); iter2++)
- {
+ for (QJsonArray::iterator iter2 = instancesArray.begin(); iter2 != instancesArray.end(); iter2++) {
m_instanceGroupIndex[(*iter2).toString()] = groupName;
}
}
@@ -757,13 +736,11 @@ void InstanceList::instanceDirContentsChanged(const QString& path)
emit instancesChanged();
}
-void InstanceList::on_InstFolderChanged(const Setting &setting, QVariant value)
+void InstanceList::on_InstFolderChanged(const Setting& setting, QVariant value)
{
QString newInstDir = QDir(value.toString()).canonicalPath();
- if(newInstDir != m_instDir)
- {
- if(m_groupsLoaded)
- {
+ if (newInstDir != m_instDir) {
+ if (m_groupsLoaded) {
saveGroupList();
}
m_instDir = newInstDir;
@@ -775,7 +752,7 @@ void InstanceList::on_InstFolderChanged(const Setting &setting, QVariant value)
void InstanceList::on_GroupStateChanged(const QString& group, bool collapsed)
{
qDebug() << "Group" << group << (collapsed ? "collapsed" : "expanded");
- if(collapsed) {
+ if (collapsed) {
m_collapsedGroups.insert(group);
} else {
m_collapsedGroups.remove(group);
@@ -783,19 +760,14 @@ void InstanceList::on_GroupStateChanged(const QString& group, bool collapsed)
saveGroupList();
}
-class InstanceStaging : public Task
-{
-Q_OBJECT
+class InstanceStaging : public Task {
+ Q_OBJECT
const unsigned minBackoff = 1;
const unsigned maxBackoff = 16;
-public:
- InstanceStaging (
- InstanceList * parent,
- Task * child,
- const QString & stagingPath,
- const QString& instanceName,
- const QString& groupName )
- : backoff(minBackoff, maxBackoff)
+
+ public:
+ InstanceStaging(InstanceList* parent, Task* child, const QString& stagingPath, const QString& instanceName, const QString& groupName)
+ : backoff(minBackoff, maxBackoff)
{
m_parent = parent;
m_child.reset(child);
@@ -810,62 +782,51 @@ public:
connect(&m_backoffTimer, &QTimer::timeout, this, &InstanceStaging::childSucceded);
}
- virtual ~InstanceStaging() {};
-
+ virtual ~InstanceStaging(){};
// FIXME/TODO: add ability to abort during instance commit retries
bool abort() override
{
- if(m_child && m_child->canAbort())
- {
+ if (m_child && m_child->canAbort()) {
return m_child->abort();
}
return false;
}
bool canAbort() const override
{
- if(m_child && m_child->canAbort())
- {
+ if (m_child && m_child->canAbort()) {
return true;
}
return false;
}
-protected:
- virtual void executeTask() override
- {
- m_child->start();
- }
- QStringList warnings() const override
- {
- return m_child->warnings();
- }
+ protected:
+ virtual void executeTask() override { m_child->start(); }
+ QStringList warnings() const override { return m_child->warnings(); }
-private slots:
+ private slots:
void childSucceded()
{
unsigned sleepTime = backoff();
- if(m_parent->commitStagedInstance(m_stagingPath, m_instanceName, m_groupName))
- {
+ if (m_parent->commitStagedInstance(m_stagingPath, m_instanceName, m_groupName)) {
emitSucceeded();
return;
}
// we actually failed, retry?
- if(sleepTime == maxBackoff)
- {
+ if (sleepTime == maxBackoff) {
emitFailed(tr("Failed to commit instance, even after multiple retries. It is being blocked by something."));
return;
}
qDebug() << "Failed to commit instance" << m_instanceName << "Initiating backoff:" << sleepTime;
m_backoffTimer.start(sleepTime * 500);
}
- void childFailed(const QString & reason)
+ void childFailed(const QString& reason)
{
m_parent->destroyStagingPath(m_stagingPath);
emitFailed(reason);
}
-private:
+ private:
/*
* WHY: the whole reason why this uses an exponential backoff retry scheme is antivirus on Windows.
* Basically, it starts messing things up while the launcher is extracting/creating instances
@@ -873,14 +834,14 @@ private:
*/
ExponentialSeries backoff;
QString m_stagingPath;
- InstanceList * m_parent;
+ InstanceList* m_parent;
unique_qobject_ptr<Task> m_child;
QString m_instanceName;
QString m_groupName;
QTimer m_backoffTimer;
};
-Task * InstanceList::wrapInstanceTask(InstanceTask * task)
+Task* InstanceList::wrapInstanceTask(InstanceTask* task)
{
auto stagingPath = getStagedInstancePath();
task->setStagingPath(stagingPath);
@@ -895,8 +856,7 @@ QString InstanceList::getStagedInstancePath()
QString relPath = FS::PathCombine(tempDir, key);
QDir rootPath(m_instDir);
auto path = FS::PathCombine(m_instDir, relPath);
- if(!rootPath.mkpath(relPath))
- {
+ if (!rootPath.mkpath(relPath)) {
return QString();
}
#ifdef Q_OS_WIN32
@@ -913,8 +873,7 @@ bool InstanceList::commitStagedInstance(const QString& path, const QString& inst
{
WatchLock lock(m_watcher, m_instDir);
QString destination = FS::PathCombine(m_instDir, instID);
- if(!dir.rename(path, destination))
- {
+ if (!dir.rename(path, destination)) {
qWarning() << "Failed to move" << path << "to" << destination;
return false;
}
@@ -933,7 +892,8 @@ bool InstanceList::destroyStagingPath(const QString& keyPath)
return FS::deletePath(keyPath);
}
-int InstanceList::getTotalPlayTime() {
+int InstanceList::getTotalPlayTime()
+{
updateTotalPlayTime();
return totalPlayTime;
}
diff --git a/launcher/InstanceList.h b/launcher/InstanceList.h
index bc6c3af0..62282f04 100644
--- a/launcher/InstanceList.h
+++ b/launcher/InstanceList.h
@@ -19,6 +19,8 @@
#include <QAbstractListModel>
#include <QSet>
#include <QList>
+#include <QStack>
+#include <QPair>
#include "BaseInstance.h"
@@ -46,6 +48,12 @@ enum class GroupsState
Dirty
};
+struct TrashHistoryItem {
+ QString id;
+ QString polyPath;
+ QString trashPath;
+ QString groupName;
+};
class InstanceList : public QAbstractListModel
{
@@ -102,6 +110,9 @@ public:
void setInstanceGroup(const InstanceId & id, const GroupId& name);
void deleteGroup(const GroupId & name);
+ bool trashInstance(const InstanceId &id);
+ bool trashedSomething();
+ void undoTrashInstance();
void deleteInstance(const InstanceId & id);
// Wrap an instance creation task in some more task machinery and make it ready to be used
@@ -180,4 +191,6 @@ private:
QSet<InstanceId> instanceSet;
bool m_groupsLoaded = false;
bool m_instancesProbed = false;
+
+ QStack<TrashHistoryItem> m_trashHistory;
};
diff --git a/launcher/LaunchController.cpp b/launcher/LaunchController.cpp
index d36ee3fe..11f9b2bb 100644
--- a/launcher/LaunchController.cpp
+++ b/launcher/LaunchController.cpp
@@ -145,16 +145,26 @@ void LaunchController::login() {
return;
}
- // we try empty password first :)
- QString password;
// we loop until the user succeeds in logging in or gives up
bool tryagain = true;
- // the failure. the default failure.
- const QString needLoginAgain = tr("Your account is currently not logged in. Please enter your password to log in again. <br /> <br /> This could be caused by a password change.");
- QString failReason = needLoginAgain;
+ unsigned int tries = 0;
while (tryagain)
{
+ if (tries > 0 && tries % 3 == 0) {
+ auto result = QMessageBox::question(
+ m_parentWidget,
+ tr("Continue launch?"),
+ tr("It looks like we couldn't launch after %1 tries. Do you want to continue trying?")
+ .arg(tries)
+ );
+
+ if (result == QMessageBox::No) {
+ emitAborted();
+ return;
+ }
+ }
+ tries++;
m_session = std::make_shared<AuthSession>();
m_session->wants_online = m_online;
m_accountToUse->fillSession(m_session);
diff --git a/launcher/Launcher.in b/launcher/Launcher.in
index 528e360e..68fac26a 100755
--- a/launcher/Launcher.in
+++ b/launcher/Launcher.in
@@ -18,13 +18,17 @@ LAUNCHER_NAME=@Launcher_APP_BINARY_NAME@
LAUNCHER_DIR="$(dirname "$(readlink -f "$0")")"
echo "Launcher Dir: ${LAUNCHER_DIR}"
-# Set up env - filter out input LD_ variables but pass them in under different names
-export GAME_LIBRARY_PATH=${GAME_LIBRARY_PATH-${LD_LIBRARY_PATH}}
-export GAME_PRELOAD=${GAME_PRELOAD-${LD_PRELOAD}}
-export LD_LIBRARY_PATH="${LAUNCHER_DIR}/lib@LIB_SUFFIX@":$LAUNCHER_LIBRARY_PATH
-export LD_PRELOAD=$LAUNCHER_PRELOAD
-export QT_PLUGIN_PATH="${LAUNCHER_DIR}/plugins"
-export QT_FONTPATH="${LAUNCHER_DIR}/fonts"
+# Set up env.
+# Pass our custom variables separately so that the launcher can remove them for child processes
+export LAUNCHER_LD_LIBRARY_PATH="${LAUNCHER_DIR}/lib@LIB_SUFFIX@"
+export LAUNCHER_LD_PRELOAD=""
+export LAUNCHER_QT_PLUGIN_PATH="${LAUNCHER_DIR}/plugins"
+export LAUNCHER_QT_FONTPATH="${LAUNCHER_DIR}/fonts"
+
+export LD_LIBRARY_PATH="$LAUNCHER_LD_LIBRARY_PATH:$LD_LIBRARY_PATH"
+export LD_PRELOAD="$LAUNCHER_LD_PRELOAD:$LD_PRELOAD"
+export QT_PLUGIN_PATH="$LAUNCHER_QT_PLUGIN_PATH:$QT_PLUGIN_PATH"
+export QT_FONTPATH="$LAUNCHER_QT_FONTPATH:$QT_FONTPATH"
# Detect missing dependencies...
DEPS_LIST=`ldd "${LAUNCHER_DIR}"/plugins/*/*.so 2>/dev/null | grep "not found" | sort -u | awk -vORS=", " '{ print $1 }'`
diff --git a/launcher/MMCZip.cpp b/launcher/MMCZip.cpp
index f20d6dff..04ca5094 100644
--- a/launcher/MMCZip.cpp
+++ b/launcher/MMCZip.cpp
@@ -127,7 +127,7 @@ bool MMCZip::compressDirFiles(QString fileCompressed, QString dir, QFileInfoList
}
// ours
-bool MMCZip::createModdedJar(QString sourceJarPath, QString targetJarPath, const QList<Mod>& mods)
+bool MMCZip::createModdedJar(QString sourceJarPath, QString targetJarPath, const QList<Mod*>& mods)
{
QuaZip zipOut(targetJarPath);
if (!zipOut.open(QuaZip::mdCreate))
@@ -141,42 +141,41 @@ bool MMCZip::createModdedJar(QString sourceJarPath, QString targetJarPath, const
QSet<QString> addedFiles;
// Modify the jar
- QListIterator<Mod> i(mods);
- i.toBack();
- while (i.hasPrevious())
+ // This needs to be done in reverse-order to ensure we respect the loading order of components
+ for (auto i = mods.crbegin(); i != mods.crend(); i++)
{
- const Mod &mod = i.previous();
+ const auto* mod = *i;
// do not merge disabled mods.
- if (!mod.enabled())
+ if (!mod->enabled())
continue;
- if (mod.type() == Mod::MOD_ZIPFILE)
+ if (mod->type() == Mod::MOD_ZIPFILE)
{
- if (!mergeZipFiles(&zipOut, mod.fileinfo(), addedFiles))
+ if (!mergeZipFiles(&zipOut, mod->fileinfo(), addedFiles))
{
zipOut.close();
QFile::remove(targetJarPath);
- qCritical() << "Failed to add" << mod.fileinfo().fileName() << "to the jar.";
+ qCritical() << "Failed to add" << mod->fileinfo().fileName() << "to the jar.";
return false;
}
}
- else if (mod.type() == Mod::MOD_SINGLEFILE)
+ else if (mod->type() == Mod::MOD_SINGLEFILE)
{
// FIXME: buggy - does not work with addedFiles
- auto filename = mod.fileinfo();
+ auto filename = mod->fileinfo();
if (!JlCompress::compressFile(&zipOut, filename.absoluteFilePath(), filename.fileName()))
{
zipOut.close();
QFile::remove(targetJarPath);
- qCritical() << "Failed to add" << mod.fileinfo().fileName() << "to the jar.";
+ qCritical() << "Failed to add" << mod->fileinfo().fileName() << "to the jar.";
return false;
}
addedFiles.insert(filename.fileName());
}
- else if (mod.type() == Mod::MOD_FOLDER)
+ else if (mod->type() == Mod::MOD_FOLDER)
{
// untested, but seems to be unused / not possible to reach
// FIXME: buggy - does not work with addedFiles
- auto filename = mod.fileinfo();
+ auto filename = mod->fileinfo();
QString what_to_zip = filename.absoluteFilePath();
QDir dir(what_to_zip);
dir.cdUp();
@@ -193,7 +192,7 @@ bool MMCZip::createModdedJar(QString sourceJarPath, QString targetJarPath, const
{
zipOut.close();
QFile::remove(targetJarPath);
- qCritical() << "Failed to add" << mod.fileinfo().fileName() << "to the jar.";
+ qCritical() << "Failed to add" << mod->fileinfo().fileName() << "to the jar.";
return false;
}
qDebug() << "Adding folder " << filename.fileName() << " from "
@@ -204,7 +203,7 @@ bool MMCZip::createModdedJar(QString sourceJarPath, QString targetJarPath, const
// Make sure we do not continue launching when something is missing or undefined...
zipOut.close();
QFile::remove(targetJarPath);
- qCritical() << "Failed to add unknown mod type" << mod.fileinfo().fileName() << "to the jar.";
+ qCritical() << "Failed to add unknown mod type" << mod->fileinfo().fileName() << "to the jar.";
return false;
}
}
@@ -269,7 +268,7 @@ bool MMCZip::findFilesInZip(QuaZip * zip, const QString & what, QStringList & re
// ours
-nonstd::optional<QStringList> MMCZip::extractSubDir(QuaZip *zip, const QString & subdir, const QString &target)
+std::optional<QStringList> MMCZip::extractSubDir(QuaZip *zip, const QString & subdir, const QString &target)
{
QDir directory(target);
QStringList extracted;
@@ -278,7 +277,7 @@ nonstd::optional<QStringList> MMCZip::extractSubDir(QuaZip *zip, const QString &
auto numEntries = zip->getEntriesCount();
if(numEntries < 0) {
qWarning() << "Failed to enumerate files in archive";
- return nonstd::nullopt;
+ return std::nullopt;
}
else if(numEntries == 0) {
qDebug() << "Extracting empty archives seems odd...";
@@ -287,7 +286,7 @@ nonstd::optional<QStringList> MMCZip::extractSubDir(QuaZip *zip, const QString &
else if (!zip->goToFirstFile())
{
qWarning() << "Failed to seek to first file in zip";
- return nonstd::nullopt;
+ return std::nullopt;
}
do
@@ -324,7 +323,7 @@ nonstd::optional<QStringList> MMCZip::extractSubDir(QuaZip *zip, const QString &
{
qWarning() << "Failed to extract file" << original_name << "to" << absFilePath;
JlCompress::removeFile(extracted);
- return nonstd::nullopt;
+ return std::nullopt;
}
extracted.append(absFilePath);
@@ -342,7 +341,7 @@ bool MMCZip::extractRelFile(QuaZip *zip, const QString &file, const QString &tar
}
// ours
-nonstd::optional<QStringList> MMCZip::extractDir(QString fileCompressed, QString dir)
+std::optional<QStringList> MMCZip::extractDir(QString fileCompressed, QString dir)
{
QuaZip zip(fileCompressed);
if (!zip.open(QuaZip::mdUnzip))
@@ -353,13 +352,13 @@ nonstd::optional<QStringList> MMCZip::extractDir(QString fileCompressed, QString
return QStringList();
}
qWarning() << "Could not open archive for unzipping:" << fileCompressed << "Error:" << zip.getZipError();;
- return nonstd::nullopt;
+ return std::nullopt;
}
return MMCZip::extractSubDir(&zip, "", dir);
}
// ours
-nonstd::optional<QStringList> MMCZip::extractDir(QString fileCompressed, QString subdir, QString dir)
+std::optional<QStringList> MMCZip::extractDir(QString fileCompressed, QString subdir, QString dir)
{
QuaZip zip(fileCompressed);
if (!zip.open(QuaZip::mdUnzip))
@@ -370,7 +369,7 @@ nonstd::optional<QStringList> MMCZip::extractDir(QString fileCompressed, QString
return QStringList();
}
qWarning() << "Could not open archive for unzipping:" << fileCompressed << "Error:" << zip.getZipError();;
- return nonstd::nullopt;
+ return std::nullopt;
}
return MMCZip::extractSubDir(&zip, subdir, dir);
}
diff --git a/launcher/MMCZip.h b/launcher/MMCZip.h
index bf90cd0b..ce9775bd 100644
--- a/launcher/MMCZip.h
+++ b/launcher/MMCZip.h
@@ -42,7 +42,7 @@
#include <functional>
#include <quazip/JlCompress.h>
-#include <nonstd/optional>
+#include <optional>
namespace MMCZip
{
@@ -75,7 +75,7 @@ namespace MMCZip
/**
* take a source jar, add mods to it, resulting in target jar
*/
- bool createModdedJar(QString sourceJarPath, QString targetJarPath, const QList<Mod>& mods);
+ bool createModdedJar(QString sourceJarPath, QString targetJarPath, const QList<Mod*>& mods);
/**
* Find a single file in archive by file name (not path)
@@ -95,7 +95,7 @@ namespace MMCZip
/**
* Extract a subdirectory from an archive
*/
- nonstd::optional<QStringList> extractSubDir(QuaZip *zip, const QString & subdir, const QString &target);
+ std::optional<QStringList> extractSubDir(QuaZip *zip, const QString & subdir, const QString &target);
bool extractRelFile(QuaZip *zip, const QString & file, const QString &target);
@@ -106,7 +106,7 @@ namespace MMCZip
* \param dir The directory to extract to, the current directory if left empty.
* \return The list of the full paths of the files extracted, empty on failure.
*/
- nonstd::optional<QStringList> extractDir(QString fileCompressed, QString dir);
+ std::optional<QStringList> extractDir(QString fileCompressed, QString dir);
/**
* Extract a subdirectory from an archive
@@ -116,7 +116,7 @@ namespace MMCZip
* \param dir The directory to extract to, the current directory if left empty.
* \return The list of the full paths of the files extracted, empty on failure.
*/
- nonstd::optional<QStringList> extractDir(QString fileCompressed, QString subdir, QString dir);
+ std::optional<QStringList> extractDir(QString fileCompressed, QString subdir, QString dir);
/**
* Extract a single file from an archive into a directory
diff --git a/launcher/ModDownloadTask.cpp b/launcher/ModDownloadTask.cpp
index 41856fb5..2b0343f4 100644
--- a/launcher/ModDownloadTask.cpp
+++ b/launcher/ModDownloadTask.cpp
@@ -27,6 +27,7 @@ ModDownloadTask::ModDownloadTask(ModPlatform::IndexedPack mod, ModPlatform::Inde
{
if (is_indexed) {
m_update_task.reset(new LocalModUpdateTask(mods->indexDir(), m_mod, m_mod_version));
+ connect(m_update_task.get(), &LocalModUpdateTask::hasOldMod, this, &ModDownloadTask::hasOldMod);
addTask(m_update_task);
}
@@ -40,12 +41,16 @@ ModDownloadTask::ModDownloadTask(ModPlatform::IndexedPack mod, ModPlatform::Inde
connect(m_filesNetJob.get(), &NetJob::failed, this, &ModDownloadTask::downloadFailed);
addTask(m_filesNetJob);
-
}
void ModDownloadTask::downloadSucceeded()
{
m_filesNetJob.reset();
+ auto name = std::get<0>(to_delete);
+ auto filename = std::get<1>(to_delete);
+ if (!name.isEmpty() && filename != m_mod_version.fileName) {
+ mods->uninstallMod(filename, true);
+ }
}
void ModDownloadTask::downloadFailed(QString reason)
@@ -58,3 +63,10 @@ void ModDownloadTask::downloadProgressChanged(qint64 current, qint64 total)
{
emit progress(current, total);
}
+
+// This indirection is done so that we don't delete a mod before being sure it was
+// downloaded successfully!
+void ModDownloadTask::hasOldMod(QString name, QString filename)
+{
+ to_delete = {name, filename};
+}
diff --git a/launcher/ModDownloadTask.h b/launcher/ModDownloadTask.h
index b3c25909..95020470 100644
--- a/launcher/ModDownloadTask.h
+++ b/launcher/ModDownloadTask.h
@@ -30,7 +30,7 @@ class ModFolderModel;
class ModDownloadTask : public SequentialTask {
Q_OBJECT
public:
- explicit ModDownloadTask(ModPlatform::IndexedPack mod, ModPlatform::IndexedVersion version, const std::shared_ptr<ModFolderModel> mods, bool is_indexed);
+ explicit ModDownloadTask(ModPlatform::IndexedPack mod, ModPlatform::IndexedVersion version, const std::shared_ptr<ModFolderModel> mods, bool is_indexed = true);
const QString& getFilename() const { return m_mod_version.fileName; }
private:
@@ -46,6 +46,11 @@ private:
void downloadFailed(QString reason);
void downloadSucceeded();
+
+ std::tuple<QString, QString> to_delete {"", ""};
+
+private slots:
+ void hasOldMod(QString name, QString filename);
};
diff --git a/launcher/QObjectPtr.h b/launcher/QObjectPtr.h
index 57974939..173dc5e7 100644
--- a/launcher/QObjectPtr.h
+++ b/launcher/QObjectPtr.h
@@ -77,10 +77,12 @@ public:
{
return m_ptr;
}
- bool operator==(const shared_qobject_ptr<T>& other) {
+ template<typename U>
+ bool operator==(const shared_qobject_ptr<U>& other) const {
return m_ptr == other.m_ptr;
}
- bool operator!=(const shared_qobject_ptr<T>& other) {
+ template<typename U>
+ bool operator!=(const shared_qobject_ptr<U>& other) const {
return m_ptr != other.m_ptr;
}
diff --git a/launcher/java/JavaUtils.cpp b/launcher/java/JavaUtils.cpp
index 749c9c88..2b19fca0 100644
--- a/launcher/java/JavaUtils.cpp
+++ b/launcher/java/JavaUtils.cpp
@@ -52,25 +52,24 @@ JavaUtils::JavaUtils()
{
}
-#if defined(Q_OS_LINUX) || defined(Q_OS_FREEBSD)
-static QString processLD_LIBRARY_PATH(const QString & LD_LIBRARY_PATH)
+QString stripVariableEntries(QString name, QString target, QString remove)
{
- QDir mmcBin(QCoreApplication::applicationDirPath());
- auto items = LD_LIBRARY_PATH.split(':');
- QStringList final;
- for(auto & item: items)
- {
- QDir test(item);
- if(test == mmcBin)
- {
- qDebug() << "Env:LD_LIBRARY_PATH ignoring path" << item;
- continue;
- }
- final.append(item);
+ char delimiter = ':';
+#ifdef Q_OS_WIN32
+ delimiter = ';';
+#endif
+
+ auto targetItems = target.split(delimiter);
+ auto toRemove = remove.split(delimiter);
+
+ for (QString item : toRemove) {
+ bool removed = targetItems.removeOne(item);
+ if (!removed)
+ qWarning() << "Entry" << item
+ << "could not be stripped from variable" << name;
}
- return final.join(':');
+ return targetItems.join(delimiter);
}
-#endif
QProcessEnvironment CleanEnviroment()
{
@@ -89,6 +88,16 @@ QProcessEnvironment CleanEnviroment()
"JAVA_OPTIONS",
"JAVA_TOOL_OPTIONS"
};
+
+ QStringList stripped =
+ {
+#if defined(Q_OS_LINUX) || defined(Q_OS_FREEBSD) || defined(Q_OS_OPENBSD)
+ "LD_LIBRARY_PATH",
+ "LD_PRELOAD",
+#endif
+ "QT_PLUGIN_PATH",
+ "QT_FONTPATH"
+ };
for(auto key: rawenv.keys())
{
auto value = rawenv.value(key);
@@ -98,19 +107,22 @@ QProcessEnvironment CleanEnviroment()
qDebug() << "Env: ignoring" << key << value;
continue;
}
- // filter PolyMC-related things
- if(key.startsWith("QT_"))
+
+ // These are used to strip the original variables
+ // If there is "LD_LIBRARY_PATH" and "LAUNCHER_LD_LIBRARY_PATH", we want to
+ // remove all values in "LAUNCHER_LD_LIBRARY_PATH" from "LD_LIBRARY_PATH"
+ if(key.startsWith("LAUNCHER_"))
{
qDebug() << "Env: ignoring" << key << value;
continue;
}
-#if defined(Q_OS_LINUX) || defined(Q_OS_FREEBSD)
- // Do not pass LD_* variables to java. They were intended for PolyMC
- if(key.startsWith("LD_"))
+ if(stripped.contains(key))
{
- qDebug() << "Env: ignoring" << key << value;
- continue;
+ QString newValue = stripVariableEntries(key, value, rawenv.value("LAUNCHER_" + key));
+
+ qDebug() << "Env: stripped" << key << value << "to" << newValue;
}
+#if defined(Q_OS_LINUX) || defined(Q_OS_FREEBSD) || defined(Q_OS_OPENBSD)
// Strip IBus
// IBus is a Linux IME framework. For some reason, it breaks MC?
if (key == "XMODIFIERS" && value.contains(IBUS))
@@ -119,22 +131,12 @@ QProcessEnvironment CleanEnviroment()
value.replace(IBUS, "");
qDebug() << "Env: stripped" << IBUS << "from" << save << ":" << value;
}
- if(key == "GAME_PRELOAD")
- {
- env.insert("LD_PRELOAD", value);
- continue;
- }
- if(key == "GAME_LIBRARY_PATH")
- {
- env.insert("LD_LIBRARY_PATH", processLD_LIBRARY_PATH(value));
- continue;
- }
#endif
// qDebug() << "Env: " << key << value;
env.insert(key, value);
}
#ifdef Q_OS_LINUX
- // HACK: Workaround for QTBUG42500
+ // HACK: Workaround for QTBUG-42500
if(!env.contains("LD_LIBRARY_PATH"))
{
env.insert("LD_LIBRARY_PATH", "");
@@ -203,7 +205,7 @@ QList<JavaInstallPtr> JavaUtils::FindJavaFromRegistryKey(DWORD keyType, QString
// Read the current type version from the registry.
// This will be used to find any key that contains the JavaHome value.
- TCHAR subKeyName[255];
+ WCHAR subKeyName[255];
DWORD subKeyNameSize, numSubKeys, retCode;
// Get the number of subkeys
@@ -229,12 +231,11 @@ QList<JavaInstallPtr> JavaUtils::FindJavaFromRegistryKey(DWORD keyType, QString
KEY_READ | KEY_WOW64_64KEY, &newKey) == ERROR_SUCCESS)
{
// Read the JavaHome value to find where Java is installed.
- TCHAR *value = NULL;
DWORD valueSz = 0;
- if (RegQueryValueExW(newKey, keyJavaDir.toStdWString().c_str(), NULL, NULL, (BYTE *)value,
- &valueSz) == ERROR_MORE_DATA)
+ if (RegQueryValueExW(newKey, keyJavaDir.toStdWString().c_str(), NULL, NULL, NULL,
+ &valueSz) == ERROR_SUCCESS)
{
- value = new TCHAR[valueSz];
+ WCHAR *value = new WCHAR[valueSz];
RegQueryValueExW(newKey, keyJavaDir.toStdWString().c_str(), NULL, NULL, (BYTE *)value,
&valueSz);
diff --git a/launcher/java/JavaUtils.h b/launcher/java/JavaUtils.h
index 26d8003b..9b69b516 100644
--- a/launcher/java/JavaUtils.h
+++ b/launcher/java/JavaUtils.h
@@ -24,6 +24,7 @@
#include <windows.h>
#endif
+QString stripVariableEntries(QString name, QString target, QString remove);
QProcessEnvironment CleanEnviroment();
class JavaUtils : public QObject
diff --git a/launcher/launch/LaunchTask.cpp b/launcher/launch/LaunchTask.cpp
index 3aa95052..28fcc4f4 100644
--- a/launcher/launch/LaunchTask.cpp
+++ b/launcher/launch/LaunchTask.cpp
@@ -282,35 +282,22 @@ void LaunchTask::emitFailed(QString reason)
Task::emitFailed(reason);
}
-void LaunchTask::substituteVariables(const QStringList &args) const
+void LaunchTask::substituteVariables(QStringList &args) const
{
- auto variables = m_instance->getVariables();
- auto envVariables = QProcessEnvironment::systemEnvironment();
+ auto env = m_instance->createEnvironment();
- for (auto arg : args) {
- for (auto key : variables)
- {
- arg.replace("$" + key, variables.value(key));
- }
- for (auto env : envVariables.keys())
- {
- arg.replace("$" + env, envVariables.value(env));
- }
+ for (auto key : env.keys())
+ {
+ args.replaceInStrings("$" + key, env.value(key));
}
}
-QString LaunchTask::substituteVariables(const QString &cmd) const
+void LaunchTask::substituteVariables(QString &cmd) const
{
- QString out = cmd;
- auto variables = m_instance->getVariables();
- for (auto it = variables.begin(); it != variables.end(); ++it)
- {
- out.replace("$" + it.key(), it.value());
- }
- auto env = QProcessEnvironment::systemEnvironment();
- for (auto var : env.keys())
+ auto env = m_instance->createEnvironment();
+
+ for (auto key : env.keys())
{
- out.replace("$" + var, env.value(var));
+ cmd.replace("$" + key, env.value(key));
}
- return out;
}
diff --git a/launcher/launch/LaunchTask.h b/launcher/launch/LaunchTask.h
index 2efe1fa2..9c72b23f 100644
--- a/launcher/launch/LaunchTask.h
+++ b/launcher/launch/LaunchTask.h
@@ -105,8 +105,8 @@ public: /* methods */
shared_qobject_ptr<LogModel> getLogModel();
public:
- void substituteVariables(const QStringList &args) const;
- QString substituteVariables(const QString &cmd) const;
+ void substituteVariables(QStringList &args) const;
+ void substituteVariables(QString &cmd) const;
QString censorPrivateInfo(QString in);
protected: /* methods */
diff --git a/launcher/launch/steps/PostLaunchCommand.cpp b/launcher/launch/steps/PostLaunchCommand.cpp
index cf765bc0..ccf07f22 100644
--- a/launcher/launch/steps/PostLaunchCommand.cpp
+++ b/launcher/launch/steps/PostLaunchCommand.cpp
@@ -56,9 +56,10 @@ void PostLaunchCommand::executeTask()
const QString program = args.takeFirst();
m_process.start(program, args);
#else
- QString postlaunch_cmd = m_parent->substituteVariables(m_command);
- emit logLine(tr("Running Post-Launch command: %1").arg(postlaunch_cmd), MessageLevel::Launcher);
- m_process.start(postlaunch_cmd);
+ m_parent->substituteVariables(m_command);
+
+ emit logLine(tr("Running Post-Launch command: %1").arg(m_command), MessageLevel::Launcher);
+ m_process.start(m_command);
#endif
}
diff --git a/launcher/launch/steps/PreLaunchCommand.cpp b/launcher/launch/steps/PreLaunchCommand.cpp
index bf7d27eb..0b0392cb 100644
--- a/launcher/launch/steps/PreLaunchCommand.cpp
+++ b/launcher/launch/steps/PreLaunchCommand.cpp
@@ -56,9 +56,10 @@ void PreLaunchCommand::executeTask()
const QString program = args.takeFirst();
m_process.start(program, args);
#else
- QString prelaunch_cmd = m_parent->substituteVariables(m_command);
- emit logLine(tr("Running Pre-Launch command: %1").arg(prelaunch_cmd), MessageLevel::Launcher);
- m_process.start(prelaunch_cmd);
+ m_parent->substituteVariables(m_command);
+
+ emit logLine(tr("Running Pre-Launch command: %1").arg(m_command), MessageLevel::Launcher);
+ m_process.start(m_command);
#endif
}
diff --git a/launcher/minecraft/MinecraftInstance.cpp b/launcher/minecraft/MinecraftInstance.cpp
index abc022b6..5a6f8de0 100644
--- a/launcher/minecraft/MinecraftInstance.cpp
+++ b/launcher/minecraft/MinecraftInstance.cpp
@@ -700,24 +700,24 @@ QStringList MinecraftInstance::verboseDescription(AuthSessionPtr session, Minecr
{
out << QString("%1:").arg(label);
auto modList = model.allMods();
- std::sort(modList.begin(), modList.end(), [](Mod &a, Mod &b) {
- auto aName = a.fileinfo().completeBaseName();
- auto bName = b.fileinfo().completeBaseName();
+ std::sort(modList.begin(), modList.end(), [](Mod::Ptr a, Mod::Ptr b) {
+ auto aName = a->fileinfo().completeBaseName();
+ auto bName = b->fileinfo().completeBaseName();
return aName.localeAwareCompare(bName) < 0;
});
- for(auto & mod: modList)
+ for(auto mod: modList)
{
- if(mod.type() == Mod::MOD_FOLDER)
+ if(mod->type() == Mod::MOD_FOLDER)
{
- out << u8" [📁] " + mod.fileinfo().completeBaseName() + " (folder)";
+ out << u8" [🖿] " + mod->fileinfo().completeBaseName() + " (folder)";
continue;
}
- if(mod.enabled()) {
- out << u8" [✔️] " + mod.fileinfo().completeBaseName();
+ if(mod->enabled()) {
+ out << u8" [✔] " + mod->fileinfo().completeBaseName();
}
else {
- out << u8" [❌] " + mod.fileinfo().completeBaseName() + " (disabled)";
+ out << u8" [✘] " + mod->fileinfo().completeBaseName() + " (disabled)";
}
}
@@ -1136,16 +1136,16 @@ std::shared_ptr<GameOptions> MinecraftInstance::gameOptionsModel() const
return m_game_options;
}
-QList< Mod > MinecraftInstance::getJarMods() const
+QList<Mod*> MinecraftInstance::getJarMods() const
{
auto profile = m_components->getProfile();
- QList<Mod> mods;
+ QList<Mod*> mods;
for (auto jarmod : profile->getJarMods())
{
QStringList jar, temp1, temp2, temp3;
jarmod->getApplicableFiles(currentSystem, jar, temp1, temp2, temp3, jarmodsPath().absolutePath());
// QString filePath = jarmodsPath().absoluteFilePath(jarmod->filename(currentSystem));
- mods.push_back(Mod(QFileInfo(jar[0])));
+ mods.push_back(new Mod(QFileInfo(jar[0])));
}
return mods;
}
diff --git a/launcher/minecraft/MinecraftInstance.h b/launcher/minecraft/MinecraftInstance.h
index 05450d41..8e1c67f2 100644
--- a/launcher/minecraft/MinecraftInstance.h
+++ b/launcher/minecraft/MinecraftInstance.h
@@ -81,7 +81,7 @@ public:
shared_qobject_ptr<LaunchTask> createLaunchTask(AuthSessionPtr account, MinecraftServerTargetPtr serverToJoin) override;
QStringList extraArguments() const override;
QStringList verboseDescription(AuthSessionPtr session, MinecraftServerTargetPtr serverToJoin) override;
- QList<Mod> getJarMods() const;
+ QList<Mod*> getJarMods() const;
QString createLaunchScript(AuthSessionPtr session, MinecraftServerTargetPtr serverToJoin);
/// get arguments passed to java
QStringList javaArguments() const;
diff --git a/launcher/minecraft/World.cpp b/launcher/minecraft/World.cpp
index dfcb43d8..90fcf337 100644
--- a/launcher/minecraft/World.cpp
+++ b/launcher/minecraft/World.cpp
@@ -53,12 +53,12 @@
#include <QCoreApplication>
-#include <nonstd/optional>
+#include <optional>
-using nonstd::optional;
-using nonstd::nullopt;
+using std::optional;
+using std::nullopt;
-GameType::GameType(nonstd::optional<int> original):
+GameType::GameType(std::optional<int> original):
original(original)
{
if(!original) {
diff --git a/launcher/minecraft/World.h b/launcher/minecraft/World.h
index 0f587620..8327253a 100644
--- a/launcher/minecraft/World.h
+++ b/launcher/minecraft/World.h
@@ -16,11 +16,11 @@
#pragma once
#include <QFileInfo>
#include <QDateTime>
-#include <nonstd/optional>
+#include <optional>
struct GameType {
GameType() = default;
- GameType (nonstd::optional<int> original);
+ GameType (std::optional<int> original);
QString toTranslatedString() const;
QString toLogString() const;
@@ -33,7 +33,7 @@ struct GameType {
Adventure,
Spectator
} type = Unknown;
- nonstd::optional<int> original;
+ std::optional<int> original;
};
class World
diff --git a/launcher/minecraft/auth/AccountList.cpp b/launcher/minecraft/auth/AccountList.cpp
index 2b851e18..b3b57c74 100644
--- a/launcher/minecraft/auth/AccountList.cpp
+++ b/launcher/minecraft/auth/AccountList.cpp
@@ -109,8 +109,10 @@ QStringList AccountList::profileNames() const {
void AccountList::addAccount(const MinecraftAccountPtr account)
{
- // NOTE: Do not allow adding something that's already there
- if(m_accounts.contains(account)) {
+ // NOTE: Do not allow adding something that's already there. We shouldn't let it continue
+ // because of the signal / slot connections after this.
+ if (m_accounts.contains(account)) {
+ qDebug() << "Tried to add account that's already on the accounts list!";
return;
}
@@ -123,6 +125,8 @@ void AccountList::addAccount(const MinecraftAccountPtr account)
if(profileId.size()) {
auto existingAccount = findAccountByProfileId(profileId);
if(existingAccount != -1) {
+ qDebug() << "Replacing old account with a new one with the same profile ID!";
+
MinecraftAccountPtr existingAccountPtr = m_accounts[existingAccount];
m_accounts[existingAccount] = account;
if(m_defaultAccount == existingAccountPtr) {
@@ -138,9 +142,12 @@ void AccountList::addAccount(const MinecraftAccountPtr account)
// if we don't have this profileId yet, add the account to the end
int row = m_accounts.count();
+ qDebug() << "Inserting account at index" << row;
+
beginInsertRows(QModelIndex(), row, row);
m_accounts.append(account);
endInsertRows();
+
onListChanged();
}
diff --git a/launcher/minecraft/auth/steps/LauncherLoginStep.cpp b/launcher/minecraft/auth/steps/LauncherLoginStep.cpp
index f5697223..8c53f037 100644
--- a/launcher/minecraft/auth/steps/LauncherLoginStep.cpp
+++ b/launcher/minecraft/auth/steps/LauncherLoginStep.cpp
@@ -5,6 +5,7 @@
#include "minecraft/auth/AuthRequest.h"
#include "minecraft/auth/Parsers.h"
#include "minecraft/auth/AccountTask.h"
+#include "net/NetUtils.h"
LauncherLoginStep::LauncherLoginStep(AccountData* data) : AuthStep(data) {
@@ -58,10 +59,18 @@ void LauncherLoginStep::onRequestDone(
#ifndef NDEBUG
qDebug() << data;
#endif
- emit finished(
- AccountTaskState::STATE_FAILED_SOFT,
- tr("Failed to get Minecraft access token: %1").arg(requestor->errorString_)
- );
+ if (Net::isApplicationError(error)) {
+ emit finished(
+ AccountTaskState::STATE_FAILED_SOFT,
+ tr("Failed to get Minecraft access token: %1").arg(requestor->errorString_)
+ );
+ }
+ else {
+ emit finished(
+ AccountTaskState::STATE_OFFLINE,
+ tr("Failed to get Minecraft access token: %1").arg(requestor->errorString_)
+ );
+ }
return;
}
diff --git a/launcher/minecraft/auth/steps/MinecraftProfileStep.cpp b/launcher/minecraft/auth/steps/MinecraftProfileStep.cpp
index add91659..b39b9326 100644
--- a/launcher/minecraft/auth/steps/MinecraftProfileStep.cpp
+++ b/launcher/minecraft/auth/steps/MinecraftProfileStep.cpp
@@ -4,6 +4,7 @@
#include "minecraft/auth/AuthRequest.h"
#include "minecraft/auth/Parsers.h"
+#include "net/NetUtils.h"
MinecraftProfileStep::MinecraftProfileStep(AccountData* data) : AuthStep(data) {
@@ -64,10 +65,18 @@ void MinecraftProfileStep::onRequestDone(
qWarning() << " Response:";
qWarning() << QString::fromUtf8(data);
- emit finished(
- AccountTaskState::STATE_FAILED_SOFT,
- tr("Minecraft Java profile acquisition failed.")
- );
+ if (Net::isApplicationError(error)) {
+ emit finished(
+ AccountTaskState::STATE_FAILED_SOFT,
+ tr("Minecraft Java profile acquisition failed: %1").arg(requestor->errorString_)
+ );
+ }
+ else {
+ emit finished(
+ AccountTaskState::STATE_OFFLINE,
+ tr("Minecraft Java profile acquisition failed: %1").arg(requestor->errorString_)
+ );
+ }
return;
}
if(!Parsers::parseMinecraftProfile(data, m_data->minecraftProfile)) {
diff --git a/launcher/minecraft/auth/steps/MinecraftProfileStepMojang.cpp b/launcher/minecraft/auth/steps/MinecraftProfileStepMojang.cpp
index d3035272..6a1eb7a0 100644
--- a/launcher/minecraft/auth/steps/MinecraftProfileStepMojang.cpp
+++ b/launcher/minecraft/auth/steps/MinecraftProfileStepMojang.cpp
@@ -4,6 +4,7 @@
#include "minecraft/auth/AuthRequest.h"
#include "minecraft/auth/Parsers.h"
+#include "net/NetUtils.h"
MinecraftProfileStepMojang::MinecraftProfileStepMojang(AccountData* data) : AuthStep(data) {
@@ -67,10 +68,18 @@ void MinecraftProfileStepMojang::onRequestDone(
qWarning() << " Response:";
qWarning() << QString::fromUtf8(data);
- emit finished(
- AccountTaskState::STATE_FAILED_SOFT,
- tr("Minecraft Java profile acquisition failed.")
- );
+ if (Net::isApplicationError(error)) {
+ emit finished(
+ AccountTaskState::STATE_FAILED_SOFT,
+ tr("Minecraft Java profile acquisition failed: %1").arg(requestor->errorString_)
+ );
+ }
+ else {
+ emit finished(
+ AccountTaskState::STATE_OFFLINE,
+ tr("Minecraft Java profile acquisition failed: %1").arg(requestor->errorString_)
+ );
+ }
return;
}
if(!Parsers::parseMinecraftProfileMojang(data, m_data->minecraftProfile)) {
diff --git a/launcher/minecraft/auth/steps/XboxAuthorizationStep.cpp b/launcher/minecraft/auth/steps/XboxAuthorizationStep.cpp
index 589768e3..14bde47e 100644
--- a/launcher/minecraft/auth/steps/XboxAuthorizationStep.cpp
+++ b/launcher/minecraft/auth/steps/XboxAuthorizationStep.cpp
@@ -6,6 +6,7 @@
#include "minecraft/auth/AuthRequest.h"
#include "minecraft/auth/Parsers.h"
+#include "net/NetUtils.h"
XboxAuthorizationStep::XboxAuthorizationStep(AccountData* data, Katabasis::Token *token, QString relyingParty, QString authorizationKind):
AuthStep(data),
@@ -62,10 +63,24 @@ void XboxAuthorizationStep::onRequestDone(
#endif
if (error != QNetworkReply::NoError) {
qWarning() << "Reply error:" << error;
- if(!processSTSError(error, data, headers)) {
+ if (Net::isApplicationError(error)) {
+ if(!processSTSError(error, data, headers)) {
+ emit finished(
+ AccountTaskState::STATE_FAILED_SOFT,
+ tr("Failed to get authorization for %1 services. Error %2.").arg(m_authorizationKind, error)
+ );
+ }
+ else {
+ emit finished(
+ AccountTaskState::STATE_FAILED_SOFT,
+ tr("Unknown STS error for %1 services: %2").arg(m_authorizationKind, requestor->errorString_)
+ );
+ }
+ }
+ else {
emit finished(
- AccountTaskState::STATE_FAILED_SOFT,
- tr("Failed to get authorization for %1 services. Error %2.").arg(m_authorizationKind, error)
+ AccountTaskState::STATE_OFFLINE,
+ tr("Failed to get authorization for %1 services: %2").arg(m_authorizationKind, requestor->errorString_)
);
}
return;
diff --git a/launcher/minecraft/auth/steps/XboxProfileStep.cpp b/launcher/minecraft/auth/steps/XboxProfileStep.cpp
index 9f50138e..738fe1db 100644
--- a/launcher/minecraft/auth/steps/XboxProfileStep.cpp
+++ b/launcher/minecraft/auth/steps/XboxProfileStep.cpp
@@ -6,6 +6,7 @@
#include "minecraft/auth/AuthRequest.h"
#include "minecraft/auth/Parsers.h"
+#include "net/NetUtils.h"
XboxProfileStep::XboxProfileStep(AccountData* data) : AuthStep(data) {
@@ -58,10 +59,18 @@ void XboxProfileStep::onRequestDone(
#ifndef NDEBUG
qDebug() << data;
#endif
- finished(
- AccountTaskState::STATE_FAILED_SOFT,
- tr("Failed to retrieve the Xbox profile.")
- );
+ if (Net::isApplicationError(error)) {
+ emit finished(
+ AccountTaskState::STATE_FAILED_SOFT,
+ tr("Failed to retrieve the Xbox profile: %1").arg(requestor->errorString_)
+ );
+ }
+ else {
+ emit finished(
+ AccountTaskState::STATE_OFFLINE,
+ tr("Failed to retrieve the Xbox profile: %1").arg(requestor->errorString_)
+ );
+ }
return;
}
diff --git a/launcher/minecraft/auth/steps/XboxUserStep.cpp b/launcher/minecraft/auth/steps/XboxUserStep.cpp
index a38a28e4..53069597 100644
--- a/launcher/minecraft/auth/steps/XboxUserStep.cpp
+++ b/launcher/minecraft/auth/steps/XboxUserStep.cpp
@@ -4,6 +4,7 @@
#include "minecraft/auth/AuthRequest.h"
#include "minecraft/auth/Parsers.h"
+#include "net/NetUtils.h"
XboxUserStep::XboxUserStep(AccountData* data) : AuthStep(data) {
@@ -53,7 +54,17 @@ void XboxUserStep::onRequestDone(
if (error != QNetworkReply::NoError) {
qWarning() << "Reply error:" << error;
- emit finished(AccountTaskState::STATE_FAILED_SOFT, tr("XBox user authentication failed."));
+ if (Net::isApplicationError(error)) {
+ emit finished(AccountTaskState::STATE_FAILED_SOFT,
+ tr("XBox user authentication failed: %1").arg(requestor->errorString_)
+ );
+ }
+ else {
+ emit finished(
+ AccountTaskState::STATE_OFFLINE,
+ tr("XBox user authentication failed: %1").arg(requestor->errorString_)
+ );
+ }
return;
}
diff --git a/launcher/minecraft/mod/MetadataHandler.h b/launcher/minecraft/mod/MetadataHandler.h
index 56962818..39723b49 100644
--- a/launcher/minecraft/mod/MetadataHandler.h
+++ b/launcher/minecraft/mod/MetadataHandler.h
@@ -37,9 +37,9 @@ class Metadata {
return Packwiz::V1::createModFormat(index_dir, mod_pack, mod_version);
}
- static auto create(QDir& index_dir, Mod& internal_mod) -> ModStruct
+ static auto create(QDir& index_dir, Mod& internal_mod, QString mod_slug) -> ModStruct
{
- return Packwiz::V1::createModFormat(index_dir, internal_mod);
+ return Packwiz::V1::createModFormat(index_dir, internal_mod, mod_slug);
}
static void update(QDir& index_dir, ModStruct& mod)
@@ -47,13 +47,23 @@ class Metadata {
Packwiz::V1::updateModIndex(index_dir, mod);
}
- static void remove(QDir& index_dir, QString& mod_name)
+ static void remove(QDir& index_dir, QString mod_slug)
{
- Packwiz::V1::deleteModIndex(index_dir, mod_name);
+ Packwiz::V1::deleteModIndex(index_dir, mod_slug);
}
- static auto get(QDir& index_dir, QString& mod_name) -> ModStruct
+ static void remove(QDir& index_dir, QVariant& mod_id)
{
- return Packwiz::V1::getIndexForMod(index_dir, mod_name);
+ Packwiz::V1::deleteModIndex(index_dir, mod_id);
+ }
+
+ static auto get(QDir& index_dir, QString mod_slug) -> ModStruct
+ {
+ return Packwiz::V1::getIndexForMod(index_dir, mod_slug);
+ }
+
+ static auto get(QDir& index_dir, QVariant& mod_id) -> ModStruct
+ {
+ return Packwiz::V1::getIndexForMod(index_dir, mod_id);
}
};
diff --git a/launcher/minecraft/mod/Mod.cpp b/launcher/minecraft/mod/Mod.cpp
index 742709e3..588d76e3 100644
--- a/launcher/minecraft/mod/Mod.cpp
+++ b/launcher/minecraft/mod/Mod.cpp
@@ -150,25 +150,30 @@ void Mod::setStatus(ModStatus status)
m_temp_status = status;
}
}
-void Mod::setMetadata(Metadata::ModStruct* metadata)
+void Mod::setMetadata(const Metadata::ModStruct& metadata)
{
if (status() == ModStatus::NoMetadata)
setStatus(ModStatus::Installed);
if (m_localDetails) {
- m_localDetails->metadata.reset(metadata);
+ m_localDetails->metadata = std::make_shared<Metadata::ModStruct>(std::move(metadata));
} else {
- m_temp_metadata.reset(metadata);
+ m_temp_metadata = std::make_shared<Metadata::ModStruct>(std::move(metadata));
}
}
-auto Mod::destroy(QDir& index_dir) -> bool
+auto Mod::destroy(QDir& index_dir, bool preserve_metadata) -> bool
{
- auto n = name();
- // FIXME: This can fail to remove the metadata if the
- // "ModMetadataDisabled" setting is on, since there could
- // be a name mismatch!
- Metadata::remove(index_dir, n);
+ if (!preserve_metadata) {
+ qDebug() << QString("Destroying metadata for '%1' on purpose").arg(name());
+
+ if (metadata()) {
+ Metadata::remove(index_dir, metadata()->slug);
+ } else {
+ auto n = name();
+ Metadata::remove(index_dir, n);
+ }
+ }
m_type = MOD_UNKNOWN;
return FS::deletePath(m_file.filePath());
@@ -182,9 +187,12 @@ auto Mod::details() const -> const ModDetails&
auto Mod::name() const -> QString
{
auto d_name = details().name;
- if (!d_name.isEmpty()) {
+ if (!d_name.isEmpty())
return d_name;
- }
+
+ if (metadata())
+ return metadata()->name;
+
return m_name;
}
@@ -235,11 +243,10 @@ void Mod::finishResolvingWithDetails(std::shared_ptr<ModDetails> details)
m_resolved = true;
m_localDetails = details;
+ setStatus(m_temp_status);
+
if (m_localDetails && m_temp_metadata && m_temp_metadata->isValid()) {
- m_localDetails->metadata = m_temp_metadata;
- if (status() == ModStatus::NoMetadata)
- setStatus(ModStatus::Installed);
+ setMetadata(*m_temp_metadata);
+ m_temp_metadata.reset();
}
-
- setStatus(m_temp_status);
}
diff --git a/launcher/minecraft/mod/Mod.h b/launcher/minecraft/mod/Mod.h
index 5f9c4684..7a13e44b 100644
--- a/launcher/minecraft/mod/Mod.h
+++ b/launcher/minecraft/mod/Mod.h
@@ -39,10 +39,12 @@
#include <QFileInfo>
#include <QList>
+#include "QObjectPtr.h"
#include "ModDetails.h"
-class Mod
+class Mod : public QObject
{
+ Q_OBJECT
public:
enum ModType
{
@@ -53,6 +55,8 @@ public:
MOD_LITEMOD, //!< The mod is a litemod
};
+ using Ptr = shared_qobject_ptr<Mod>;
+
Mod() = default;
Mod(const QFileInfo &file);
explicit Mod(const QDir& mods_dir, const Metadata::ModStruct& metadata);
@@ -77,12 +81,12 @@ public:
auto metadata() const -> const std::shared_ptr<Metadata::ModStruct>;
void setStatus(ModStatus status);
- void setMetadata(Metadata::ModStruct* metadata);
+ void setMetadata(const Metadata::ModStruct& metadata);
auto enable(bool value) -> bool;
// delete all the files of this mod
- auto destroy(QDir& index_dir) -> bool;
+ auto destroy(QDir& index_dir, bool preserve_metadata = false) -> bool;
// change the mod's filesystem path (used by mod lists for *MAGIC* purposes)
void repath(const QFileInfo &file);
@@ -111,7 +115,7 @@ protected:
std::shared_ptr<Metadata::ModStruct> m_temp_metadata;
/* Set the mod status while it doesn't have local details just yet */
- ModStatus m_temp_status = ModStatus::NotInstalled;
+ ModStatus m_temp_status = ModStatus::NoMetadata;
std::shared_ptr<ModDetails> m_localDetails;
diff --git a/launcher/minecraft/mod/ModFolderModel.cpp b/launcher/minecraft/mod/ModFolderModel.cpp
index 5ee08cbf..112d219e 100644
--- a/launcher/minecraft/mod/ModFolderModel.cpp
+++ b/launcher/minecraft/mod/ModFolderModel.cpp
@@ -65,15 +65,21 @@ void ModFolderModel::startWatching()
update();
+ // Watch the mods folder
is_watching = m_watcher->addPath(m_dir.absolutePath());
- if (is_watching)
- {
+ if (is_watching) {
qDebug() << "Started watching " << m_dir.absolutePath();
- }
- else
- {
+ } else {
qDebug() << "Failed to start watching " << m_dir.absolutePath();
}
+
+ // Watch the mods index folder
+ is_watching = m_watcher->addPath(indexDir().absolutePath());
+ if (is_watching) {
+ qDebug() << "Started watching " << indexDir().absolutePath();
+ } else {
+ qDebug() << "Failed to start watching " << indexDir().absolutePath();
+ }
}
void ModFolderModel::stopWatching()
@@ -82,14 +88,18 @@ void ModFolderModel::stopWatching()
return;
is_watching = !m_watcher->removePath(m_dir.absolutePath());
- if (!is_watching)
- {
+ if (!is_watching) {
qDebug() << "Stopped watching " << m_dir.absolutePath();
- }
- else
- {
+ } else {
qDebug() << "Failed to stop watching " << m_dir.absolutePath();
}
+
+ is_watching = !m_watcher->removePath(indexDir().absolutePath());
+ if (!is_watching) {
+ qDebug() << "Stopped watching " << indexDir().absolutePath();
+ } else {
+ qDebug() << "Failed to stop watching " << indexDir().absolutePath();
+ }
}
bool ModFolderModel::update()
@@ -124,7 +134,7 @@ void ModFolderModel::finishUpdate()
QSet<QString> newSet(newList.begin(), newList.end());
#else
QSet<QString> currentSet = modsIndex.keys().toSet();
- auto & newMods = m_update->mods;
+ auto& newMods = m_update->mods;
QSet<QString> newSet = newMods.keys().toSet();
#endif
@@ -132,19 +142,20 @@ void ModFolderModel::finishUpdate()
{
QSet<QString> kept = currentSet;
kept.intersect(newSet);
- for(auto & keptMod: kept) {
- auto & newMod = newMods[keptMod];
+ for(auto& keptMod : kept) {
+ auto newMod = newMods[keptMod];
auto row = modsIndex[keptMod];
- auto & currentMod = mods[row];
- if(newMod.dateTimeChanged() == currentMod.dateTimeChanged()) {
+ auto currentMod = mods[row];
+ if(newMod->dateTimeChanged() == currentMod->dateTimeChanged()) {
// no significant change, ignore...
continue;
}
- auto & oldMod = mods[row];
- if(oldMod.isResolving()) {
- activeTickets.remove(oldMod.resolutionTicket());
+ auto oldMod = mods[row];
+ if(oldMod->isResolving()) {
+ activeTickets.remove(oldMod->resolutionTicket());
}
- oldMod = newMod;
+
+ mods[row] = newMod;
resolveMod(mods[row]);
emit dataChanged(index(row, 0), index(row, columnCount(QModelIndex()) - 1));
}
@@ -163,9 +174,10 @@ void ModFolderModel::finishUpdate()
int removedIndex = *iter;
beginRemoveRows(QModelIndex(), removedIndex, removedIndex);
auto removedIter = mods.begin() + removedIndex;
- if(removedIter->isResolving()) {
- activeTickets.remove(removedIter->resolutionTicket());
+ if((*removedIter)->isResolving()) {
+ activeTickets.remove((*removedIter)->resolutionTicket());
}
+
mods.erase(removedIter);
endRemoveRows();
}
@@ -191,8 +203,8 @@ void ModFolderModel::finishUpdate()
{
modsIndex.clear();
int idx = 0;
- for(auto & mod: mods) {
- modsIndex[mod.internal_id()] = idx;
+ for(auto mod: mods) {
+ modsIndex[mod->internal_id()] = idx;
idx++;
}
}
@@ -207,17 +219,17 @@ void ModFolderModel::finishUpdate()
}
}
-void ModFolderModel::resolveMod(Mod& m)
+void ModFolderModel::resolveMod(Mod::Ptr m)
{
- if(!m.shouldResolve()) {
+ if(!m->shouldResolve()) {
return;
}
- auto task = new LocalModParseTask(nextResolutionTicket, m.type(), m.fileinfo());
+ auto task = new LocalModParseTask(nextResolutionTicket, m->type(), m->fileinfo());
auto result = task->result();
- result->id = m.internal_id();
+ result->id = m->internal_id();
activeTickets.insert(nextResolutionTicket, result);
- m.setResolving(true, nextResolutionTicket);
+ m->setResolving(true, nextResolutionTicket);
nextResolutionTicket++;
QThreadPool *threadPool = QThreadPool::globalInstance();
connect(task, &LocalModParseTask::finished, this, &ModFolderModel::finishModParse);
@@ -233,8 +245,8 @@ void ModFolderModel::finishModParse(int token)
auto result = *iter;
activeTickets.remove(token);
int row = modsIndex[result->id];
- auto & mod = mods[row];
- mod.finishResolvingWithDetails(result->details);
+ auto mod = mods[row];
+ mod->finishResolvingWithDetails(result->details);
emit dataChanged(index(row), index(row, columnCount(QModelIndex()) - 1));
}
@@ -259,6 +271,18 @@ bool ModFolderModel::isValid()
return m_dir.exists() && m_dir.isReadable();
}
+auto ModFolderModel::selectedMods(QModelIndexList& indexes) -> QList<Mod::Ptr>
+{
+ QList<Mod::Ptr> selected_mods;
+ for (auto i : indexes) {
+ if(i.column() != 0)
+ continue;
+
+ selected_mods.push_back(mods[i.row()]);
+ }
+ return selected_mods;
+}
+
// FIXME: this does not take disabled mod (with extra .disable extension) into account...
bool ModFolderModel::installMod(const QString &filename)
{
@@ -344,6 +368,20 @@ bool ModFolderModel::installMod(const QString &filename)
return false;
}
+bool ModFolderModel::uninstallMod(const QString& filename, bool preserve_metadata)
+{
+
+ for(auto mod : allMods()){
+ if(mod->fileinfo().fileName() == filename){
+ auto index_dir = indexDir();
+ mod->destroy(index_dir, preserve_metadata);
+ return true;
+ }
+ }
+
+ return false;
+}
+
bool ModFolderModel::setModStatus(const QModelIndexList& indexes, ModStatusAction enable)
{
if(interaction_disabled) {
@@ -377,9 +415,9 @@ bool ModFolderModel::deleteMods(const QModelIndexList& indexes)
if(i.column() != 0) {
continue;
}
- Mod &m = mods[i.row()];
+ auto m = mods[i.row()];
auto index_dir = indexDir();
- m.destroy(index_dir);
+ m->destroy(index_dir);
}
return true;
}
@@ -406,9 +444,9 @@ QVariant ModFolderModel::data(const QModelIndex &index, int role) const
switch (column)
{
case NameColumn:
- return mods[row].name();
+ return mods[row]->name();
case VersionColumn: {
- switch(mods[row].type()) {
+ switch(mods[row]->type()) {
case Mod::MOD_FOLDER:
return tr("Folder");
case Mod::MOD_SINGLEFILE:
@@ -416,23 +454,23 @@ QVariant ModFolderModel::data(const QModelIndex &index, int role) const
default:
break;
}
- return mods[row].version();
+ return mods[row]->version();
}
case DateColumn:
- return mods[row].dateTimeChanged();
+ return mods[row]->dateTimeChanged();
default:
return QVariant();
}
case Qt::ToolTipRole:
- return mods[row].internal_id();
+ return mods[row]->internal_id();
case Qt::CheckStateRole:
switch (column)
{
case ActiveColumn:
- return mods[row].enabled() ? Qt::Checked : Qt::Unchecked;
+ return mods[row]->enabled() ? Qt::Checked : Qt::Unchecked;
default:
return QVariant();
}
@@ -472,20 +510,20 @@ bool ModFolderModel::setModStatus(int row, ModFolderModel::ModStatusAction actio
break;
case Toggle:
default:
- desiredStatus = !mod.enabled();
+ desiredStatus = !mod->enabled();
break;
}
- if(desiredStatus == mod.enabled()) {
+ if(desiredStatus == mod->enabled()) {
return true;
}
// preserve the row, but change its ID
- auto oldId = mod.internal_id();
- if(!mod.enable(!mod.enabled())) {
+ auto oldId = mod->internal_id();
+ if(!mod->enable(!mod->enabled())) {
return false;
}
- auto newId = mod.internal_id();
+ auto newId = mod->internal_id();
if(modsIndex.contains(newId)) {
// NOTE: this could handle a corner case, where we are overwriting a file, because the same 'mod' exists both enabled and disabled
// But is it necessary?
diff --git a/launcher/minecraft/mod/ModFolderModel.h b/launcher/minecraft/mod/ModFolderModel.h
index 24b4d358..a7d3ece0 100644
--- a/launcher/minecraft/mod/ModFolderModel.h
+++ b/launcher/minecraft/mod/ModFolderModel.h
@@ -101,13 +101,13 @@ public:
{
return size() == 0;
}
- Mod &operator[](size_t index)
+ Mod& operator[](size_t index)
{
- return mods[index];
+ return *mods[index];
}
- const Mod &at(size_t index) const
+ const Mod& at(size_t index) const
{
- return mods.at(index);
+ return *mods.at(index);
}
/// Reloads the mod list and returns true if the list changed.
@@ -118,6 +118,8 @@ public:
*/
bool installMod(const QString& filename);
+ bool uninstallMod(const QString& filename, bool preserve_metadata = false);
+
/// Deletes all the selected mods
bool deleteMods(const QModelIndexList &indexes);
@@ -139,11 +141,13 @@ public:
return { QString("%1/.index").arg(dir().absolutePath()) };
}
- const QList<Mod> & allMods()
+ const QList<Mod::Ptr>& allMods()
{
return mods;
}
+ auto selectedMods(QModelIndexList& indexes) -> QList<Mod::Ptr>;
+
public slots:
void disableInteraction(bool disabled);
@@ -157,7 +161,7 @@ signals:
void updateFinished();
private:
- void resolveMod(Mod& m);
+ void resolveMod(Mod::Ptr m);
bool setModStatus(int index, ModStatusAction action);
protected:
@@ -171,5 +175,5 @@ protected:
QMap<QString, int> modsIndex;
QMap<int, LocalModParseTask::ResultPtr> activeTickets;
int nextResolutionTicket = 0;
- QList<Mod> mods;
+ QList<Mod::Ptr> mods;
};
diff --git a/launcher/minecraft/mod/tasks/LocalModParseTask.cpp b/launcher/minecraft/mod/tasks/LocalModParseTask.cpp
index 3354732b..1519f49d 100644
--- a/launcher/minecraft/mod/tasks/LocalModParseTask.cpp
+++ b/launcher/minecraft/mod/tasks/LocalModParseTask.cpp
@@ -4,6 +4,7 @@
#include <QJsonObject>
#include <QJsonArray>
#include <QJsonValue>
+#include <QString>
#include <quazip/quazip.h>
#include <quazip/quazipfile.h>
#include <toml.h>
@@ -71,7 +72,13 @@ std::shared_ptr<ModDetails> ReadMCModInfo(QByteArray contents)
if(val.isUndefined()) {
val = jsonDoc.object().value("modListVersion");
}
- int version = val.toDouble();
+
+ int version = Json::ensureInteger(val, -1);
+
+ // Some mods set the number with "", so it's a String instead
+ if (version < 0)
+ version = Json::ensureString(val, "").toInt();
+
if (version != 2)
{
qCritical() << "BAD stuff happened to mod json:";
diff --git a/launcher/minecraft/mod/tasks/LocalModUpdateTask.cpp b/launcher/minecraft/mod/tasks/LocalModUpdateTask.cpp
index 1bdecb8c..4b878918 100644
--- a/launcher/minecraft/mod/tasks/LocalModUpdateTask.cpp
+++ b/launcher/minecraft/mod/tasks/LocalModUpdateTask.cpp
@@ -44,10 +44,21 @@ void LocalModUpdateTask::executeTask()
{
setStatus(tr("Updating index for mod:\n%1").arg(m_mod.name));
- auto pw_mod = Metadata::create(m_index_dir, m_mod, m_mod_version);
- Metadata::update(m_index_dir, pw_mod);
+ auto old_metadata = Metadata::get(m_index_dir, m_mod.addonId);
+ if (old_metadata.isValid()) {
+ emit hasOldMod(old_metadata.name, old_metadata.filename);
+ if (m_mod.slug.isEmpty())
+ m_mod.slug = old_metadata.slug;
+ }
- emitSucceeded();
+ auto pw_mod = Metadata::create(m_index_dir, m_mod, m_mod_version);
+ if (pw_mod.isValid()) {
+ Metadata::update(m_index_dir, pw_mod);
+ emitSucceeded();
+ } else {
+ qCritical() << "Tried to update an invalid mod!";
+ emitFailed(tr("Invalid metadata"));
+ }
}
auto LocalModUpdateTask::abort() -> bool
diff --git a/launcher/minecraft/mod/tasks/LocalModUpdateTask.h b/launcher/minecraft/mod/tasks/LocalModUpdateTask.h
index 2db183e0..1d2f06a6 100644
--- a/launcher/minecraft/mod/tasks/LocalModUpdateTask.h
+++ b/launcher/minecraft/mod/tasks/LocalModUpdateTask.h
@@ -37,6 +37,9 @@ class LocalModUpdateTask : public Task {
//! Entry point for tasks.
void executeTask() override;
+ signals:
+ void hasOldMod(QString name, QString filename);
+
private:
QDir m_index_dir;
ModPlatform::IndexedPack& m_mod;
diff --git a/launcher/minecraft/mod/tasks/ModFolderLoadTask.cpp b/launcher/minecraft/mod/tasks/ModFolderLoadTask.cpp
index 276414e4..9b70e7a1 100644
--- a/launcher/minecraft/mod/tasks/ModFolderLoadTask.cpp
+++ b/launcher/minecraft/mod/tasks/ModFolderLoadTask.cpp
@@ -36,7 +36,6 @@
#include "ModFolderLoadTask.h"
-#include "Application.h"
#include "minecraft/mod/MetadataHandler.h"
ModFolderLoadTask::ModFolderLoadTask(QDir& mods_dir, QDir& index_dir, bool is_indexed)
@@ -53,37 +52,48 @@ void ModFolderLoadTask::run()
// Read JAR files that don't have metadata
m_mods_dir.refresh();
for (auto entry : m_mods_dir.entryInfoList()) {
- Mod mod(entry);
+ Mod::Ptr mod(new Mod(entry));
- if (mod.enabled()) {
- if (m_result->mods.contains(mod.internal_id())) {
- m_result->mods[mod.internal_id()].setStatus(ModStatus::Installed);
+ if (mod->enabled()) {
+ if (m_result->mods.contains(mod->internal_id())) {
+ m_result->mods[mod->internal_id()]->setStatus(ModStatus::Installed);
}
else {
- m_result->mods[mod.internal_id()] = mod;
- m_result->mods[mod.internal_id()].setStatus(ModStatus::NoMetadata);
+ m_result->mods[mod->internal_id()] = mod;
+ m_result->mods[mod->internal_id()]->setStatus(ModStatus::NoMetadata);
}
}
else {
- QString chopped_id = mod.internal_id().chopped(9);
+ QString chopped_id = mod->internal_id().chopped(9);
if (m_result->mods.contains(chopped_id)) {
- m_result->mods[mod.internal_id()] = mod;
+ m_result->mods[mod->internal_id()] = mod;
- auto metadata = m_result->mods[chopped_id].metadata();
+ auto metadata = m_result->mods[chopped_id]->metadata();
if (metadata) {
- mod.setMetadata(new Metadata::ModStruct(*metadata));
+ mod->setMetadata(*metadata);
- m_result->mods[mod.internal_id()].setStatus(ModStatus::Installed);
+ m_result->mods[mod->internal_id()]->setStatus(ModStatus::Installed);
m_result->mods.remove(chopped_id);
}
}
else {
- m_result->mods[mod.internal_id()] = mod;
- m_result->mods[mod.internal_id()].setStatus(ModStatus::NoMetadata);
+ m_result->mods[mod->internal_id()] = mod;
+ m_result->mods[mod->internal_id()]->setStatus(ModStatus::NoMetadata);
}
}
}
+ // Remove orphan metadata to prevent issues
+ // See https://github.com/PolyMC/PolyMC/issues/996
+ QMutableMapIterator<QString, Mod::Ptr> iter(m_result->mods);
+ while (iter.hasNext()) {
+ auto mod = iter.next().value();
+ if (mod->status() == ModStatus::NotInstalled) {
+ mod->destroy(m_index_dir, false);
+ iter.remove();
+ }
+ }
+
emit succeeded();
}
@@ -97,8 +107,8 @@ void ModFolderLoadTask::getFromMetadata()
return;
}
- Mod mod(m_mods_dir, metadata);
- mod.setStatus(ModStatus::NotInstalled);
- m_result->mods[mod.internal_id()] = mod;
+ auto* mod = new Mod(m_mods_dir, metadata);
+ mod->setStatus(ModStatus::NotInstalled);
+ m_result->mods[mod->internal_id()] = mod;
}
}
diff --git a/launcher/minecraft/mod/tasks/ModFolderLoadTask.h b/launcher/minecraft/mod/tasks/ModFolderLoadTask.h
index 088f873e..0b6bb6cc 100644
--- a/launcher/minecraft/mod/tasks/ModFolderLoadTask.h
+++ b/launcher/minecraft/mod/tasks/ModFolderLoadTask.h
@@ -48,7 +48,7 @@ class ModFolderLoadTask : public QObject, public QRunnable
Q_OBJECT
public:
struct Result {
- QMap<QString, Mod> mods;
+ QMap<QString, Mod::Ptr> mods;
};
using ResultPtr = std::shared_ptr<Result>;
ResultPtr result() const {
diff --git a/launcher/modplatform/CheckUpdateTask.h b/launcher/modplatform/CheckUpdateTask.h
new file mode 100644
index 00000000..91922034
--- /dev/null
+++ b/launcher/modplatform/CheckUpdateTask.h
@@ -0,0 +1,51 @@
+#pragma once
+
+#include "minecraft/mod/Mod.h"
+#include "modplatform/ModAPI.h"
+#include "modplatform/ModIndex.h"
+#include "tasks/Task.h"
+
+class ModDownloadTask;
+class ModFolderModel;
+
+class CheckUpdateTask : public Task {
+ Q_OBJECT
+
+ public:
+ CheckUpdateTask(QList<Mod*>& mods, std::list<Version>& mcVersions, ModAPI::ModLoaderTypes loaders, std::shared_ptr<ModFolderModel> mods_folder)
+ : Task(nullptr), m_mods(mods), m_game_versions(mcVersions), m_loaders(loaders), m_mods_folder(mods_folder) {};
+
+ struct UpdatableMod {
+ QString name;
+ QString old_hash;
+ QString old_version;
+ QString new_version;
+ QString changelog;
+ ModPlatform::Provider provider;
+ ModDownloadTask* download;
+
+ public:
+ UpdatableMod(QString name, QString old_h, QString old_v, QString new_v, QString changelog, ModPlatform::Provider p, ModDownloadTask* t)
+ : name(name), old_hash(old_h), old_version(old_v), new_version(new_v), changelog(changelog), provider(p), download(t)
+ {}
+ };
+
+ auto getUpdatable() -> std::vector<UpdatableMod>&& { return std::move(m_updatable); }
+
+ public slots:
+ bool abort() override = 0;
+
+ protected slots:
+ void executeTask() override = 0;
+
+ signals:
+ void checkFailed(Mod* failed, QString reason, QUrl recover_url = {});
+
+ protected:
+ QList<Mod*>& m_mods;
+ std::list<Version>& m_game_versions;
+ ModAPI::ModLoaderTypes m_loaders;
+ std::shared_ptr<ModFolderModel> m_mods_folder;
+
+ std::vector<UpdatableMod> m_updatable;
+};
diff --git a/launcher/modplatform/EnsureMetadataTask.cpp b/launcher/modplatform/EnsureMetadataTask.cpp
new file mode 100644
index 00000000..60c54c4e
--- /dev/null
+++ b/launcher/modplatform/EnsureMetadataTask.cpp
@@ -0,0 +1,534 @@
+#include "EnsureMetadataTask.h"
+
+#include <MurmurHash2.h>
+#include <QDebug>
+
+#include "FileSystem.h"
+#include "Json.h"
+#include "minecraft/mod/Mod.h"
+#include "minecraft/mod/tasks/LocalModUpdateTask.h"
+#include "modplatform/flame/FlameAPI.h"
+#include "modplatform/flame/FlameModIndex.h"
+#include "modplatform/modrinth/ModrinthAPI.h"
+#include "modplatform/modrinth/ModrinthPackIndex.h"
+#include "net/NetJob.h"
+#include "tasks/MultipleOptionsTask.h"
+
+static ModPlatform::ProviderCapabilities ProviderCaps;
+
+static ModrinthAPI modrinth_api;
+static FlameAPI flame_api;
+
+EnsureMetadataTask::EnsureMetadataTask(Mod* mod, QDir dir, ModPlatform::Provider prov) : Task(nullptr), m_index_dir(dir), m_provider(prov)
+{
+ auto hash = getHash(mod);
+ if (hash.isEmpty())
+ emitFail(mod);
+ else
+ m_mods.insert(hash, mod);
+}
+
+EnsureMetadataTask::EnsureMetadataTask(QList<Mod*>& mods, QDir dir, ModPlatform::Provider prov)
+ : Task(nullptr), m_index_dir(dir), m_provider(prov)
+{
+ for (auto* mod : mods) {
+ if (!mod->valid()) {
+ emitFail(mod);
+ continue;
+ }
+
+ auto hash = getHash(mod);
+ if (hash.isEmpty()) {
+ emitFail(mod);
+ continue;
+ }
+
+ m_mods.insert(hash, mod);
+ }
+}
+
+QString EnsureMetadataTask::getHash(Mod* mod)
+{
+ /* Here we create a mapping hash -> mod, because we need that relationship to parse the API routes */
+ QByteArray jar_data;
+ try {
+ jar_data = FS::read(mod->fileinfo().absoluteFilePath());
+ } catch (FS::FileSystemException& e) {
+ qCritical() << QString("Failed to open / read JAR file of %1").arg(mod->name());
+ qCritical() << QString("Reason: ") << e.cause();
+
+ return {};
+ }
+
+ switch (m_provider) {
+ case ModPlatform::Provider::MODRINTH: {
+ auto hash_type = ProviderCaps.hashType(ModPlatform::Provider::MODRINTH).first();
+
+ return QString(ProviderCaps.hash(ModPlatform::Provider::MODRINTH, jar_data, hash_type).toHex());
+ }
+ case ModPlatform::Provider::FLAME: {
+ QByteArray jar_data_treated;
+ for (char c : jar_data) {
+ // CF-specific
+ if (!(c == 9 || c == 10 || c == 13 || c == 32))
+ jar_data_treated.push_back(c);
+ }
+
+ return QString::number(MurmurHash2(jar_data_treated, jar_data_treated.length()));
+ }
+ }
+
+ return {};
+}
+
+bool EnsureMetadataTask::abort()
+{
+ // Prevent sending signals to a dead object
+ disconnect(this, 0, 0, 0);
+
+ if (m_current_task)
+ return m_current_task->abort();
+ return true;
+}
+
+void EnsureMetadataTask::executeTask()
+{
+ setStatus(tr("Checking if mods have metadata..."));
+
+ for (auto* mod : m_mods) {
+ if (!mod->valid()) {
+ qDebug() << "Mod" << mod->name() << "is invalid!";
+ emitFail(mod);
+ continue;
+ }
+
+ // They already have the right metadata :o
+ if (mod->status() != ModStatus::NoMetadata && mod->metadata() && mod->metadata()->provider == m_provider) {
+ qDebug() << "Mod" << mod->name() << "already has metadata!";
+ emitReady(mod);
+ continue;
+ }
+
+ // Folders don't have metadata
+ if (mod->type() == Mod::MOD_FOLDER) {
+ emitReady(mod);
+ }
+ }
+
+ NetJob::Ptr version_task;
+
+ switch (m_provider) {
+ case (ModPlatform::Provider::MODRINTH):
+ version_task = modrinthVersionsTask();
+ break;
+ case (ModPlatform::Provider::FLAME):
+ version_task = flameVersionsTask();
+ break;
+ }
+
+ auto invalidade_leftover = [this] {
+ QMutableHashIterator<QString, Mod*> mods_iter(m_mods);
+ while (mods_iter.hasNext()) {
+ auto mod = mods_iter.next();
+ emitFail(mod.value());
+ }
+
+ emitSucceeded();
+ };
+
+ connect(version_task.get(), &Task::finished, this, [this, invalidade_leftover] {
+ NetJob::Ptr project_task;
+
+ switch (m_provider) {
+ case (ModPlatform::Provider::MODRINTH):
+ project_task = modrinthProjectsTask();
+ break;
+ case (ModPlatform::Provider::FLAME):
+ project_task = flameProjectsTask();
+ break;
+ }
+
+ if (!project_task) {
+ invalidade_leftover();
+ return;
+ }
+
+ connect(project_task.get(), &Task::finished, this, [=] {
+ invalidade_leftover();
+ project_task->deleteLater();
+ m_current_task = nullptr;
+ });
+
+ m_current_task = project_task.get();
+ project_task->start();
+ });
+
+ connect(version_task.get(), &Task::finished, [=] {
+ version_task->deleteLater();
+ m_current_task = nullptr;
+ });
+
+ if (m_mods.size() > 1)
+ setStatus(tr("Requesting metadata information from %1...").arg(ProviderCaps.readableName(m_provider)));
+ else if (!m_mods.empty())
+ setStatus(tr("Requesting metadata information from %1 for '%2'...")
+ .arg(ProviderCaps.readableName(m_provider), m_mods.begin().value()->name()));
+
+ m_current_task = version_task.get();
+ version_task->start();
+}
+
+void EnsureMetadataTask::emitReady(Mod* m)
+{
+ qDebug() << QString("Generated metadata for %1").arg(m->name());
+ emit metadataReady(m);
+
+ m_mods.remove(getHash(m));
+}
+
+void EnsureMetadataTask::emitFail(Mod* m)
+{
+ qDebug() << QString("Failed to generate metadata for %1").arg(m->name());
+ emit metadataFailed(m);
+
+ m_mods.remove(getHash(m));
+}
+
+// Modrinth
+
+NetJob::Ptr EnsureMetadataTask::modrinthVersionsTask()
+{
+ auto hash_type = ProviderCaps.hashType(ModPlatform::Provider::MODRINTH).first();
+
+ auto* response = new QByteArray();
+ auto ver_task = modrinth_api.currentVersions(m_mods.keys(), hash_type, response);
+
+ // Prevents unfortunate timings when aborting the task
+ if (!ver_task)
+ return {};
+
+ connect(ver_task.get(), &NetJob::succeeded, this, [this, response] {
+ QJsonParseError parse_error{};
+ QJsonDocument doc = QJsonDocument::fromJson(*response, &parse_error);
+ if (parse_error.error != QJsonParseError::NoError) {
+ qWarning() << "Error while parsing JSON response from Modrinth::CurrentVersions at " << parse_error.offset
+ << " reason: " << parse_error.errorString();
+ qWarning() << *response;
+
+ failed(parse_error.errorString());
+ return;
+ }
+
+ try {
+ auto entries = Json::requireObject(doc);
+ for (auto& hash : m_mods.keys()) {
+ auto mod = m_mods.find(hash).value();
+ try {
+ auto entry = Json::requireObject(entries, hash);
+
+ setStatus(tr("Parsing API response from Modrinth for '%1'...").arg(mod->name()));
+ qDebug() << "Getting version for" << mod->name() << "from Modrinth";
+
+ m_temp_versions.insert(hash, Modrinth::loadIndexedPackVersion(entry));
+ } catch (Json::JsonException& e) {
+ qDebug() << e.cause();
+ qDebug() << entries;
+
+ emitFail(mod);
+ }
+ }
+ } catch (Json::JsonException& e) {
+ qDebug() << e.cause();
+ qDebug() << doc;
+ }
+ });
+
+ return ver_task;
+}
+
+NetJob::Ptr EnsureMetadataTask::modrinthProjectsTask()
+{
+ QHash<QString, QString> addonIds;
+ for (auto const& data : m_temp_versions)
+ addonIds.insert(data.addonId.toString(), data.hash);
+
+ auto response = new QByteArray();
+ NetJob::Ptr proj_task;
+
+ if (addonIds.isEmpty()) {
+ qWarning() << "No addonId found!";
+ } else if (addonIds.size() == 1) {
+ proj_task = modrinth_api.getProject(*addonIds.keyBegin(), response);
+ } else {
+ proj_task = modrinth_api.getProjects(addonIds.keys(), response);
+ }
+
+ // Prevents unfortunate timings when aborting the task
+ if (!proj_task)
+ return {};
+
+ connect(proj_task.get(), &NetJob::succeeded, this, [this, response, addonIds] {
+ QJsonParseError parse_error{};
+ auto doc = QJsonDocument::fromJson(*response, &parse_error);
+ if (parse_error.error != QJsonParseError::NoError) {
+ qWarning() << "Error while parsing JSON response from Modrinth projects task at " << parse_error.offset
+ << " reason: " << parse_error.errorString();
+ qWarning() << *response;
+ return;
+ }
+
+ try {
+ QJsonArray entries;
+ if (addonIds.size() == 1)
+ entries = { doc.object() };
+ else
+ entries = Json::requireArray(doc);
+
+ for (auto entry : entries) {
+ auto entry_obj = Json::requireObject(entry);
+
+ ModPlatform::IndexedPack pack;
+ Modrinth::loadIndexedPack(pack, entry_obj);
+
+ auto hash = addonIds.find(pack.addonId.toString()).value();
+
+ auto mod_iter = m_mods.find(hash);
+ if (mod_iter == m_mods.end()) {
+ qWarning() << "Invalid project id from the API response.";
+ continue;
+ }
+
+ auto* mod = mod_iter.value();
+
+ try {
+ setStatus(tr("Parsing API response from Modrinth for '%1'...").arg(mod->name()));
+
+ modrinthCallback(pack, m_temp_versions.find(hash).value(), mod);
+ } catch (Json::JsonException& e) {
+ qDebug() << e.cause();
+ qDebug() << entries;
+
+ emitFail(mod);
+ }
+ }
+ } catch (Json::JsonException& e) {
+ qDebug() << e.cause();
+ qDebug() << doc;
+ }
+ });
+
+ return proj_task;
+}
+
+// Flame
+NetJob::Ptr EnsureMetadataTask::flameVersionsTask()
+{
+ auto* response = new QByteArray();
+
+ QList<uint> fingerprints;
+ for (auto& murmur : m_mods.keys()) {
+ fingerprints.push_back(murmur.toUInt());
+ }
+
+ auto ver_task = flame_api.matchFingerprints(fingerprints, response);
+
+ connect(ver_task.get(), &Task::succeeded, this, [this, response] {
+ QJsonParseError parse_error{};
+ QJsonDocument doc = QJsonDocument::fromJson(*response, &parse_error);
+ if (parse_error.error != QJsonParseError::NoError) {
+ qWarning() << "Error while parsing JSON response from Modrinth::CurrentVersions at " << parse_error.offset
+ << " reason: " << parse_error.errorString();
+ qWarning() << *response;
+
+ failed(parse_error.errorString());
+ return;
+ }
+
+ try {
+ auto doc_obj = Json::requireObject(doc);
+ auto data_obj = Json::requireObject(doc_obj, "data");
+ auto data_arr = Json::requireArray(data_obj, "exactMatches");
+
+ if (data_arr.isEmpty()) {
+ qWarning() << "No matches found for fingerprint search!";
+
+ return;
+ }
+
+ for (auto match : data_arr) {
+ auto match_obj = Json::ensureObject(match, {});
+ auto file_obj = Json::ensureObject(match_obj, "file", {});
+
+ if (match_obj.isEmpty() || file_obj.isEmpty()) {
+ qWarning() << "Fingerprint match is empty!";
+
+ return;
+ }
+
+ auto fingerprint = QString::number(Json::ensureVariant(file_obj, "fileFingerprint").toUInt());
+ auto mod = m_mods.find(fingerprint);
+ if (mod == m_mods.end()) {
+ qWarning() << "Invalid fingerprint from the API response.";
+ continue;
+ }
+
+ setStatus(tr("Parsing API response from CurseForge for '%1'...").arg((*mod)->name()));
+
+ m_temp_versions.insert(fingerprint, FlameMod::loadIndexedPackVersion(file_obj));
+ }
+
+ } catch (Json::JsonException& e) {
+ qDebug() << e.cause();
+ qDebug() << doc;
+ }
+ });
+
+ return ver_task;
+}
+
+NetJob::Ptr EnsureMetadataTask::flameProjectsTask()
+{
+ QHash<QString, QString> addonIds;
+ for (auto const& hash : m_mods.keys()) {
+ if (m_temp_versions.contains(hash)) {
+ auto const& data = m_temp_versions.find(hash).value();
+
+ auto id_str = data.addonId.toString();
+ if (!id_str.isEmpty())
+ addonIds.insert(data.addonId.toString(), hash);
+ }
+ }
+
+ auto response = new QByteArray();
+ NetJob::Ptr proj_task;
+
+ if (addonIds.isEmpty()) {
+ qWarning() << "No addonId found!";
+ } else if (addonIds.size() == 1) {
+ proj_task = flame_api.getProject(*addonIds.keyBegin(), response);
+ } else {
+ proj_task = flame_api.getProjects(addonIds.keys(), response);
+ }
+
+ // Prevents unfortunate timings when aborting the task
+ if (!proj_task)
+ return {};
+
+ connect(proj_task.get(), &NetJob::succeeded, this, [this, response, addonIds] {
+ QJsonParseError parse_error{};
+ auto doc = QJsonDocument::fromJson(*response, &parse_error);
+ if (parse_error.error != QJsonParseError::NoError) {
+ qWarning() << "Error while parsing JSON response from Modrinth projects task at " << parse_error.offset
+ << " reason: " << parse_error.errorString();
+ qWarning() << *response;
+ return;
+ }
+
+ try {
+ QJsonArray entries;
+ if (addonIds.size() == 1)
+ entries = { Json::requireObject(Json::requireObject(doc), "data") };
+ else
+ entries = Json::requireArray(Json::requireObject(doc), "data");
+
+ for (auto entry : entries) {
+ auto entry_obj = Json::requireObject(entry);
+
+ auto id = QString::number(Json::requireInteger(entry_obj, "id"));
+ auto hash = addonIds.find(id).value();
+ auto mod = m_mods.find(hash).value();
+
+ try {
+ setStatus(tr("Parsing API response from CurseForge for '%1'...").arg(mod->name()));
+
+ ModPlatform::IndexedPack pack;
+ FlameMod::loadIndexedPack(pack, entry_obj);
+
+ flameCallback(pack, m_temp_versions.find(hash).value(), mod);
+ } catch (Json::JsonException& e) {
+ qDebug() << e.cause();
+ qDebug() << entries;
+
+ emitFail(mod);
+ }
+ }
+ } catch (Json::JsonException& e) {
+ qDebug() << e.cause();
+ qDebug() << doc;
+ }
+ });
+
+ return proj_task;
+}
+
+void EnsureMetadataTask::modrinthCallback(ModPlatform::IndexedPack& pack, ModPlatform::IndexedVersion& ver, Mod* mod)
+{
+ // Prevent file name mismatch
+ ver.fileName = mod->fileinfo().fileName();
+ if (ver.fileName.endsWith(".disabled"))
+ ver.fileName.chop(9);
+
+ QDir tmp_index_dir(m_index_dir);
+
+ {
+ LocalModUpdateTask update_metadata(m_index_dir, pack, ver);
+ QEventLoop loop;
+
+ QObject::connect(&update_metadata, &Task::finished, &loop, &QEventLoop::quit);
+
+ update_metadata.start();
+
+ if (!update_metadata.isFinished())
+ loop.exec();
+ }
+
+ auto metadata = Metadata::get(tmp_index_dir, pack.slug);
+ if (!metadata.isValid()) {
+ qCritical() << "Failed to generate metadata at last step!";
+ emitFail(mod);
+ return;
+ }
+
+ mod->setMetadata(metadata);
+
+ emitReady(mod);
+}
+
+void EnsureMetadataTask::flameCallback(ModPlatform::IndexedPack& pack, ModPlatform::IndexedVersion& ver, Mod* mod)
+{
+ try {
+ // Prevent file name mismatch
+ ver.fileName = mod->fileinfo().fileName();
+ if (ver.fileName.endsWith(".disabled"))
+ ver.fileName.chop(9);
+
+ QDir tmp_index_dir(m_index_dir);
+
+ {
+ LocalModUpdateTask update_metadata(m_index_dir, pack, ver);
+ QEventLoop loop;
+
+ QObject::connect(&update_metadata, &Task::finished, &loop, &QEventLoop::quit);
+
+ update_metadata.start();
+
+ if (!update_metadata.isFinished())
+ loop.exec();
+ }
+
+ auto metadata = Metadata::get(tmp_index_dir, pack.slug);
+ if (!metadata.isValid()) {
+ qCritical() << "Failed to generate metadata at last step!";
+ emitFail(mod);
+ return;
+ }
+
+ mod->setMetadata(metadata);
+
+ emitReady(mod);
+ } catch (Json::JsonException& e) {
+ qDebug() << e.cause();
+
+ emitFail(mod);
+ }
+}
diff --git a/launcher/modplatform/EnsureMetadataTask.h b/launcher/modplatform/EnsureMetadataTask.h
new file mode 100644
index 00000000..79db6976
--- /dev/null
+++ b/launcher/modplatform/EnsureMetadataTask.h
@@ -0,0 +1,54 @@
+#pragma once
+
+#include "ModIndex.h"
+#include "tasks/SequentialTask.h"
+#include "net/NetJob.h"
+
+class Mod;
+class QDir;
+class MultipleOptionsTask;
+
+class EnsureMetadataTask : public Task {
+ Q_OBJECT
+
+ public:
+ EnsureMetadataTask(Mod*, QDir, ModPlatform::Provider = ModPlatform::Provider::MODRINTH);
+ EnsureMetadataTask(QList<Mod*>&, QDir, ModPlatform::Provider = ModPlatform::Provider::MODRINTH);
+
+ ~EnsureMetadataTask() = default;
+
+ public slots:
+ bool abort() override;
+ protected slots:
+ void executeTask() override;
+
+ private:
+ // FIXME: Move to their own namespace
+ auto modrinthVersionsTask() -> NetJob::Ptr;
+ auto modrinthProjectsTask() -> NetJob::Ptr;
+
+ auto flameVersionsTask() -> NetJob::Ptr;
+ auto flameProjectsTask() -> NetJob::Ptr;
+
+ // Helpers
+ void emitReady(Mod*);
+ void emitFail(Mod*);
+
+ auto getHash(Mod*) -> QString;
+
+ private slots:
+ void modrinthCallback(ModPlatform::IndexedPack& pack, ModPlatform::IndexedVersion& ver, Mod*);
+ void flameCallback(ModPlatform::IndexedPack& pack, ModPlatform::IndexedVersion& ver, Mod*);
+
+ signals:
+ void metadataReady(Mod*);
+ void metadataFailed(Mod*);
+
+ private:
+ QHash<QString, Mod*> m_mods;
+ QDir m_index_dir;
+ ModPlatform::Provider m_provider;
+
+ QHash<QString, ModPlatform::IndexedVersion> m_temp_versions;
+ NetJob* m_current_task;
+};
diff --git a/launcher/modplatform/ModAPI.h b/launcher/modplatform/ModAPI.h
index cf116353..4114d83c 100644
--- a/launcher/modplatform/ModAPI.h
+++ b/launcher/modplatform/ModAPI.h
@@ -40,6 +40,7 @@
#include <list>
#include "Version.h"
+#include "net/NetJob.h"
namespace ModPlatform {
class ListModel;
@@ -74,6 +75,9 @@ class ModAPI {
virtual void searchMods(CallerType* caller, SearchArgs&& args) const = 0;
virtual void getModInfo(CallerType* caller, ModPlatform::IndexedPack& pack) = 0;
+ virtual auto getProject(QString addonId, QByteArray* response) const -> NetJob* = 0;
+ virtual auto getProjects(QStringList addonIds, QByteArray* response) const -> NetJob* = 0;
+
struct VersionSearchArgs {
QString addonId;
diff --git a/launcher/modplatform/ModIndex.h b/launcher/modplatform/ModIndex.h
index c27643af..dc297d03 100644
--- a/launcher/modplatform/ModIndex.h
+++ b/launcher/modplatform/ModIndex.h
@@ -54,13 +54,16 @@ struct IndexedVersion {
QVariant addonId;
QVariant fileId;
QString version;
- QVector<QString> mcVersion;
+ QString version_number = {};
+ QStringList mcVersion;
QString downloadUrl;
QString date;
QString fileName;
- QVector<QString> loaders = {};
+ QStringList loaders = {};
QString hash_type;
QString hash;
+ bool is_preferred = true;
+ QString changelog;
};
struct ExtraPackData {
@@ -76,6 +79,7 @@ struct IndexedPack {
QVariant addonId;
Provider provider;
QString name;
+ QString slug;
QString description;
QList<ModpackAuthor> authors;
QString logoName;
diff --git a/launcher/modplatform/atlauncher/ATLPackInstallTask.h b/launcher/modplatform/atlauncher/ATLPackInstallTask.h
index f55873e9..992ba9c5 100644
--- a/launcher/modplatform/atlauncher/ATLPackInstallTask.h
+++ b/launcher/modplatform/atlauncher/ATLPackInstallTask.h
@@ -46,7 +46,7 @@
#include "minecraft/PackProfile.h"
#include "meta/Version.h"
-#include <nonstd/optional>
+#include <optional>
namespace ATLauncher {
@@ -131,8 +131,8 @@ private:
Meta::VersionPtr minecraftVersion;
QMap<QString, Meta::VersionPtr> componentsToInstall;
- QFuture<nonstd::optional<QStringList>> m_extractFuture;
- QFutureWatcher<nonstd::optional<QStringList>> m_extractFutureWatcher;
+ QFuture<std::optional<QStringList>> m_extractFuture;
+ QFutureWatcher<std::optional<QStringList>> m_extractFutureWatcher;
QFuture<bool> m_modExtractFuture;
QFutureWatcher<bool> m_modExtractFutureWatcher;
diff --git a/launcher/modplatform/flame/FileResolvingTask.cpp b/launcher/modplatform/flame/FileResolvingTask.cpp
index c1f56658..058d2471 100644
--- a/launcher/modplatform/flame/FileResolvingTask.cpp
+++ b/launcher/modplatform/flame/FileResolvingTask.cpp
@@ -7,6 +7,13 @@ Flame::FileResolvingTask::FileResolvingTask(const shared_qobject_ptr<QNetworkAcc
: m_network(network), m_toProcess(toProcess)
{}
+bool Flame::FileResolvingTask::abort()
+{
+ if (m_dljob)
+ return m_dljob->abort();
+ return true;
+}
+
void Flame::FileResolvingTask::executeTask()
{
setStatus(tr("Resolving mod IDs..."));
diff --git a/launcher/modplatform/flame/FileResolvingTask.h b/launcher/modplatform/flame/FileResolvingTask.h
index 87981f0a..f71b87ce 100644
--- a/launcher/modplatform/flame/FileResolvingTask.h
+++ b/launcher/modplatform/flame/FileResolvingTask.h
@@ -13,6 +13,9 @@ public:
explicit FileResolvingTask(const shared_qobject_ptr<QNetworkAccessManager>& network, Flame::Manifest &toProcess);
virtual ~FileResolvingTask() {};
+ bool canAbort() const override { return true; }
+ bool abort() override;
+
const Flame::Manifest &getResults() const
{
return m_toProcess;
diff --git a/launcher/modplatform/flame/FlameAPI.cpp b/launcher/modplatform/flame/FlameAPI.cpp
new file mode 100644
index 00000000..0ff04f72
--- /dev/null
+++ b/launcher/modplatform/flame/FlameAPI.cpp
@@ -0,0 +1,148 @@
+#include "FlameAPI.h"
+#include "FlameModIndex.h"
+
+#include "Application.h"
+#include "BuildConfig.h"
+#include "Json.h"
+
+#include "net/Upload.h"
+
+auto FlameAPI::matchFingerprints(const QList<uint>& fingerprints, QByteArray* response) -> NetJob::Ptr
+{
+ auto* netJob = new NetJob(QString("Flame::MatchFingerprints"), APPLICATION->network());
+
+ QJsonObject body_obj;
+ QJsonArray fingerprints_arr;
+ for (auto& fp : fingerprints) {
+ fingerprints_arr.append(QString("%1").arg(fp));
+ }
+
+ body_obj["fingerprints"] = fingerprints_arr;
+
+ QJsonDocument body(body_obj);
+ auto body_raw = body.toJson();
+
+ netJob->addNetAction(Net::Upload::makeByteArray(QString("https://api.curseforge.com/v1/fingerprints"), response, body_raw));
+
+ QObject::connect(netJob, &NetJob::finished, [response] { delete response; });
+
+ return netJob;
+}
+
+auto FlameAPI::getModFileChangelog(int modId, int fileId) -> QString
+{
+ QEventLoop lock;
+ QString changelog;
+
+ auto* netJob = new NetJob(QString("Flame::FileChangelog"), APPLICATION->network());
+ auto* response = new QByteArray();
+ netJob->addNetAction(Net::Download::makeByteArray(
+ QString("https://api.curseforge.com/v1/mods/%1/files/%2/changelog")
+ .arg(QString::fromStdString(std::to_string(modId)), QString::fromStdString(std::to_string(fileId))),
+ response));
+
+ QObject::connect(netJob, &NetJob::succeeded, [netJob, response, &changelog] {
+ QJsonParseError parse_error{};
+ QJsonDocument doc = QJsonDocument::fromJson(*response, &parse_error);
+ if (parse_error.error != QJsonParseError::NoError) {
+ qWarning() << "Error while parsing JSON response from Flame::FileChangelog at " << parse_error.offset
+ << " reason: " << parse_error.errorString();
+ qWarning() << *response;
+
+ netJob->failed(parse_error.errorString());
+ return;
+ }
+
+ changelog = Json::ensureString(doc.object(), "data");
+ });
+
+ QObject::connect(netJob, &NetJob::finished, [response, &lock] {
+ delete response;
+ lock.quit();
+ });
+
+ netJob->start();
+ lock.exec();
+
+ return changelog;
+}
+
+auto FlameAPI::getLatestVersion(VersionSearchArgs&& args) -> ModPlatform::IndexedVersion
+{
+ QEventLoop loop;
+
+ auto netJob = new NetJob(QString("Flame::GetLatestVersion(%1)").arg(args.addonId), APPLICATION->network());
+ auto response = new QByteArray();
+ ModPlatform::IndexedVersion ver;
+
+ netJob->addNetAction(Net::Download::makeByteArray(getVersionsURL(args), response));
+
+ QObject::connect(netJob, &NetJob::succeeded, [response, args, &ver] {
+ QJsonParseError parse_error{};
+ QJsonDocument doc = QJsonDocument::fromJson(*response, &parse_error);
+ if (parse_error.error != QJsonParseError::NoError) {
+ qWarning() << "Error while parsing JSON response from latest mod version at " << parse_error.offset
+ << " reason: " << parse_error.errorString();
+ qWarning() << *response;
+ return;
+ }
+
+ try {
+ auto obj = Json::requireObject(doc);
+ auto arr = Json::requireArray(obj, "data");
+
+ QJsonObject latest_file_obj;
+ ModPlatform::IndexedVersion ver_tmp;
+
+ for (auto file : arr) {
+ auto file_obj = Json::requireObject(file);
+ auto file_tmp = FlameMod::loadIndexedPackVersion(file_obj);
+ if(file_tmp.date > ver_tmp.date) {
+ ver_tmp = file_tmp;
+ latest_file_obj = file_obj;
+ }
+ }
+
+ ver = FlameMod::loadIndexedPackVersion(latest_file_obj);
+ } catch (Json::JsonException& e) {
+ qCritical() << "Failed to parse response from a version request.";
+ qCritical() << e.what();
+ qDebug() << doc;
+ }
+ });
+
+ QObject::connect(netJob, &NetJob::finished, [response, netJob, &loop] {
+ netJob->deleteLater();
+ delete response;
+ loop.quit();
+ });
+
+ netJob->start();
+
+ loop.exec();
+
+ return ver;
+}
+
+auto FlameAPI::getProjects(QStringList addonIds, QByteArray* response) const -> NetJob*
+{
+ auto* netJob = new NetJob(QString("Flame::GetProjects"), APPLICATION->network());
+
+ QJsonObject body_obj;
+ QJsonArray addons_arr;
+ for (auto& addonId : addonIds) {
+ addons_arr.append(addonId);
+ }
+
+ body_obj["modIds"] = addons_arr;
+
+ QJsonDocument body(body_obj);
+ auto body_raw = body.toJson();
+
+ netJob->addNetAction(Net::Upload::makeByteArray(QString("https://api.curseforge.com/v1/mods"), response, body_raw));
+
+ QObject::connect(netJob, &NetJob::finished, [response, netJob] { delete response; netJob->deleteLater(); });
+ QObject::connect(netJob, &NetJob::failed, [body_raw] { qDebug() << body_raw; });
+
+ return netJob;
+}
diff --git a/launcher/modplatform/flame/FlameAPI.h b/launcher/modplatform/flame/FlameAPI.h
index aea76ff1..336df387 100644
--- a/launcher/modplatform/flame/FlameAPI.h
+++ b/launcher/modplatform/flame/FlameAPI.h
@@ -4,6 +4,14 @@
#include "modplatform/helpers/NetworkModAPI.h"
class FlameAPI : public NetworkModAPI {
+ public:
+ auto matchFingerprints(const QList<uint>& fingerprints, QByteArray* response) -> NetJob::Ptr;
+ auto getModFileChangelog(int modId, int fileId) -> QString;
+
+ auto getLatestVersion(VersionSearchArgs&& args) -> ModPlatform::IndexedVersion;
+
+ auto getProjects(QStringList addonIds, QByteArray* response) const -> NetJob* override;
+
private:
inline auto getSortFieldInt(QString sortString) const -> int
{
diff --git a/launcher/modplatform/flame/FlameCheckUpdate.cpp b/launcher/modplatform/flame/FlameCheckUpdate.cpp
new file mode 100644
index 00000000..8dd3a846
--- /dev/null
+++ b/launcher/modplatform/flame/FlameCheckUpdate.cpp
@@ -0,0 +1,179 @@
+#include "FlameCheckUpdate.h"
+#include "FlameAPI.h"
+#include "FlameModIndex.h"
+
+#include <MurmurHash2.h>
+
+#include "FileSystem.h"
+#include "Json.h"
+
+#include "ModDownloadTask.h"
+
+static FlameAPI api;
+
+bool FlameCheckUpdate::abort()
+{
+ m_was_aborted = true;
+ if (m_net_job)
+ return m_net_job->abort();
+ return true;
+}
+
+ModPlatform::IndexedPack getProjectInfo(ModPlatform::IndexedVersion& ver_info)
+{
+ ModPlatform::IndexedPack pack;
+
+ QEventLoop loop;
+
+ auto get_project_job = new NetJob("Flame::GetProjectJob", APPLICATION->network());
+
+ auto response = new QByteArray();
+ auto url = QString("https://api.curseforge.com/v1/mods/%1").arg(ver_info.addonId.toString());
+ auto dl = Net::Download::makeByteArray(url, response);
+ get_project_job->addNetAction(dl);
+
+ QObject::connect(get_project_job, &NetJob::succeeded, [response, &pack]() {
+ QJsonParseError parse_error{};
+ QJsonDocument doc = QJsonDocument::fromJson(*response, &parse_error);
+ if (parse_error.error != QJsonParseError::NoError) {
+ qWarning() << "Error while parsing JSON response from FlameCheckUpdate at " << parse_error.offset
+ << " reason: " << parse_error.errorString();
+ qWarning() << *response;
+ return;
+ }
+
+ try {
+ auto doc_obj = Json::requireObject(doc);
+ auto data_obj = Json::requireObject(doc_obj, "data");
+ FlameMod::loadIndexedPack(pack, data_obj);
+ } catch (Json::JsonException& e) {
+ qWarning() << e.cause();
+ qDebug() << doc;
+ }
+ });
+
+ QObject::connect(get_project_job, &NetJob::finished, [&loop, get_project_job] {
+ get_project_job->deleteLater();
+ loop.quit();
+ });
+
+ get_project_job->start();
+ loop.exec();
+
+ return pack;
+}
+
+ModPlatform::IndexedVersion getFileInfo(int addonId, int fileId)
+{
+ ModPlatform::IndexedVersion ver;
+
+ QEventLoop loop;
+
+ auto get_file_info_job = new NetJob("Flame::GetFileInfoJob", APPLICATION->network());
+
+ auto response = new QByteArray();
+ auto url = QString("https://api.curseforge.com/v1/mods/%1/files/%2").arg(QString::number(addonId), QString::number(fileId));
+ auto dl = Net::Download::makeByteArray(url, response);
+ get_file_info_job->addNetAction(dl);
+
+ QObject::connect(get_file_info_job, &NetJob::succeeded, [response, &ver]() {
+ QJsonParseError parse_error{};
+ QJsonDocument doc = QJsonDocument::fromJson(*response, &parse_error);
+ if (parse_error.error != QJsonParseError::NoError) {
+ qWarning() << "Error while parsing JSON response from FlameCheckUpdate at " << parse_error.offset
+ << " reason: " << parse_error.errorString();
+ qWarning() << *response;
+ return;
+ }
+
+ try {
+ auto doc_obj = Json::requireObject(doc);
+ auto data_obj = Json::requireObject(doc_obj, "data");
+ ver = FlameMod::loadIndexedPackVersion(data_obj);
+ } catch (Json::JsonException& e) {
+ qWarning() << e.cause();
+ qDebug() << doc;
+ }
+ });
+
+ QObject::connect(get_file_info_job, &NetJob::finished, [&loop, get_file_info_job] {
+ get_file_info_job->deleteLater();
+ loop.quit();
+ });
+
+ get_file_info_job->start();
+ loop.exec();
+
+ return ver;
+}
+
+/* Check for update:
+ * - Get latest version available
+ * - Compare hash of the latest version with the current hash
+ * - If equal, no updates, else, there's updates, so add to the list
+ * */
+void FlameCheckUpdate::executeTask()
+{
+ setStatus(tr("Preparing mods for CurseForge..."));
+
+ int i = 0;
+ for (auto* mod : m_mods) {
+ if (!mod->enabled()) {
+ emit checkFailed(mod, tr("Disabled mods won't be updated, to prevent mod duplication issues!"));
+ continue;
+ }
+
+ setStatus(tr("Getting API response from CurseForge for '%1'...").arg(mod->name()));
+ setProgress(i++, m_mods.size());
+
+ auto latest_ver = api.getLatestVersion({ mod->metadata()->project_id.toString(), m_game_versions, m_loaders });
+
+ // Check if we were aborted while getting the latest version
+ if (m_was_aborted) {
+ aborted();
+ return;
+ }
+
+ setStatus(tr("Parsing the API response from CurseForge for '%1'...").arg(mod->name()));
+
+ if (!latest_ver.addonId.isValid()) {
+ emit checkFailed(mod, tr("No valid version found for this mod. It's probably unavailable for the current game "
+ "version / mod loader."));
+ continue;
+ }
+
+ if (latest_ver.downloadUrl.isEmpty() && latest_ver.fileId != mod->metadata()->file_id) {
+ auto pack = getProjectInfo(latest_ver);
+ auto recover_url = QString("%1/download/%2").arg(pack.websiteUrl, latest_ver.fileId.toString());
+ emit checkFailed(mod, tr("Mod has a new update available, but is not downloadable using CurseForge."), recover_url);
+
+ continue;
+ }
+
+ if (!latest_ver.hash.isEmpty() && (mod->metadata()->hash != latest_ver.hash || mod->status() == ModStatus::NotInstalled)) {
+ // Fake pack with the necessary info to pass to the download task :)
+ ModPlatform::IndexedPack pack;
+ pack.name = mod->name();
+ pack.slug = mod->metadata()->slug;
+ pack.addonId = mod->metadata()->project_id;
+ pack.websiteUrl = mod->homeurl();
+ for (auto& author : mod->authors())
+ pack.authors.append({ author });
+ pack.description = mod->description();
+ pack.provider = ModPlatform::Provider::FLAME;
+
+ auto old_version = mod->version();
+ if (old_version.isEmpty() && mod->status() != ModStatus::NotInstalled) {
+ auto current_ver = getFileInfo(latest_ver.addonId.toInt(), mod->metadata()->file_id.toInt());
+ old_version = current_ver.version;
+ }
+
+ auto download_task = new ModDownloadTask(pack, latest_ver, m_mods_folder);
+ m_updatable.emplace_back(pack.name, mod->metadata()->hash, old_version, latest_ver.version,
+ api.getModFileChangelog(latest_ver.addonId.toInt(), latest_ver.fileId.toInt()),
+ ModPlatform::Provider::FLAME, download_task);
+ }
+ }
+
+ emitSucceeded();
+}
diff --git a/launcher/modplatform/flame/FlameCheckUpdate.h b/launcher/modplatform/flame/FlameCheckUpdate.h
new file mode 100644
index 00000000..163c706c
--- /dev/null
+++ b/launcher/modplatform/flame/FlameCheckUpdate.h
@@ -0,0 +1,25 @@
+#pragma once
+
+#include "Application.h"
+#include "modplatform/CheckUpdateTask.h"
+#include "net/NetJob.h"
+
+class FlameCheckUpdate : public CheckUpdateTask {
+ Q_OBJECT
+
+ public:
+ FlameCheckUpdate(QList<Mod*>& mods, std::list<Version>& mcVersions, ModAPI::ModLoaderTypes loaders, std::shared_ptr<ModFolderModel> mods_folder)
+ : CheckUpdateTask(mods, mcVersions, loaders, mods_folder)
+ {}
+
+ public slots:
+ bool abort() override;
+
+ protected slots:
+ void executeTask() override;
+
+ private:
+ NetJob* m_net_job = nullptr;
+
+ bool m_was_aborted = false;
+};
diff --git a/launcher/modplatform/flame/FlameModIndex.cpp b/launcher/modplatform/flame/FlameModIndex.cpp
index b99bfb26..746018e2 100644
--- a/launcher/modplatform/flame/FlameModIndex.cpp
+++ b/launcher/modplatform/flame/FlameModIndex.cpp
@@ -7,20 +7,22 @@
#include "net/NetJob.h"
static ModPlatform::ProviderCapabilities ProviderCaps;
+static FlameAPI api;
void FlameMod::loadIndexedPack(ModPlatform::IndexedPack& pack, QJsonObject& obj)
{
pack.addonId = Json::requireInteger(obj, "id");
pack.provider = ModPlatform::Provider::FLAME;
pack.name = Json::requireString(obj, "name");
+ pack.slug = Json::requireString(obj, "slug");
pack.websiteUrl = Json::ensureString(Json::ensureObject(obj, "links"), "websiteUrl", "");
pack.description = Json::ensureString(obj, "summary", "");
- QJsonObject logo = Json::requireObject(obj, "logo");
- pack.logoName = Json::requireString(logo, "title");
- pack.logoUrl = Json::requireString(logo, "thumbnailUrl");
+ QJsonObject logo = Json::ensureObject(obj, "logo");
+ pack.logoName = Json::ensureString(logo, "title");
+ pack.logoUrl = Json::ensureString(logo, "thumbnailUrl");
- auto authors = Json::requireArray(obj, "authors");
+ auto authors = Json::ensureArray(obj, "authors");
for (auto authorIter : authors) {
auto author = Json::requireObject(authorIter);
ModPlatform::ModpackAuthor packAuthor;
@@ -91,7 +93,7 @@ void FlameMod::loadIndexedPackVersions(ModPlatform::IndexedPack& pack,
pack.versionsLoaded = true;
}
-auto FlameMod::loadIndexedPackVersion(QJsonObject& obj) -> ModPlatform::IndexedVersion
+auto FlameMod::loadIndexedPackVersion(QJsonObject& obj, bool load_changelog) -> ModPlatform::IndexedVersion
{
auto versionArray = Json::requireArray(obj, "gameVersions");
if (versionArray.isEmpty()) {
@@ -110,7 +112,7 @@ auto FlameMod::loadIndexedPackVersion(QJsonObject& obj) -> ModPlatform::IndexedV
file.fileId = Json::requireInteger(obj, "id");
file.date = Json::requireString(obj, "fileDate");
file.version = Json::requireString(obj, "displayName");
- file.downloadUrl = Json::requireString(obj, "downloadUrl");
+ file.downloadUrl = Json::ensureString(obj, "downloadUrl");
file.fileName = Json::requireString(obj, "fileName");
auto hash_list = Json::ensureArray(obj, "hashes");
@@ -124,5 +126,9 @@ auto FlameMod::loadIndexedPackVersion(QJsonObject& obj) -> ModPlatform::IndexedV
break;
}
}
+
+ if(load_changelog)
+ file.changelog = api.getModFileChangelog(file.addonId.toInt(), file.fileId.toInt());
+
return file;
}
diff --git a/launcher/modplatform/flame/FlameModIndex.h b/launcher/modplatform/flame/FlameModIndex.h
index 9c6c1c6c..a839dd83 100644
--- a/launcher/modplatform/flame/FlameModIndex.h
+++ b/launcher/modplatform/flame/FlameModIndex.h
@@ -17,6 +17,6 @@ void loadIndexedPackVersions(ModPlatform::IndexedPack& pack,
QJsonArray& arr,
const shared_qobject_ptr<QNetworkAccessManager>& network,
BaseInstance* inst);
-auto loadIndexedPackVersion(QJsonObject& obj) -> ModPlatform::IndexedVersion;
+auto loadIndexedPackVersion(QJsonObject& obj, bool load_changelog = false) -> ModPlatform::IndexedVersion;
} // namespace FlameMod
diff --git a/launcher/modplatform/helpers/NetworkModAPI.cpp b/launcher/modplatform/helpers/NetworkModAPI.cpp
index d7abd10f..90edfe31 100644
--- a/launcher/modplatform/helpers/NetworkModAPI.cpp
+++ b/launcher/modplatform/helpers/NetworkModAPI.cpp
@@ -33,18 +33,14 @@ void NetworkModAPI::searchMods(CallerType* caller, SearchArgs&& args) const
void NetworkModAPI::getModInfo(CallerType* caller, ModPlatform::IndexedPack& pack)
{
- auto id_str = pack.addonId.toString();
- auto netJob = new NetJob(QString("%1::ModInfo").arg(id_str), APPLICATION->network());
- auto searchUrl = getModInfoURL(id_str);
-
auto response = new QByteArray();
- netJob->addNetAction(Net::Download::makeByteArray(QUrl(searchUrl), response));
+ auto job = getProject(pack.addonId.toString(), response);
- QObject::connect(netJob, &NetJob::succeeded, [response, &pack, caller] {
+ QObject::connect(job, &NetJob::succeeded, caller, [caller, &pack, response] {
QJsonParseError parse_error{};
- auto doc = QJsonDocument::fromJson(*response, &parse_error);
+ QJsonDocument doc = QJsonDocument::fromJson(*response, &parse_error);
if (parse_error.error != QJsonParseError::NoError) {
- qWarning() << "Error while parsing JSON response for " << pack.name << " at " << parse_error.offset
+ qWarning() << "Error while parsing JSON response from " << caller->debugName() << " at " << parse_error.offset
<< " reason: " << parse_error.errorString();
qWarning() << *response;
return;
@@ -53,7 +49,7 @@ void NetworkModAPI::getModInfo(CallerType* caller, ModPlatform::IndexedPack& pac
caller->infoRequestFinished(doc, pack);
});
- netJob->start();
+ job->start();
}
void NetworkModAPI::getVersions(CallerType* caller, VersionSearchArgs&& args) const
@@ -83,3 +79,18 @@ void NetworkModAPI::getVersions(CallerType* caller, VersionSearchArgs&& args) co
netJob->start();
}
+
+auto NetworkModAPI::getProject(QString addonId, QByteArray* response) const -> NetJob*
+{
+ auto netJob = new NetJob(QString("%1::GetProject").arg(addonId), APPLICATION->network());
+ auto searchUrl = getModInfoURL(addonId);
+
+ netJob->addNetAction(Net::Download::makeByteArray(QUrl(searchUrl), response));
+
+ QObject::connect(netJob, &NetJob::finished, [response, netJob] {
+ netJob->deleteLater();
+ delete response;
+ });
+
+ return netJob;
+}
diff --git a/launcher/modplatform/helpers/NetworkModAPI.h b/launcher/modplatform/helpers/NetworkModAPI.h
index 87d77ad1..989bcec4 100644
--- a/launcher/modplatform/helpers/NetworkModAPI.h
+++ b/launcher/modplatform/helpers/NetworkModAPI.h
@@ -8,6 +8,8 @@ class NetworkModAPI : public ModAPI {
void getModInfo(CallerType* caller, ModPlatform::IndexedPack& pack) override;
void getVersions(CallerType* caller, VersionSearchArgs&& args) const override;
+ auto getProject(QString addonId, QByteArray* response) const -> NetJob* override;
+
protected:
virtual auto getModSearchURL(SearchArgs& args) const -> QString = 0;
virtual auto getModInfoURL(QString& id) const -> QString = 0;
diff --git a/launcher/modplatform/legacy_ftb/PackInstallTask.h b/launcher/modplatform/legacy_ftb/PackInstallTask.h
index a7395220..da4c0da5 100644
--- a/launcher/modplatform/legacy_ftb/PackInstallTask.h
+++ b/launcher/modplatform/legacy_ftb/PackInstallTask.h
@@ -10,7 +10,7 @@
#include "net/NetJob.h"
-#include <nonstd/optional>
+#include <optional>
namespace LegacyFTB {
@@ -46,8 +46,8 @@ private: /* data */
shared_qobject_ptr<QNetworkAccessManager> m_network;
bool abortable = false;
std::unique_ptr<QuaZip> m_packZip;
- QFuture<nonstd::optional<QStringList>> m_extractFuture;
- QFutureWatcher<nonstd::optional<QStringList>> m_extractFutureWatcher;
+ QFuture<std::optional<QStringList>> m_extractFuture;
+ QFutureWatcher<std::optional<QStringList>> m_extractFutureWatcher;
NetJob::Ptr netJobContainer;
QString archivePath;
diff --git a/launcher/modplatform/modpacksch/FTBPackInstallTask.cpp b/launcher/modplatform/modpacksch/FTBPackInstallTask.cpp
index cac432cd..16013070 100644
--- a/launcher/modplatform/modpacksch/FTBPackInstallTask.cpp
+++ b/launcher/modplatform/modpacksch/FTBPackInstallTask.cpp
@@ -1,7 +1,9 @@
// SPDX-License-Identifier: GPL-3.0-only
/*
* PolyMC - Minecraft Launcher
+ * Copyright (C) 2022 flowln <flowlnlnln@gmail.com>
* Copyright (c) 2022 Jamie Mansfield <jmansfield@cadixdev.org>
+ * Copyright (C) 2022 Sefa Eyeoglu <contact@scrumplex.net>
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
@@ -40,103 +42,190 @@
#include "Json.h"
#include "minecraft/MinecraftInstance.h"
#include "minecraft/PackProfile.h"
+#include "modplatform/flame/PackManifest.h"
#include "net/ChecksumValidator.h"
#include "settings/INISettingsObject.h"
-#include "BuildConfig.h"
#include "Application.h"
+#include "BuildConfig.h"
+#include "ui/dialogs/ScrollMessageBox.h"
namespace ModpacksCH {
-PackInstallTask::PackInstallTask(Modpack pack, QString version)
-{
- m_pack = pack;
- m_version_name = version;
-}
+PackInstallTask::PackInstallTask(Modpack pack, QString version, QWidget* parent)
+ : m_pack(std::move(pack)), m_version_name(std::move(version)), m_parent(parent)
+{}
bool PackInstallTask::abort()
{
- if(abortable)
- {
- return jobPtr->abort();
- }
- return false;
+ bool aborted = true;
+
+ if (m_net_job)
+ aborted &= m_net_job->abort();
+ if (m_mod_id_resolver_task)
+ aborted &= m_mod_id_resolver_task->abort();
+
+ // FIXME: This should be 'emitAborted()', but InstanceStaging doesn't connect to the abort signal yet...
+ if (aborted)
+ emitFailed(tr("Aborted"));
+
+ return aborted;
}
void PackInstallTask::executeTask()
{
- // Find pack version
- bool found = false;
- VersionInfo version;
+ setStatus(tr("Getting the manifest..."));
- for(auto vInfo : m_pack.versions) {
- if (vInfo.name == m_version_name) {
- found = true;
- version = vInfo;
- break;
- }
- }
+ // Find pack version
+ auto version_it = std::find_if(m_pack.versions.constBegin(), m_pack.versions.constEnd(),
+ [this](ModpacksCH::VersionInfo const& a) { return a.name == m_version_name; });
- if(!found) {
+ if (version_it == m_pack.versions.constEnd()) {
emitFailed(tr("Failed to find pack version %1").arg(m_version_name));
return;
}
- auto *netJob = new NetJob("ModpacksCH::VersionFetch", APPLICATION->network());
+ auto version = *version_it;
+
+ auto* netJob = new NetJob("ModpacksCH::VersionFetch", APPLICATION->network());
+
auto searchUrl = QString(BuildConfig.MODPACKSCH_API_BASE_URL + "public/modpack/%1/%2").arg(m_pack.id).arg(version.id);
- netJob->addNetAction(Net::Download::makeByteArray(QUrl(searchUrl), &response));
- jobPtr = netJob;
- jobPtr->start();
+ netJob->addNetAction(Net::Download::makeByteArray(QUrl(searchUrl), &m_response));
+
+ QObject::connect(netJob, &NetJob::succeeded, this, &PackInstallTask::onManifestDownloadSucceeded);
+ QObject::connect(netJob, &NetJob::failed, this, &PackInstallTask::onManifestDownloadFailed);
+ QObject::connect(netJob, &NetJob::progress, this, &PackInstallTask::setProgress);
- QObject::connect(netJob, &NetJob::succeeded, this, &PackInstallTask::onDownloadSucceeded);
- QObject::connect(netJob, &NetJob::failed, this, &PackInstallTask::onDownloadFailed);
+ m_net_job = netJob;
+
+ netJob->start();
}
-void PackInstallTask::onDownloadSucceeded()
+void PackInstallTask::onManifestDownloadSucceeded()
{
- jobPtr.reset();
-
- QJsonParseError parse_error;
- QJsonDocument doc = QJsonDocument::fromJson(response, &parse_error);
- if(parse_error.error != QJsonParseError::NoError) {
- qWarning() << "Error while parsing JSON response from ModpacksCH at " << parse_error.offset << " reason: " << parse_error.errorString();
- qWarning() << response;
+ m_net_job.reset();
+
+ QJsonParseError parse_error{};
+ QJsonDocument doc = QJsonDocument::fromJson(m_response, &parse_error);
+ if (parse_error.error != QJsonParseError::NoError) {
+ qWarning() << "Error while parsing JSON response from ModpacksCH at " << parse_error.offset
+ << " reason: " << parse_error.errorString();
+ qWarning() << m_response;
return;
}
- auto obj = doc.object();
-
ModpacksCH::Version version;
- try
- {
+ try {
+ auto obj = Json::requireObject(doc);
ModpacksCH::loadVersion(version, obj);
- }
- catch (const JSONValidationError &e)
- {
+ } catch (const JSONValidationError& e) {
emitFailed(tr("Could not understand pack manifest:\n") + e.cause());
return;
}
+
m_version = version;
- downloadPack();
+ resolveMods();
}
-void PackInstallTask::onDownloadFailed(QString reason)
+void PackInstallTask::resolveMods()
{
- jobPtr.reset();
- emitFailed(reason);
+ setStatus(tr("Resolving mods..."));
+ setProgress(0, 100);
+
+ m_file_id_map.clear();
+
+ Flame::Manifest manifest;
+ int index = 0;
+
+ for (auto const& file : m_version.files) {
+ if (!file.serverOnly && file.url.isEmpty()) {
+ if (file.curseforge.file_id <= 0) {
+ emitFailed(tr("Invalid manifest: There's no information available to download the file '%1'!").arg(file.name));
+ return;
+ }
+
+ Flame::File flame_file;
+ flame_file.projectId = file.curseforge.project_id;
+ flame_file.fileId = file.curseforge.file_id;
+ flame_file.hash = file.sha1;
+
+ manifest.files.insert(flame_file.fileId, flame_file);
+ m_file_id_map.append(flame_file.fileId);
+ } else {
+ m_file_id_map.append(-1);
+ }
+
+ index++;
+ }
+
+ m_mod_id_resolver_task = new Flame::FileResolvingTask(APPLICATION->network(), manifest);
+
+ connect(m_mod_id_resolver_task.get(), &Flame::FileResolvingTask::succeeded, this, &PackInstallTask::onResolveModsSucceeded);
+ connect(m_mod_id_resolver_task.get(), &Flame::FileResolvingTask::failed, this, &PackInstallTask::onResolveModsFailed);
+ connect(m_mod_id_resolver_task.get(), &Flame::FileResolvingTask::progress, this, &PackInstallTask::setProgress);
+
+ m_mod_id_resolver_task->start();
+}
+
+void PackInstallTask::onResolveModsSucceeded()
+{
+ m_abortable = false;
+
+ QString text;
+ auto anyBlocked = false;
+
+ Flame::Manifest results = m_mod_id_resolver_task->getResults();
+ for (int index = 0; index < m_file_id_map.size(); index++) {
+ auto const file_id = m_file_id_map.at(index);
+ if (file_id < 0)
+ continue;
+
+ Flame::File results_file = results.files[file_id];
+ VersionFile& local_file = m_version.files[index];
+
+ // First check for blocked mods
+ if (!results_file.resolved || results_file.url.isEmpty()) {
+ QString type(local_file.type);
+
+ type[0] = type[0].toUpper();
+ text += QString("%1: %2 - <a href='%3'>%3</a><br/>").arg(type, local_file.name, results_file.websiteUrl);
+ anyBlocked = true;
+ } else {
+ local_file.url = results_file.url.toString();
+ }
+ }
+
+ m_mod_id_resolver_task.reset();
+
+ if (anyBlocked) {
+ qDebug() << "Blocked files found, displaying file list";
+
+ auto message_dialog = new ScrollMessageBox(m_parent, tr("Blocked files found"),
+ tr("The following files are not available for download in third party launchers.<br/>"
+ "You will need to manually download them and add them to the instance."),
+ text);
+
+ if (message_dialog->exec() == QDialog::Accepted)
+ downloadPack();
+ else
+ abort();
+ } else {
+ downloadPack();
+ }
}
void PackInstallTask::downloadPack()
{
setStatus(tr("Downloading mods..."));
- jobPtr = new NetJob(tr("Mod download"), APPLICATION->network());
- for(auto file : m_version.files) {
- if(file.serverOnly) continue;
+ auto* jobPtr = new NetJob(tr("Mod download"), APPLICATION->network());
+ for (auto const& file : m_version.files) {
+ if (file.serverOnly || file.url.isEmpty())
+ continue;
- QFileInfo fileName(file.name);
- auto cacheName = fileName.completeBaseName() + "-" + file.sha1 + "." + fileName.suffix();
+ QFileInfo file_info(file.name);
+ auto cacheName = file_info.completeBaseName() + "-" + file.sha1 + "." + file_info.suffix();
auto entry = APPLICATION->metacache()->resolveEntry("ModpacksCHPacks", cacheName);
entry->setStale(true);
@@ -144,58 +233,64 @@ void PackInstallTask::downloadPack()
auto relpath = FS::PathCombine("minecraft", file.path, file.name);
auto path = FS::PathCombine(m_stagingPath, relpath);
- if (filesToCopy.contains(path)) {
+ if (m_files_to_copy.contains(path)) {
qWarning() << "Ignoring" << file.url << "as a file of that path is already downloading.";
continue;
}
+
qDebug() << "Will download" << file.url << "to" << path;
- filesToCopy[path] = entry->getFullPath();
+ m_files_to_copy[path] = entry->getFullPath();
auto dl = Net::Download::makeCached(file.url, entry);
if (!file.sha1.isEmpty()) {
auto rawSha1 = QByteArray::fromHex(file.sha1.toLatin1());
dl->addValidator(new Net::ChecksumValidator(QCryptographicHash::Sha1, rawSha1));
}
+
jobPtr->addNetAction(dl);
}
- connect(jobPtr.get(), &NetJob::succeeded, this, [&]()
- {
- abortable = false;
- jobPtr.reset();
- install();
- });
- connect(jobPtr.get(), &NetJob::failed, [&](QString reason)
- {
- abortable = false;
- jobPtr.reset();
- emitFailed(reason);
- });
- connect(jobPtr.get(), &NetJob::progress, [&](qint64 current, qint64 total)
- {
- abortable = true;
- setProgress(current, total);
- });
+ connect(jobPtr, &NetJob::succeeded, this, &PackInstallTask::onModDownloadSucceeded);
+ connect(jobPtr, &NetJob::failed, this, &PackInstallTask::onModDownloadFailed);
+ connect(jobPtr, &NetJob::progress, this, &PackInstallTask::setProgress);
+ m_net_job = jobPtr;
jobPtr->start();
+
+ m_abortable = true;
+}
+
+void PackInstallTask::onModDownloadSucceeded()
+{
+ m_net_job.reset();
+ install();
}
void PackInstallTask::install()
{
- setStatus(tr("Copying modpack files"));
+ setStatus(tr("Copying modpack files..."));
+ setProgress(0, m_files_to_copy.size());
+ QCoreApplication::processEvents();
+
+ m_abortable = false;
- for (auto iter = filesToCopy.begin(); iter != filesToCopy.end(); iter++) {
- auto &to = iter.key();
- auto &from = iter.value();
+ int i = 0;
+ for (auto iter = m_files_to_copy.constBegin(); iter != m_files_to_copy.constEnd(); iter++) {
+ auto& to = iter.key();
+ auto& from = iter.value();
FS::copy fileCopyOperation(from, to);
- if(!fileCopyOperation()) {
+ if (!fileCopyOperation()) {
qWarning() << "Failed to copy" << from << "to" << to;
emitFailed(tr("Failed to copy files"));
return;
}
+
+ setProgress(i++, m_files_to_copy.size());
+ QCoreApplication::processEvents();
}
- setStatus(tr("Installing modpack"));
+ setStatus(tr("Installing modpack..."));
+ QCoreApplication::processEvents();
auto instanceConfigPath = FS::PathCombine(m_stagingPath, "instance.cfg");
auto instanceSettings = std::make_shared<INISettingsObject>(instanceConfigPath);
@@ -205,20 +300,20 @@ void PackInstallTask::install()
auto components = instance.getPackProfile();
components->buildingFromScratch();
- for(auto target : m_version.targets) {
- if(target.type == "game" && target.name == "minecraft") {
+ for (auto target : m_version.targets) {
+ if (target.type == "game" && target.name == "minecraft") {
components->setComponentVersion("net.minecraft", target.version, true);
break;
}
}
- for(auto target : m_version.targets) {
- if(target.type != "modloader") continue;
+ for (auto target : m_version.targets) {
+ if (target.type != "modloader")
+ continue;
- if(target.name == "forge") {
+ if (target.name == "forge") {
components->setComponentVersion("net.minecraftforge", target.version);
- }
- else if(target.name == "fabric") {
+ } else if (target.name == "fabric") {
components->setComponentVersion("net.fabricmc.fabric-loader", target.version);
}
}
@@ -245,4 +340,20 @@ void PackInstallTask::install()
emitSucceeded();
}
+void PackInstallTask::onManifestDownloadFailed(QString reason)
+{
+ m_net_job.reset();
+ emitFailed(reason);
+}
+void PackInstallTask::onResolveModsFailed(QString reason)
+{
+ m_net_job.reset();
+ emitFailed(reason);
}
+void PackInstallTask::onModDownloadFailed(QString reason)
+{
+ m_net_job.reset();
+ emitFailed(reason);
+}
+
+} // namespace ModpacksCH
diff --git a/launcher/modplatform/modpacksch/FTBPackInstallTask.h b/launcher/modplatform/modpacksch/FTBPackInstallTask.h
index ff59b695..e63ca0df 100644
--- a/launcher/modplatform/modpacksch/FTBPackInstallTask.h
+++ b/launcher/modplatform/modpacksch/FTBPackInstallTask.h
@@ -1,18 +1,38 @@
+// SPDX-License-Identifier: GPL-3.0-only
/*
- * Copyright 2020-2021 Jamie Mansfield <jmansfield@cadixdev.org>
- * Copyright 2020-2021 Petr Mrazek <peterix@gmail.com>
+ * PolyMC - Minecraft Launcher
+ * Copyright (C) 2022 flowln <flowlnlnln@gmail.com>
+ * Copyright (C) 2022 Sefa Eyeoglu <contact@scrumplex.net>
*
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, version 3.
*
- * http://www.apache.org/licenses/LICENSE-2.0
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
*
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see <https://www.gnu.org/licenses/>.
+ *
+ * This file incorporates work covered by the following copyright and
+ * permission notice:
+ *
+ * Copyright 2020-2021 Jamie Mansfield <jmansfield@cadixdev.org>
+ * Copyright 2020-2021 Petr Mrazek <peterix@gmail.com>
+ *
+ * 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
@@ -20,44 +40,60 @@
#include "FTBPackManifest.h"
#include "InstanceTask.h"
+#include "QObjectPtr.h"
+#include "modplatform/flame/FileResolvingTask.h"
#include "net/NetJob.h"
+#include <QWidget>
+
namespace ModpacksCH {
-class PackInstallTask : public InstanceTask
+class PackInstallTask final : public InstanceTask
{
Q_OBJECT
public:
- explicit PackInstallTask(Modpack pack, QString version);
- virtual ~PackInstallTask(){}
+ explicit PackInstallTask(Modpack pack, QString version, QWidget* parent = nullptr);
+ ~PackInstallTask() override = default;
- bool canAbort() const override { return true; }
+ bool canAbort() const override { return m_abortable; }
bool abort() override;
protected:
- virtual void executeTask() override;
+ void executeTask() override;
private slots:
- void onDownloadSucceeded();
- void onDownloadFailed(QString reason);
+ void onManifestDownloadSucceeded();
+ void onResolveModsSucceeded();
+ void onModDownloadSucceeded();
+
+ void onManifestDownloadFailed(QString reason);
+ void onResolveModsFailed(QString reason);
+ void onModDownloadFailed(QString reason);
private:
+ void resolveMods();
void downloadPack();
void install();
private:
- bool abortable = false;
+ bool m_abortable = true;
+
+ NetJob::Ptr m_net_job = nullptr;
+ shared_qobject_ptr<Flame::FileResolvingTask> m_mod_id_resolver_task = nullptr;
+
+ QList<int> m_file_id_map;
- NetJob::Ptr jobPtr;
- QByteArray response;
+ QByteArray m_response;
Modpack m_pack;
QString m_version_name;
Version m_version;
- QMap<QString, QString> filesToCopy;
+ QMap<QString, QString> m_files_to_copy;
+ //FIXME: nuke
+ QWidget* m_parent;
};
}
diff --git a/launcher/modplatform/modpacksch/FTBPackManifest.cpp b/launcher/modplatform/modpacksch/FTBPackManifest.cpp
index e2d47a5b..421527ae 100644
--- a/launcher/modplatform/modpacksch/FTBPackManifest.cpp
+++ b/launcher/modplatform/modpacksch/FTBPackManifest.cpp
@@ -1,18 +1,37 @@
+// SPDX-License-Identifier: GPL-3.0-only
/*
- * Copyright 2020 Jamie Mansfield <jmansfield@cadixdev.org>
- * Copyright 2020-2021 Petr Mrazek <peterix@gmail.com>
+ * PolyMC - Minecraft Launcher
+ * Copyright (C) 2022 Sefa Eyeoglu <contact@scrumplex.net>
*
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, version 3.
*
- * http://www.apache.org/licenses/LICENSE-2.0
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
*
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see <https://www.gnu.org/licenses/>.
+ *
+ * This file incorporates work covered by the following copyright and
+ * permission notice:
+ *
+ * Copyright 2020 Jamie Mansfield <jmansfield@cadixdev.org>
+ * Copyright 2020-2021 Petr Mrazek <peterix@gmail.com>
+ *
+ * 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 "FTBPackManifest.h"
@@ -127,13 +146,16 @@ static void loadVersionFile(ModpacksCH::VersionFile & a, QJsonObject & obj)
a.path = Json::requireString(obj, "path");
a.name = Json::requireString(obj, "name");
a.version = Json::requireString(obj, "version");
- a.url = Json::requireString(obj, "url");
+ a.url = Json::ensureString(obj, "url"); // optional
a.sha1 = Json::requireString(obj, "sha1");
a.size = Json::requireInteger(obj, "size");
a.clientOnly = Json::requireBoolean(obj, "clientonly");
a.serverOnly = Json::requireBoolean(obj, "serveronly");
a.optional = Json::requireBoolean(obj, "optional");
a.updated = Json::requireInteger(obj, "updated");
+ auto curseforgeObj = Json::ensureObject(obj, "curseforge"); // optional
+ a.curseforge.project_id = Json::ensureInteger(curseforgeObj, "project");
+ a.curseforge.file_id = Json::ensureInteger(curseforgeObj, "file");
}
void ModpacksCH::loadVersion(ModpacksCH::Version & m, QJsonObject & obj)
diff --git a/launcher/modplatform/modpacksch/FTBPackManifest.h b/launcher/modplatform/modpacksch/FTBPackManifest.h
index da45d8ac..a8b6f35e 100644
--- a/launcher/modplatform/modpacksch/FTBPackManifest.h
+++ b/launcher/modplatform/modpacksch/FTBPackManifest.h
@@ -1,18 +1,37 @@
+// SPDX-License-Identifier: GPL-3.0-only
/*
- * Copyright 2020-2021 Jamie Mansfield <jmansfield@cadixdev.org>
- * Copyright 2020 Petr Mrazek <peterix@gmail.com>
+ * PolyMC - Minecraft Launcher
+ * Copyright (C) 2022 Sefa Eyeoglu <contact@scrumplex.net>
*
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, version 3.
*
- * http://www.apache.org/licenses/LICENSE-2.0
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
*
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see <https://www.gnu.org/licenses/>.
+ *
+ * This file incorporates work covered by the following copyright and
+ * permission notice:
+ *
+ * Copyright 2020-2021 Jamie Mansfield <jmansfield@cadixdev.org>
+ * Copyright 2020 Petr Mrazek <peterix@gmail.com>
+ *
+ * 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
@@ -97,6 +116,12 @@ struct VersionTarget
int64_t updated;
};
+struct VersionFileCurseForge
+{
+ int project_id;
+ int file_id;
+};
+
struct VersionFile
{
int id;
@@ -111,6 +136,7 @@ struct VersionFile
bool serverOnly;
bool optional;
int64_t updated;
+ VersionFileCurseForge curseforge;
};
struct Version
diff --git a/launcher/modplatform/modrinth/ModrinthAPI.cpp b/launcher/modplatform/modrinth/ModrinthAPI.cpp
new file mode 100644
index 00000000..747cf4c3
--- /dev/null
+++ b/launcher/modplatform/modrinth/ModrinthAPI.cpp
@@ -0,0 +1,108 @@
+#include "ModrinthAPI.h"
+
+#include "Application.h"
+#include "Json.h"
+#include "net/Upload.h"
+
+auto ModrinthAPI::currentVersion(QString hash, QString hash_format, QByteArray* response) -> NetJob::Ptr
+{
+ auto* netJob = new NetJob(QString("Modrinth::GetCurrentVersion"), APPLICATION->network());
+
+ netJob->addNetAction(Net::Download::makeByteArray(
+ QString(BuildConfig.MODRINTH_PROD_URL + "/version_file/%1?algorithm=%2").arg(hash, hash_format), response));
+
+ QObject::connect(netJob, &NetJob::finished, [response] { delete response; });
+
+ return netJob;
+}
+
+auto ModrinthAPI::currentVersions(const QStringList& hashes, QString hash_format, QByteArray* response) -> NetJob::Ptr
+{
+ auto* netJob = new NetJob(QString("Modrinth::GetCurrentVersions"), APPLICATION->network());
+
+ QJsonObject body_obj;
+
+ Json::writeStringList(body_obj, "hashes", hashes);
+ Json::writeString(body_obj, "algorithm", hash_format);
+
+ QJsonDocument body(body_obj);
+ auto body_raw = body.toJson();
+
+ netJob->addNetAction(Net::Upload::makeByteArray(QString(BuildConfig.MODRINTH_PROD_URL + "/version_files"), response, body_raw));
+
+ QObject::connect(netJob, &NetJob::finished, [response] { delete response; });
+
+ return netJob;
+}
+
+auto ModrinthAPI::latestVersion(QString hash,
+ QString hash_format,
+ std::list<Version> mcVersions,
+ ModLoaderTypes loaders,
+ QByteArray* response) -> NetJob::Ptr
+{
+ auto* netJob = new NetJob(QString("Modrinth::GetLatestVersion"), APPLICATION->network());
+
+ QJsonObject body_obj;
+
+ Json::writeStringList(body_obj, "loaders", getModLoaderStrings(loaders));
+
+ QStringList game_versions;
+ for (auto& ver : mcVersions) {
+ game_versions.append(ver.toString());
+ }
+ Json::writeStringList(body_obj, "game_versions", game_versions);
+
+ QJsonDocument body(body_obj);
+ auto body_raw = body.toJson();
+
+ netJob->addNetAction(Net::Upload::makeByteArray(
+ QString(BuildConfig.MODRINTH_PROD_URL + "/version_file/%1/update?algorithm=%2").arg(hash, hash_format), response, body_raw));
+
+ QObject::connect(netJob, &NetJob::finished, [response] { delete response; });
+
+ return netJob;
+}
+
+auto ModrinthAPI::latestVersions(const QStringList& hashes,
+ QString hash_format,
+ std::list<Version> mcVersions,
+ ModLoaderTypes loaders,
+ QByteArray* response) -> NetJob::Ptr
+{
+ auto* netJob = new NetJob(QString("Modrinth::GetLatestVersions"), APPLICATION->network());
+
+ QJsonObject body_obj;
+
+ Json::writeStringList(body_obj, "hashes", hashes);
+ Json::writeString(body_obj, "algorithm", hash_format);
+
+ Json::writeStringList(body_obj, "loaders", getModLoaderStrings(loaders));
+
+ QStringList game_versions;
+ for (auto& ver : mcVersions) {
+ game_versions.append(ver.toString());
+ }
+ Json::writeStringList(body_obj, "game_versions", game_versions);
+
+ QJsonDocument body(body_obj);
+ auto body_raw = body.toJson();
+
+ netJob->addNetAction(Net::Upload::makeByteArray(QString(BuildConfig.MODRINTH_PROD_URL + "/version_files/update"), response, body_raw));
+
+ QObject::connect(netJob, &NetJob::finished, [response] { delete response; });
+
+ return netJob;
+}
+
+auto ModrinthAPI::getProjects(QStringList addonIds, QByteArray* response) const -> NetJob*
+{
+ auto netJob = new NetJob(QString("Modrinth::GetProjects"), APPLICATION->network());
+ auto searchUrl = getMultipleModInfoURL(addonIds);
+
+ netJob->addNetAction(Net::Download::makeByteArray(QUrl(searchUrl), response));
+
+ QObject::connect(netJob, &NetJob::finished, [response, netJob] { delete response; netJob->deleteLater(); });
+
+ return netJob;
+}
diff --git a/launcher/modplatform/modrinth/ModrinthAPI.h b/launcher/modplatform/modrinth/ModrinthAPI.h
index 89e52d6c..e1a18681 100644
--- a/launcher/modplatform/modrinth/ModrinthAPI.h
+++ b/launcher/modplatform/modrinth/ModrinthAPI.h
@@ -27,6 +27,29 @@
class ModrinthAPI : public NetworkModAPI {
public:
+ auto currentVersion(QString hash,
+ QString hash_format,
+ QByteArray* response) -> NetJob::Ptr;
+
+ auto currentVersions(const QStringList& hashes,
+ QString hash_format,
+ QByteArray* response) -> NetJob::Ptr;
+
+ auto latestVersion(QString hash,
+ QString hash_format,
+ std::list<Version> mcVersions,
+ ModLoaderTypes loaders,
+ QByteArray* response) -> NetJob::Ptr;
+
+ auto latestVersions(const QStringList& hashes,
+ QString hash_format,
+ std::list<Version> mcVersions,
+ ModLoaderTypes loaders,
+ QByteArray* response) -> NetJob::Ptr;
+
+ auto getProjects(QStringList addonIds, QByteArray* response) const -> NetJob* override;
+
+ public:
inline auto getAuthorURL(const QString& name) const -> QString { return "https://modrinth.com/user/" + name; };
static auto getModLoaderStrings(const ModLoaderTypes types) -> const QStringList
@@ -81,6 +104,11 @@ class ModrinthAPI : public NetworkModAPI {
return BuildConfig.MODRINTH_PROD_URL + "/project/" + id;
};
+ inline auto getMultipleModInfoURL(QStringList ids) const -> QString
+ {
+ return BuildConfig.MODRINTH_PROD_URL + QString("/projects?ids=[\"%1\"]").arg(ids.join("\",\""));
+ };
+
inline auto getVersionsURL(VersionSearchArgs& args) const -> QString override
{
return QString(BuildConfig.MODRINTH_PROD_URL +
diff --git a/launcher/modplatform/modrinth/ModrinthCheckUpdate.cpp b/launcher/modplatform/modrinth/ModrinthCheckUpdate.cpp
new file mode 100644
index 00000000..79d8edf7
--- /dev/null
+++ b/launcher/modplatform/modrinth/ModrinthCheckUpdate.cpp
@@ -0,0 +1,174 @@
+#include "ModrinthCheckUpdate.h"
+#include "ModrinthAPI.h"
+#include "ModrinthPackIndex.h"
+
+#include "FileSystem.h"
+#include "Json.h"
+
+#include "ModDownloadTask.h"
+
+static ModrinthAPI api;
+static ModPlatform::ProviderCapabilities ProviderCaps;
+
+bool ModrinthCheckUpdate::abort()
+{
+ if (m_net_job)
+ return m_net_job->abort();
+ return true;
+}
+
+/* Check for update:
+ * - Get latest version available
+ * - Compare hash of the latest version with the current hash
+ * - If equal, no updates, else, there's updates, so add to the list
+ * */
+void ModrinthCheckUpdate::executeTask()
+{
+ setStatus(tr("Preparing mods for Modrinth..."));
+ setProgress(0, 3);
+
+ QHash<QString, Mod*> mappings;
+
+ // Create all hashes
+ QStringList hashes;
+ auto best_hash_type = ProviderCaps.hashType(ModPlatform::Provider::MODRINTH).first();
+ for (auto* mod : m_mods) {
+ if (!mod->enabled()) {
+ emit checkFailed(mod, tr("Disabled mods won't be updated, to prevent mod duplication issues!"));
+ continue;
+ }
+
+ auto hash = mod->metadata()->hash;
+
+ // Sadly the API can only handle one hash type per call, se we
+ // need to generate a new hash if the current one is innadequate
+ // (though it will rarely happen, if at all)
+ if (mod->metadata()->hash_format != best_hash_type) {
+ QByteArray jar_data;
+
+ try {
+ jar_data = FS::read(mod->fileinfo().absoluteFilePath());
+ } catch (FS::FileSystemException& e) {
+ qCritical() << QString("Failed to open / read JAR file of %1").arg(mod->name());
+ qCritical() << QString("Reason: ") << e.cause();
+
+ failed(e.what());
+ return;
+ }
+
+ hash = QString(ProviderCaps.hash(ModPlatform::Provider::MODRINTH, jar_data, best_hash_type).toHex());
+ }
+
+ hashes.append(hash);
+ mappings.insert(hash, mod);
+ }
+
+ auto* response = new QByteArray();
+ auto job = api.latestVersions(hashes, best_hash_type, m_game_versions, m_loaders, response);
+
+ QEventLoop lock;
+
+ connect(job.get(), &Task::succeeded, this, [this, response, &mappings, best_hash_type, job] {
+ QJsonParseError parse_error{};
+ QJsonDocument doc = QJsonDocument::fromJson(*response, &parse_error);
+ if (parse_error.error != QJsonParseError::NoError) {
+ qWarning() << "Error while parsing JSON response from ModrinthCheckUpdate at " << parse_error.offset
+ << " reason: " << parse_error.errorString();
+ qWarning() << *response;
+
+ failed(parse_error.errorString());
+ return;
+ }
+
+ setStatus(tr("Parsing the API response from Modrinth..."));
+ setProgress(2, 3);
+
+ try {
+ for (auto hash : mappings.keys()) {
+ auto project_obj = doc[hash].toObject();
+
+ // If the returned project is empty, but we have Modrinth metadata,
+ // it means this specific version is not available
+ if (project_obj.isEmpty()) {
+ qDebug() << "Mod " << mappings.find(hash).value()->name() << " got an empty response.";
+ qDebug() << "Hash: " << hash;
+
+ emit checkFailed(
+ mappings.find(hash).value(),
+ tr("No valid version found for this mod. It's probably unavailable for the current game version / mod loader."));
+
+ continue;
+ }
+
+ // 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 = { ModAPI::ModLoaderType::Forge, ModAPI::ModLoaderType::Fabric, ModAPI::ModLoaderType::Quilt };
+ for (auto flag : flags) {
+ if (m_loaders.testFlag(flag)) {
+ loader_filter = api.getModLoaderString(flag);
+ break;
+ }
+ }
+
+ // Currently, we rely on a couple heuristics to determine whether an update is actually available or not:
+ // - The file needs to be preferred: It is either the primary file, or the one found via (explicit) usage of the loader_filter
+ // - The version reported by the JAR is different from the version reported by the indexed version (it's usually the case)
+ // Such is the pain of having arbitrary files for a given version .-.
+
+ auto project_ver = Modrinth::loadIndexedPackVersion(project_obj, best_hash_type, loader_filter);
+ if (project_ver.downloadUrl.isEmpty()) {
+ qCritical() << "Modrinth mod without download url!";
+ qCritical() << project_ver.fileName;
+
+ emit checkFailed(mappings.find(hash).value(), tr("Mod has an empty download URL"));
+
+ continue;
+ }
+
+ auto mod_iter = mappings.find(hash);
+ if (mod_iter == mappings.end()) {
+ qCritical() << "Failed to remap mod from Modrinth!";
+ continue;
+ }
+ auto mod = *mod_iter;
+
+ auto key = project_ver.hash;
+ if ((key != hash && project_ver.is_preferred) || (mod->status() == ModStatus::NotInstalled)) {
+ if (mod->version() == project_ver.version_number)
+ continue;
+
+ // Fake pack with the necessary info to pass to the download task :)
+ ModPlatform::IndexedPack pack;
+ pack.name = mod->name();
+ pack.slug = mod->metadata()->slug;
+ pack.addonId = mod->metadata()->project_id;
+ pack.websiteUrl = mod->homeurl();
+ for (auto& author : mod->authors())
+ pack.authors.append({ author });
+ pack.description = mod->description();
+ pack.provider = ModPlatform::Provider::MODRINTH;
+
+ auto download_task = new ModDownloadTask(pack, project_ver, m_mods_folder);
+
+ m_updatable.emplace_back(pack.name, hash, mod->version(), project_ver.version_number, project_ver.changelog,
+ ModPlatform::Provider::MODRINTH, download_task);
+ }
+ }
+ } catch (Json::JsonException& e) {
+ failed(e.cause() + " : " + e.what());
+ }
+ });
+
+ connect(job.get(), &Task::finished, &lock, &QEventLoop::quit);
+
+ setStatus(tr("Waiting for the API response from Modrinth..."));
+ setProgress(1, 3);
+
+ m_net_job = job.get();
+ job->start();
+
+ lock.exec();
+
+ emitSucceeded();
+}
diff --git a/launcher/modplatform/modrinth/ModrinthCheckUpdate.h b/launcher/modplatform/modrinth/ModrinthCheckUpdate.h
new file mode 100644
index 00000000..abf8ada1
--- /dev/null
+++ b/launcher/modplatform/modrinth/ModrinthCheckUpdate.h
@@ -0,0 +1,23 @@
+#pragma once
+
+#include "Application.h"
+#include "modplatform/CheckUpdateTask.h"
+#include "net/NetJob.h"
+
+class ModrinthCheckUpdate : public CheckUpdateTask {
+ Q_OBJECT
+
+ public:
+ ModrinthCheckUpdate(QList<Mod*>& mods, std::list<Version>& mcVersions, ModAPI::ModLoaderTypes loaders, std::shared_ptr<ModFolderModel> mods_folder)
+ : CheckUpdateTask(mods, mcVersions, loaders, mods_folder)
+ {}
+
+ public slots:
+ bool abort() override;
+
+ protected slots:
+ void executeTask() override;
+
+ private:
+ NetJob* m_net_job = nullptr;
+};
diff --git a/launcher/modplatform/modrinth/ModrinthPackIndex.cpp b/launcher/modplatform/modrinth/ModrinthPackIndex.cpp
index b6f5490a..e50dd96d 100644
--- a/launcher/modplatform/modrinth/ModrinthPackIndex.cpp
+++ b/launcher/modplatform/modrinth/ModrinthPackIndex.cpp
@@ -29,13 +29,16 @@ static ModPlatform::ProviderCapabilities ProviderCaps;
void Modrinth::loadIndexedPack(ModPlatform::IndexedPack& pack, QJsonObject& obj)
{
- pack.addonId = Json::requireString(obj, "project_id");
+ pack.addonId = Json::ensureString(obj, "project_id");
+ if (pack.addonId.toString().isEmpty())
+ pack.addonId = Json::requireString(obj, "id");
+
pack.provider = ModPlatform::Provider::MODRINTH;
pack.name = Json::requireString(obj, "title");
- QString slug = Json::ensureString(obj, "slug", "");
- if (!slug.isEmpty())
- pack.websiteUrl = "https://modrinth.com/mod/" + Json::ensureString(obj, "slug", "");
+ pack.slug = Json::ensureString(obj, "slug", "");
+ if (!pack.slug.isEmpty())
+ pack.websiteUrl = "https://modrinth.com/mod/" + pack.slug;
else
pack.websiteUrl = "";
@@ -45,7 +48,7 @@ void Modrinth::loadIndexedPack(ModPlatform::IndexedPack& pack, QJsonObject& obj)
pack.logoName = pack.addonId.toString();
ModPlatform::ModpackAuthor modAuthor;
- modAuthor.name = Json::requireString(obj, "author");
+ modAuthor.name = Json::ensureString(obj, "author", QObject::tr("No author(s)"));
modAuthor.url = api.getAuthorURL(modAuthor.name);
pack.authors.append(modAuthor);
@@ -111,7 +114,7 @@ void Modrinth::loadIndexedPackVersions(ModPlatform::IndexedPack& pack,
pack.versionsLoaded = true;
}
-auto Modrinth::loadIndexedPackVersion(QJsonObject &obj) -> ModPlatform::IndexedVersion
+auto Modrinth::loadIndexedPackVersion(QJsonObject &obj, QString preferred_hash_type, QString preferred_file_name) -> ModPlatform::IndexedVersion
{
ModPlatform::IndexedVersion file;
@@ -130,6 +133,8 @@ auto Modrinth::loadIndexedPackVersion(QJsonObject &obj) -> ModPlatform::IndexedV
file.loaders.append(loader.toString());
}
file.version = Json::requireString(obj, "name");
+ file.version_number = Json::requireString(obj, "version_number");
+ file.changelog = Json::requireString(obj, "changelog");
auto files = Json::requireArray(obj, "files");
int i = 0;
@@ -142,6 +147,11 @@ auto Modrinth::loadIndexedPackVersion(QJsonObject &obj) -> ModPlatform::IndexedV
auto parent = files[i].toObject();
auto fileName = Json::requireString(parent, "filename");
+ if (!preferred_file_name.isEmpty() && fileName.contains(preferred_file_name)) {
+ file.is_preferred = true;
+ break;
+ }
+
// Grab the primary file, if available
if (Json::requireBoolean(parent, "primary"))
break;
@@ -153,13 +163,20 @@ auto Modrinth::loadIndexedPackVersion(QJsonObject &obj) -> ModPlatform::IndexedV
if (parent.contains("url")) {
file.downloadUrl = Json::requireString(parent, "url");
file.fileName = Json::requireString(parent, "filename");
+ file.is_preferred = Json::requireBoolean(parent, "primary") || (files.count() == 1);
auto hash_list = Json::requireObject(parent, "hashes");
- auto hash_types = ProviderCaps.hashType(ModPlatform::Provider::MODRINTH);
- for (auto& hash_type : hash_types) {
- if (hash_list.contains(hash_type)) {
- file.hash = Json::requireString(hash_list, hash_type);
- file.hash_type = hash_type;
- break;
+
+ if (hash_list.contains(preferred_hash_type)) {
+ file.hash = Json::requireString(hash_list, preferred_hash_type);
+ file.hash_type = preferred_hash_type;
+ } else {
+ auto hash_types = ProviderCaps.hashType(ModPlatform::Provider::MODRINTH);
+ for (auto& hash_type : hash_types) {
+ if (hash_list.contains(hash_type)) {
+ file.hash = Json::requireString(hash_list, hash_type);
+ file.hash_type = hash_type;
+ break;
+ }
}
}
diff --git a/launcher/modplatform/modrinth/ModrinthPackIndex.h b/launcher/modplatform/modrinth/ModrinthPackIndex.h
index b7936204..31881414 100644
--- a/launcher/modplatform/modrinth/ModrinthPackIndex.h
+++ b/launcher/modplatform/modrinth/ModrinthPackIndex.h
@@ -30,6 +30,6 @@ void loadIndexedPackVersions(ModPlatform::IndexedPack& pack,
QJsonArray& arr,
const shared_qobject_ptr<QNetworkAccessManager>& network,
BaseInstance* inst);
-auto loadIndexedPackVersion(QJsonObject& obj) -> ModPlatform::IndexedVersion;
+auto loadIndexedPackVersion(QJsonObject& obj, QString hash_type = "sha512", QString filename_prefer = "") -> ModPlatform::IndexedVersion;
} // namespace Modrinth
diff --git a/launcher/modplatform/packwiz/Packwiz.cpp b/launcher/modplatform/packwiz/Packwiz.cpp
index 0782b9f4..c3561093 100644
--- a/launcher/modplatform/packwiz/Packwiz.cpp
+++ b/launcher/modplatform/packwiz/Packwiz.cpp
@@ -55,11 +55,11 @@ auto getRealIndexName(QDir& index_dir, QString normalized_fname, bool should_fin
}
// Helpers
-static inline auto indexFileName(QString const& mod_name) -> QString
+static inline auto indexFileName(QString const& mod_slug) -> QString
{
- if(mod_name.endsWith(".pw.toml"))
- return mod_name;
- return QString("%1.pw.toml").arg(mod_name);
+ if(mod_slug.endsWith(".pw.toml"))
+ return mod_slug;
+ return QString("%1.pw.toml").arg(mod_slug);
}
static ModPlatform::ProviderCapabilities ProviderCaps;
@@ -95,6 +95,7 @@ auto V1::createModFormat(QDir& index_dir, ModPlatform::IndexedPack& mod_pack, Mo
{
Mod mod;
+ mod.slug = mod_pack.slug;
mod.name = mod_pack.name;
mod.filename = mod_version.fileName;
@@ -116,12 +117,10 @@ auto V1::createModFormat(QDir& index_dir, ModPlatform::IndexedPack& mod_pack, Mo
return mod;
}
-auto V1::createModFormat(QDir& index_dir, ::Mod& internal_mod) -> Mod
+auto V1::createModFormat(QDir& index_dir, ::Mod& internal_mod, QString slug) -> Mod
{
- auto mod_name = internal_mod.name();
-
// Try getting metadata if it exists
- Mod mod { getIndexForMod(index_dir, mod_name) };
+ Mod mod { getIndexForMod(index_dir, slug) };
if(mod.isValid())
return mod;
@@ -139,11 +138,14 @@ void V1::updateModIndex(QDir& index_dir, Mod& mod)
// Ensure the corresponding mod's info exists, and create it if not
- auto normalized_fname = indexFileName(mod.name);
+ auto normalized_fname = indexFileName(mod.slug);
auto real_fname = getRealIndexName(index_dir, normalized_fname);
QFile index_file(index_dir.absoluteFilePath(real_fname));
+ if (real_fname != normalized_fname)
+ index_file.rename(normalized_fname);
+
// There's already data on there!
// TODO: We should do more stuff here, as the user is likely trying to
// override a file. In this case, check versions and ask the user what
@@ -184,33 +186,46 @@ void V1::updateModIndex(QDir& index_dir, Mod& mod)
}
}
+ index_file.flush();
index_file.close();
}
-void V1::deleteModIndex(QDir& index_dir, QString& mod_name)
+void V1::deleteModIndex(QDir& index_dir, QString& mod_slug)
{
- auto normalized_fname = indexFileName(mod_name);
+ auto normalized_fname = indexFileName(mod_slug);
auto real_fname = getRealIndexName(index_dir, normalized_fname);
if (real_fname.isEmpty())
return;
QFile index_file(index_dir.absoluteFilePath(real_fname));
- if(!index_file.exists()){
- qWarning() << QString("Tried to delete non-existent mod metadata for %1!").arg(mod_name);
+ if (!index_file.exists()) {
+ qWarning() << QString("Tried to delete non-existent mod metadata for %1!").arg(mod_slug);
return;
}
- if(!index_file.remove()){
- qWarning() << QString("Failed to remove metadata for mod %1!").arg(mod_name);
+ if (!index_file.remove()) {
+ qWarning() << QString("Failed to remove metadata for mod %1!").arg(mod_slug);
+ }
+}
+
+void V1::deleteModIndex(QDir& index_dir, QVariant& mod_id)
+{
+ for (auto& file_name : index_dir.entryList(QDir::Filter::Files)) {
+ auto mod = getIndexForMod(index_dir, file_name);
+
+ if (mod.mod_id() == mod_id) {
+ deleteModIndex(index_dir, mod.name);
+ break;
+ }
}
}
-auto V1::getIndexForMod(QDir& index_dir, QString& index_file_name) -> Mod
+auto V1::getIndexForMod(QDir& index_dir, QString slug) -> Mod
{
Mod mod;
- auto normalized_fname = indexFileName(index_file_name);
+ auto normalized_fname = indexFileName(slug);
auto real_fname = getRealIndexName(index_dir, normalized_fname, true);
if (real_fname.isEmpty())
return {};
@@ -218,7 +233,7 @@ auto V1::getIndexForMod(QDir& index_dir, QString& index_file_name) -> Mod
QFile index_file(index_dir.absoluteFilePath(real_fname));
if (!index_file.open(QIODevice::ReadOnly)) {
- qWarning() << QString("Failed to open mod metadata for %1").arg(index_file_name);
+ qWarning() << QString("Failed to open mod metadata for %1").arg(slug);
return {};
}
@@ -232,11 +247,13 @@ auto V1::getIndexForMod(QDir& index_dir, QString& index_file_name) -> Mod
index_file.close();
if (!table) {
- qWarning() << QString("Could not open file %1!").arg(indexFileName(index_file_name));
+ qWarning() << QString("Could not open file %1!").arg(normalized_fname);
qWarning() << "Reason: " << QString(errbuf);
return {};
}
+ mod.slug = slug;
+
{ // Basic info
mod.name = stringEntry(table, "name");
mod.filename = stringEntry(table, "filename");
@@ -286,4 +303,16 @@ auto V1::getIndexForMod(QDir& index_dir, QString& index_file_name) -> Mod
return mod;
}
-} // namespace Packwiz
+auto V1::getIndexForMod(QDir& index_dir, QVariant& mod_id) -> Mod
+{
+ for (auto& file_name : index_dir.entryList(QDir::Filter::Files)) {
+ auto mod = getIndexForMod(index_dir, file_name);
+
+ if (mod.mod_id() == mod_id)
+ return mod;
+ }
+
+ return {};
+}
+
+} // namespace Packwiz
diff --git a/launcher/modplatform/packwiz/Packwiz.h b/launcher/modplatform/packwiz/Packwiz.h
index 3c99769c..3ec80377 100644
--- a/launcher/modplatform/packwiz/Packwiz.h
+++ b/launcher/modplatform/packwiz/Packwiz.h
@@ -40,6 +40,7 @@ auto intEntry(toml_table_t* parent, const char* entry_name) -> int;
class V1 {
public:
struct Mod {
+ QString slug {};
QString name {};
QString filename {};
// FIXME: make side an enum
@@ -58,7 +59,7 @@ class V1 {
public:
// This is a totally heuristic, but should work for now.
- auto isValid() const -> bool { return !name.isEmpty() && !project_id.isNull(); }
+ auto isValid() const -> bool { return !slug.isEmpty() && !project_id.isNull(); }
// Different providers can use different names for the same thing
// Modrinth-specific
@@ -71,9 +72,9 @@ class V1 {
* */
static auto createModFormat(QDir& index_dir, ModPlatform::IndexedPack& mod_pack, ModPlatform::IndexedVersion& mod_version) -> Mod;
/* Generates the object representing the information in a mod.pw.toml file via
- * its common representation in the launcher.
+ * its common representation in the launcher, plus a necessary slug.
* */
- static auto createModFormat(QDir& index_dir, ::Mod& internal_mod) -> Mod;
+ static auto createModFormat(QDir& index_dir, ::Mod& internal_mod, QString slug) -> Mod;
/* Updates the mod index for the provided mod.
* This creates a new index if one does not exist already
@@ -81,13 +82,21 @@ class V1 {
* */
static void updateModIndex(QDir& index_dir, Mod& mod);
- /* Deletes the metadata for the mod with the given name. If the metadata doesn't exist, it does nothing. */
- static void deleteModIndex(QDir& index_dir, QString& mod_name);
+ /* Deletes the metadata for the mod with the given slug. If the metadata doesn't exist, it does nothing. */
+ static void deleteModIndex(QDir& index_dir, QString& mod_slug);
- /* Gets the metadata for a mod with a particular name.
+ /* Deletes the metadata for the mod with the given id. If the metadata doesn't exist, it does nothing. */
+ static void deleteModIndex(QDir& index_dir, QVariant& mod_id);
+
+ /* Gets the metadata for a mod with a particular file name.
+ * If the mod doesn't have a metadata, it simply returns an empty Mod object.
+ * */
+ static auto getIndexForMod(QDir& index_dir, QString slug) -> Mod;
+
+ /* Gets the metadata for a mod with a particular id.
* If the mod doesn't have a metadata, it simply returns an empty Mod object.
* */
- static auto getIndexForMod(QDir& index_dir, QString& index_file_name) -> Mod;
+ static auto getIndexForMod(QDir& index_dir, QVariant& mod_id) -> Mod;
};
} // namespace Packwiz
diff --git a/launcher/modplatform/packwiz/Packwiz_test.cpp b/launcher/modplatform/packwiz/Packwiz_test.cpp
index d6251148..aa0c35df 100644
--- a/launcher/modplatform/packwiz/Packwiz_test.cpp
+++ b/launcher/modplatform/packwiz/Packwiz_test.cpp
@@ -32,10 +32,11 @@ class PackwizTest : public QObject {
QString source = QFINDTESTDATA("testdata");
QDir index_dir(source);
- QString name_mod("borderless-mining.pw.toml");
- QVERIFY(index_dir.entryList().contains(name_mod));
+ QString slug_mod("borderless-mining");
+ QString file_name = slug_mod + ".pw.toml";
+ QVERIFY(index_dir.entryList().contains(file_name));
- auto metadata = Packwiz::V1::getIndexForMod(index_dir, name_mod);
+ auto metadata = Packwiz::V1::getIndexForMod(index_dir, slug_mod);
QVERIFY(metadata.isValid());
diff --git a/launcher/modplatform/technic/SingleZipPackInstallTask.h b/launcher/modplatform/technic/SingleZipPackInstallTask.h
index 4d1fcbff..981ccf8a 100644
--- a/launcher/modplatform/technic/SingleZipPackInstallTask.h
+++ b/launcher/modplatform/technic/SingleZipPackInstallTask.h
@@ -24,7 +24,7 @@
#include <QStringList>
#include <QUrl>
-#include <nonstd/optional>
+#include <optional>
namespace Technic {
@@ -57,8 +57,8 @@ private:
QString m_archivePath;
NetJob::Ptr m_filesNetJob;
std::unique_ptr<QuaZip> m_packZip;
- QFuture<nonstd::optional<QStringList>> m_extractFuture;
- QFutureWatcher<nonstd::optional<QStringList>> m_extractFutureWatcher;
+ QFuture<std::optional<QStringList>> m_extractFuture;
+ QFutureWatcher<std::optional<QStringList>> m_extractFutureWatcher;
};
} // namespace Technic
diff --git a/launcher/net/NetUtils.h b/launcher/net/NetUtils.h
new file mode 100644
index 00000000..fa3bd8c0
--- /dev/null
+++ b/launcher/net/NetUtils.h
@@ -0,0 +1,44 @@
+// SPDX-License-Identifier: GPL-3.0-only
+/*
+ * PolyMC - Minecraft Launcher
+ * Copyright (c) 2022 Sefa Eyeoglu <contact@scrumplex.net>
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, version 3.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see <https://www.gnu.org/licenses/>.
+ */
+
+#pragma once
+
+#include <QNetworkReply>
+#include <QSet>
+
+namespace Net {
+ inline bool isApplicationError(QNetworkReply::NetworkError x) {
+ // Mainly taken from https://github.com/qt/qtbase/blob/dev/src/network/access/qhttpthreaddelegate.cpp
+ static QSet<QNetworkReply::NetworkError> errors = {
+ QNetworkReply::ProtocolInvalidOperationError,
+ QNetworkReply::AuthenticationRequiredError,
+ QNetworkReply::ContentAccessDenied,
+ QNetworkReply::ContentNotFoundError,
+ QNetworkReply::ContentOperationNotPermittedError,
+ QNetworkReply::ProxyAuthenticationRequiredError,
+ QNetworkReply::ContentConflictError,
+ QNetworkReply::ContentGoneError,
+ QNetworkReply::InternalServerError,
+ QNetworkReply::OperationNotImplementedError,
+ QNetworkReply::ServiceUnavailableError,
+ QNetworkReply::UnknownServerError,
+ QNetworkReply::UnknownContentError
+ };
+ return errors.contains(x);
+ }
+} // namespace Net
diff --git a/launcher/net/Upload.cpp b/launcher/net/Upload.cpp
index 12dd1e78..cfda4b4e 100644
--- a/launcher/net/Upload.cpp
+++ b/launcher/net/Upload.cpp
@@ -43,6 +43,16 @@
namespace Net {
+ bool Upload::abort()
+ {
+ if (m_reply) {
+ m_reply->abort();
+ } else {
+ m_state = State::AbortedByUser;
+ }
+ return true;
+ }
+
void Upload::downloadProgress(qint64 bytesReceived, qint64 bytesTotal) {
setProgress(bytesReceived, bytesTotal);
}
diff --git a/launcher/net/Upload.h b/launcher/net/Upload.h
index 56687a31..7c194bbc 100644
--- a/launcher/net/Upload.h
+++ b/launcher/net/Upload.h
@@ -46,6 +46,8 @@ namespace Net {
public:
static Upload::Ptr makeByteArray(QUrl url, QByteArray *output, QByteArray m_post_data);
+ auto abort() -> bool override;
+ auto canAbort() const -> bool override { return true; };
protected slots:
void downloadProgress(qint64 bytesReceived, qint64 bytesTotal) override;
diff --git a/launcher/tasks/MultipleOptionsTask.cpp b/launcher/tasks/MultipleOptionsTask.cpp
new file mode 100644
index 00000000..6e853568
--- /dev/null
+++ b/launcher/tasks/MultipleOptionsTask.cpp
@@ -0,0 +1,48 @@
+#include "MultipleOptionsTask.h"
+
+#include <QDebug>
+
+MultipleOptionsTask::MultipleOptionsTask(QObject* parent, const QString& task_name) : SequentialTask(parent, task_name) {}
+
+void MultipleOptionsTask::startNext()
+{
+ Task* previous = nullptr;
+ if (m_currentIndex != -1) {
+ previous = m_queue[m_currentIndex].get();
+ disconnect(previous, 0, this, 0);
+ }
+
+ m_currentIndex++;
+ if ((previous && previous->wasSuccessful())) {
+ emitSucceeded();
+ return;
+ }
+
+ Task::Ptr next = m_queue[m_currentIndex];
+
+ connect(next.get(), &Task::failed, this, &MultipleOptionsTask::subTaskFailed);
+ connect(next.get(), &Task::succeeded, this, &MultipleOptionsTask::startNext);
+
+ connect(next.get(), &Task::status, this, &MultipleOptionsTask::subTaskStatus);
+ connect(next.get(), &Task::stepStatus, this, &MultipleOptionsTask::subTaskStatus);
+
+ connect(next.get(), &Task::progress, this, &MultipleOptionsTask::subTaskProgress);
+
+ qDebug() << QString("Making attemp %1 out of %2").arg(m_currentIndex + 1).arg(m_queue.size());
+ setStatus(tr("Making attempt #%1 out of %2").arg(m_currentIndex + 1).arg(m_queue.size()));
+ setStepStatus(next->isMultiStep() ? next->getStepStatus() : next->getStatus());
+
+ next->start();
+}
+
+void MultipleOptionsTask::subTaskFailed(QString const& reason)
+{
+ qDebug() << QString("Failed attempt #%1 of %2. Reason: %3").arg(m_currentIndex + 1).arg(m_queue.size()).arg(reason);
+ if(m_currentIndex < m_queue.size() - 1) {
+ startNext();
+ return;
+ }
+
+ qWarning() << QString("All attempts have failed!");
+ emitFailed();
+}
diff --git a/launcher/tasks/MultipleOptionsTask.h b/launcher/tasks/MultipleOptionsTask.h
new file mode 100644
index 00000000..7c508b00
--- /dev/null
+++ b/launcher/tasks/MultipleOptionsTask.h
@@ -0,0 +1,19 @@
+#pragma once
+
+#include "SequentialTask.h"
+
+/* This task type will attempt to do run each of it's subtasks in sequence,
+ * until one of them succeeds. When that happens, the remaining tasks will not run.
+ * */
+class MultipleOptionsTask : public SequentialTask
+{
+ Q_OBJECT
+public:
+ explicit MultipleOptionsTask(QObject *parent = nullptr, const QString& task_name = "");
+ virtual ~MultipleOptionsTask() = default;
+
+private
+slots:
+ void startNext() override;
+ void subTaskFailed(const QString &msg) override;
+};
diff --git a/launcher/tasks/SequentialTask.cpp b/launcher/tasks/SequentialTask.cpp
index 7f03ad2e..f1e1a889 100644
--- a/launcher/tasks/SequentialTask.cpp
+++ b/launcher/tasks/SequentialTask.cpp
@@ -1,5 +1,7 @@
#include "SequentialTask.h"
+#include <QDebug>
+
SequentialTask::SequentialTask(QObject* parent, const QString& task_name) : Task(parent), m_name(task_name), m_currentIndex(-1) {}
SequentialTask::~SequentialTask()
@@ -39,14 +41,15 @@ bool SequentialTask::abort()
emit aborted();
emit finished();
}
- m_queue.clear();
+
+ m_aborted = true;
return true;
}
bool succeeded = m_queue[m_currentIndex]->abort();
- m_queue.clear();
+ m_aborted = succeeded;
- if(succeeded)
+ if (succeeded)
emitAborted();
return succeeded;
@@ -54,10 +57,14 @@ bool SequentialTask::abort()
void SequentialTask::startNext()
{
- if (m_currentIndex != -1) {
- Task::Ptr previous = m_queue[m_currentIndex];
+ if (m_aborted)
+ return;
+
+ if (m_currentIndex != -1 && m_currentIndex < m_queue.size()) {
+ Task::Ptr previous = m_queue.at(m_currentIndex);
disconnect(previous.get(), 0, this, 0);
}
+
m_currentIndex++;
if (m_queue.isEmpty() || m_currentIndex >= m_queue.size()) {
emitSucceeded();
@@ -76,6 +83,8 @@ void SequentialTask::startNext()
setStatus(tr("Executing task %1 out of %2").arg(m_currentIndex + 1).arg(m_queue.size()));
setStepStatus(next->isMultiStep() ? next->getStepStatus() : next->getStatus());
+ setProgress(m_currentIndex + 1, m_queue.count());
+
next->start();
}
@@ -93,7 +102,6 @@ void SequentialTask::subTaskProgress(qint64 current, qint64 total)
setProgress(0, 100);
return;
}
- setProgress(m_currentIndex + 1, m_queue.count());
m_stepProgress = current;
m_stepTotalProgress = total;
diff --git a/launcher/tasks/SequentialTask.h b/launcher/tasks/SequentialTask.h
index e10cb6f7..f5a58b1b 100644
--- a/launcher/tasks/SequentialTask.h
+++ b/launcher/tasks/SequentialTask.h
@@ -20,17 +20,17 @@ public:
void addTask(Task::Ptr task);
-protected slots:
- void executeTask() override;
public slots:
bool abort() override;
-private
+protected
slots:
- void startNext();
- void subTaskFailed(const QString &msg);
- void subTaskStatus(const QString &msg);
- void subTaskProgress(qint64 current, qint64 total);
+ void executeTask() override;
+
+ virtual void startNext();
+ virtual void subTaskFailed(const QString &msg);
+ virtual void subTaskStatus(const QString &msg);
+ virtual void subTaskProgress(qint64 current, qint64 total);
protected:
void setStepStatus(QString status) { m_step_status = status; emit stepStatus(status); };
@@ -44,4 +44,6 @@ protected:
qint64 m_stepProgress = 0;
qint64 m_stepTotalProgress = 100;
+
+ bool m_aborted = false;
};
diff --git a/launcher/ui/MainWindow.cpp b/launcher/ui/MainWindow.cpp
index d58f158e..c3d95599 100644
--- a/launcher/ui/MainWindow.cpp
+++ b/launcher/ui/MainWindow.cpp
@@ -252,6 +252,9 @@ public:
TranslatedAction actionViewInstanceFolder;
TranslatedAction actionViewCentralModsFolder;
+ QMenu * editMenu = nullptr;
+ TranslatedAction actionUndoTrashInstance;
+
QMenu * helpMenu = nullptr;
TranslatedToolButton helpMenuButton;
TranslatedAction actionReportBug;
@@ -335,6 +338,14 @@ public:
actionSettings->setShortcut(QKeySequence::Preferences);
all_actions.append(&actionSettings);
+ actionUndoTrashInstance = TranslatedAction(MainWindow);
+ connect(actionUndoTrashInstance, SIGNAL(triggered(bool)), MainWindow, SLOT(undoTrashInstance()));
+ actionUndoTrashInstance->setObjectName(QStringLiteral("actionUndoTrashInstance"));
+ actionUndoTrashInstance.setTextId(QT_TRANSLATE_NOOP("MainWindow", "&Undo Last Instance Deletion"));
+ actionUndoTrashInstance->setEnabled(APPLICATION->instances()->trashedSomething());
+ actionUndoTrashInstance->setShortcut(QKeySequence("Ctrl+Z"));
+ all_actions.append(&actionUndoTrashInstance);
+
if (!BuildConfig.BUG_TRACKER_URL.isEmpty()) {
actionReportBug = TranslatedAction(MainWindow);
actionReportBug->setObjectName(QStringLiteral("actionReportBug"));
@@ -508,6 +519,9 @@ public:
fileMenu->addSeparator();
fileMenu->addAction(actionSettings);
+ editMenu = menuBar->addMenu(tr("&Edit"));
+ editMenu->addAction(actionUndoTrashInstance);
+
viewMenu = menuBar->addMenu(tr("&View"));
viewMenu->setSeparatorsCollapsible(false);
viewMenu->addAction(actionCAT);
@@ -732,9 +746,10 @@ public:
actionDeleteInstance = TranslatedAction(MainWindow);
actionDeleteInstance->setObjectName(QStringLiteral("actionDeleteInstance"));
- actionDeleteInstance.setTextId(QT_TRANSLATE_NOOP("MainWindow", "Dele&te Instance..."));
+ actionDeleteInstance.setTextId(QT_TRANSLATE_NOOP("MainWindow", "Dele&te Instance"));
actionDeleteInstance.setTooltipId(QT_TRANSLATE_NOOP("MainWindow", "Delete the selected instance."));
actionDeleteInstance->setShortcuts({QKeySequence(tr("Backspace")), QKeySequence::Delete});
+ actionDeleteInstance->setAutoRepeat(false);
all_actions.append(&actionDeleteInstance);
actionCopyInstance = TranslatedAction(MainWindow);
@@ -1150,6 +1165,11 @@ void MainWindow::showInstanceContextMenu(const QPoint &pos)
connect(actionDeleteGroup, SIGNAL(triggered(bool)), SLOT(deleteGroup()));
actions.append(actionDeleteGroup);
}
+
+ QAction *actionUndoTrashInstance = new QAction("Undo last trash instance", this);
+ connect(actionUndoTrashInstance, SIGNAL(triggered(bool)), SLOT(undoTrashInstance()));
+ actionUndoTrashInstance->setEnabled(APPLICATION->instances()->trashedSomething());
+ actions.append(actionUndoTrashInstance);
}
QMenu myMenu;
myMenu.addActions(actions);
@@ -1832,6 +1852,11 @@ void MainWindow::deleteGroup()
}
}
+void MainWindow::undoTrashInstance()
+{
+ APPLICATION->instances()->undoTrashInstance();
+}
+
void MainWindow::on_actionViewInstanceFolder_triggered()
{
QString str = APPLICATION->settings()->get("InstanceDir").toString();
@@ -1957,7 +1982,12 @@ void MainWindow::on_actionDeleteInstance_triggered()
{
return;
}
+
auto id = m_selectedInstance->id();
+ if (APPLICATION->instances()->trashInstance(id)) {
+ return;
+ }
+
auto response = CustomMessageBox::selectable(
this,
tr("CAREFUL!"),
diff --git a/launcher/ui/MainWindow.h b/launcher/ui/MainWindow.h
index d7930b5a..dde3d02c 100644
--- a/launcher/ui/MainWindow.h
+++ b/launcher/ui/MainWindow.h
@@ -145,6 +145,7 @@ private slots:
void on_actionDeleteInstance_triggered();
void deleteGroup();
+ void undoTrashInstance();
void on_actionExportInstance_triggered();
diff --git a/launcher/ui/dialogs/ChooseProviderDialog.cpp b/launcher/ui/dialogs/ChooseProviderDialog.cpp
new file mode 100644
index 00000000..89935d9a
--- /dev/null
+++ b/launcher/ui/dialogs/ChooseProviderDialog.cpp
@@ -0,0 +1,96 @@
+#include "ChooseProviderDialog.h"
+#include "ui_ChooseProviderDialog.h"
+
+#include <QPushButton>
+#include <QRadioButton>
+
+#include "modplatform/ModIndex.h"
+
+static ModPlatform::ProviderCapabilities ProviderCaps;
+
+ChooseProviderDialog::ChooseProviderDialog(QWidget* parent, bool single_choice, bool allow_skipping)
+ : QDialog(parent), ui(new Ui::ChooseProviderDialog)
+{
+ ui->setupUi(this);
+
+ addProviders();
+ m_providers.button(0)->click();
+
+ connect(ui->skipOneButton, &QPushButton::clicked, this, &ChooseProviderDialog::skipOne);
+ connect(ui->skipAllButton, &QPushButton::clicked, this, &ChooseProviderDialog::skipAll);
+
+ connect(ui->confirmOneButton, &QPushButton::clicked, this, &ChooseProviderDialog::confirmOne);
+ connect(ui->confirmAllButton, &QPushButton::clicked, this, &ChooseProviderDialog::confirmAll);
+
+ if (single_choice) {
+ ui->providersLayout->removeWidget(ui->skipAllButton);
+ ui->providersLayout->removeWidget(ui->confirmAllButton);
+ }
+
+ if (!allow_skipping) {
+ ui->providersLayout->removeWidget(ui->skipOneButton);
+ ui->providersLayout->removeWidget(ui->skipAllButton);
+ }
+}
+
+ChooseProviderDialog::~ChooseProviderDialog()
+{
+ delete ui;
+}
+
+void ChooseProviderDialog::setDescription(QString desc)
+{
+ ui->explanationLabel->setText(desc);
+}
+
+void ChooseProviderDialog::skipOne()
+{
+ reject();
+}
+void ChooseProviderDialog::skipAll()
+{
+ m_response.skip_all = true;
+ reject();
+}
+
+void ChooseProviderDialog::confirmOne()
+{
+ m_response.chosen = getSelectedProvider();
+ m_response.try_others = ui->tryOthersCheckbox->isChecked();
+ accept();
+}
+void ChooseProviderDialog::confirmAll()
+{
+ m_response.chosen = getSelectedProvider();
+ m_response.confirm_all = true;
+ m_response.try_others = ui->tryOthersCheckbox->isChecked();
+ accept();
+}
+
+auto ChooseProviderDialog::getSelectedProvider() const -> ModPlatform::Provider
+{
+ return ModPlatform::Provider(m_providers.checkedId());
+}
+
+void ChooseProviderDialog::addProviders()
+{
+ int btn_index = 0;
+ QRadioButton* btn;
+
+ for (auto& provider : { ModPlatform::Provider::MODRINTH, ModPlatform::Provider::FLAME }) {
+ btn = new QRadioButton(ProviderCaps.readableName(provider), this);
+ m_providers.addButton(btn, btn_index++);
+ ui->providersLayout->addWidget(btn);
+ }
+}
+
+void ChooseProviderDialog::disableInput()
+{
+ for (auto& btn : m_providers.buttons())
+ btn->setEnabled(false);
+
+ ui->skipOneButton->setEnabled(false);
+ ui->skipAllButton->setEnabled(false);
+ ui->confirmOneButton->setEnabled(false);
+ ui->confirmAllButton->setEnabled(false);
+}
diff --git a/launcher/ui/dialogs/ChooseProviderDialog.h b/launcher/ui/dialogs/ChooseProviderDialog.h
new file mode 100644
index 00000000..4a3b9f29
--- /dev/null
+++ b/launcher/ui/dialogs/ChooseProviderDialog.h
@@ -0,0 +1,56 @@
+#pragma once
+
+#include <QButtonGroup>
+#include <QDialog>
+
+namespace Ui {
+class ChooseProviderDialog;
+}
+
+namespace ModPlatform {
+enum class Provider;
+}
+
+class Mod;
+class NetJob;
+class ModUpdateDialog;
+
+class ChooseProviderDialog : public QDialog {
+ Q_OBJECT
+
+ struct Response {
+ bool skip_all = false;
+ bool confirm_all = false;
+
+ bool try_others = false;
+
+ ModPlatform::Provider chosen;
+ };
+
+ public:
+ explicit ChooseProviderDialog(QWidget* parent, bool single_choice = false, bool allow_skipping = true);
+ ~ChooseProviderDialog();
+
+ auto getResponse() const -> Response { return m_response; }
+
+ void setDescription(QString desc);
+
+ private slots:
+ void skipOne();
+ void skipAll();
+ void confirmOne();
+ void confirmAll();
+
+ private:
+ void addProviders();
+ void disableInput();
+
+ auto getSelectedProvider() const -> ModPlatform::Provider;
+
+ private:
+ Ui::ChooseProviderDialog* ui;
+
+ QButtonGroup m_providers;
+
+ Response m_response;
+};
diff --git a/launcher/ui/dialogs/ChooseProviderDialog.ui b/launcher/ui/dialogs/ChooseProviderDialog.ui
new file mode 100644
index 00000000..78cd9613
--- /dev/null
+++ b/launcher/ui/dialogs/ChooseProviderDialog.ui
@@ -0,0 +1,89 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<ui version="4.0">
+ <class>ChooseProviderDialog</class>
+ <widget class="QDialog" name="ChooseProviderDialog">
+ <property name="geometry">
+ <rect>
+ <x>0</x>
+ <y>0</y>
+ <width>453</width>
+ <height>197</height>
+ </rect>
+ </property>
+ <property name="windowTitle">
+ <string>Choose a mod provider</string>
+ </property>
+ <layout class="QGridLayout" name="gridLayout">
+ <item row="0" column="0" colspan="2">
+ <widget class="QLabel" name="explanationLabel">
+ <property name="alignment">
+ <set>Qt::AlignJustify|Qt::AlignTop</set>
+ </property>
+ <property name="wordWrap">
+ <bool>true</bool>
+ </property>
+ <property name="indent">
+ <number>-1</number>
+ </property>
+ </widget>
+ </item>
+ <item row="1" column="0" colspan="2">
+ <layout class="QFormLayout" name="providersLayout">
+ <property name="labelAlignment">
+ <set>Qt::AlignHCenter|Qt::AlignTop</set>
+ </property>
+ <property name="formAlignment">
+ <set>Qt::AlignHCenter|Qt::AlignTop</set>
+ </property>
+ </layout>
+ </item>
+ <item row="4" column="0" colspan="2">
+ <layout class="QHBoxLayout" name="buttonsLayout">
+ <item>
+ <widget class="QPushButton" name="skipOneButton">
+ <property name="text">
+ <string>Skip this mod</string>
+ </property>
+ </widget>
+ </item>
+ <item>
+ <widget class="QPushButton" name="skipAllButton">
+ <property name="text">
+ <string>Skip all</string>
+ </property>
+ </widget>
+ </item>
+ <item>
+ <widget class="QPushButton" name="confirmAllButton">
+ <property name="text">
+ <string>Confirm for all</string>
+ </property>
+ </widget>
+ </item>
+ <item>
+ <widget class="QPushButton" name="confirmOneButton">
+ <property name="text">
+ <string>Confirm</string>
+ </property>
+ <property name="default">
+ <bool>true</bool>
+ </property>
+ </widget>
+ </item>
+ </layout>
+ </item>
+ <item row="3" column="0">
+ <widget class="QCheckBox" name="tryOthersCheckbox">
+ <property name="text">
+ <string>Try to automatically use other providers if the chosen one fails</string>
+ </property>
+ <property name="checked">
+ <bool>true</bool>
+ </property>
+ </widget>
+ </item>
+ </layout>
+ </widget>
+ <resources/>
+ <connections/>
+</ui>
diff --git a/launcher/ui/dialogs/ModUpdateDialog.cpp b/launcher/ui/dialogs/ModUpdateDialog.cpp
new file mode 100644
index 00000000..d73c8ebb
--- /dev/null
+++ b/launcher/ui/dialogs/ModUpdateDialog.cpp
@@ -0,0 +1,409 @@
+#include "ModUpdateDialog.h"
+#include "ChooseProviderDialog.h"
+#include "CustomMessageBox.h"
+#include "ProgressDialog.h"
+#include "ScrollMessageBox.h"
+#include "ui_ReviewMessageBox.h"
+
+#include "FileSystem.h"
+#include "Json.h"
+
+#include "tasks/ConcurrentTask.h"
+
+#include "minecraft/MinecraftInstance.h"
+#include "minecraft/PackProfile.h"
+
+#include "modplatform/EnsureMetadataTask.h"
+#include "modplatform/flame/FlameCheckUpdate.h"
+#include "modplatform/modrinth/ModrinthCheckUpdate.h"
+
+#include <HoeDown.h>
+#include <QTextBrowser>
+#include <QTreeWidgetItem>
+
+static ModPlatform::ProviderCapabilities ProviderCaps;
+
+static std::list<Version> mcVersions(BaseInstance* inst)
+{
+ return { static_cast<MinecraftInstance*>(inst)->getPackProfile()->getComponent("net.minecraft")->getVersion() };
+}
+
+static ModAPI::ModLoaderTypes mcLoaders(BaseInstance* inst)
+{
+ return { static_cast<MinecraftInstance*>(inst)->getPackProfile()->getModLoaders() };
+}
+
+ModUpdateDialog::ModUpdateDialog(QWidget* parent,
+ BaseInstance* instance,
+ const std::shared_ptr<ModFolderModel> mods,
+ QList<Mod::Ptr>& search_for)
+ : ReviewMessageBox(parent, tr("Confirm mods to update"), "")
+ , m_parent(parent)
+ , m_mod_model(mods)
+ , m_candidates(search_for)
+ , m_second_try_metadata(new ConcurrentTask())
+ , m_instance(instance)
+{
+ ReviewMessageBox::setGeometry(0, 0, 800, 600);
+
+ ui->explainLabel->setText(tr("You're about to update the following mods:"));
+ ui->onlyCheckedLabel->setText(tr("Only mods with a check will be updated!"));
+}
+
+void ModUpdateDialog::checkCandidates()
+{
+ // Ensure mods have valid metadata
+ auto went_well = ensureMetadata();
+ if (!went_well) {
+ m_aborted = true;
+ return;
+ }
+
+ // Report failed metadata generation
+ if (!m_failed_metadata.empty()) {
+ QString text;
+ for (const auto& failed : m_failed_metadata) {
+ const auto& mod = std::get<0>(failed);
+ const auto& reason = std::get<1>(failed);
+ text += tr("Mod name: %1<br>File name: %2<br>Reason: %3<br><br>").arg(mod->name(), mod->fileinfo().fileName(), reason);
+ }
+
+ ScrollMessageBox message_dialog(m_parent, tr("Metadata generation failed"),
+ tr("Could not generate metadata for the following mods:<br>"
+ "Do you wish to proceed without those mods?"),
+ text);
+ message_dialog.setModal(true);
+ if (message_dialog.exec() == QDialog::Rejected) {
+ m_aborted = true;
+ QMetaObject::invokeMethod(this, "reject", Qt::QueuedConnection);
+ return;
+ }
+ }
+
+ auto versions = mcVersions(m_instance);
+ auto loaders = mcLoaders(m_instance);
+
+ SequentialTask check_task(m_parent, tr("Checking for updates"));
+
+ if (!m_modrinth_to_update.empty()) {
+ m_modrinth_check_task = new ModrinthCheckUpdate(m_modrinth_to_update, versions, loaders, m_mod_model);
+ connect(m_modrinth_check_task, &CheckUpdateTask::checkFailed, this,
+ [this](Mod* mod, QString reason, QUrl recover_url) { m_failed_check_update.append({mod, reason, recover_url}); });
+ check_task.addTask(m_modrinth_check_task);
+ }
+
+ if (!m_flame_to_update.empty()) {
+ m_flame_check_task = new FlameCheckUpdate(m_flame_to_update, versions, loaders, m_mod_model);
+ connect(m_flame_check_task, &CheckUpdateTask::checkFailed, this,
+ [this](Mod* mod, QString reason, QUrl recover_url) { m_failed_check_update.append({mod, reason, recover_url}); });
+ check_task.addTask(m_flame_check_task);
+ }
+
+ connect(&check_task, &Task::failed, this,
+ [&](QString reason) { CustomMessageBox::selectable(this, tr("Error"), reason, QMessageBox::Critical)->exec(); });
+
+ connect(&check_task, &Task::succeeded, this, [&]() {
+ QStringList warnings = check_task.warnings();
+ if (warnings.count()) {
+ CustomMessageBox::selectable(this, tr("Warnings"), warnings.join('\n'), QMessageBox::Warning)->exec();
+ }
+ });
+
+ // Check for updates
+ ProgressDialog progress_dialog(m_parent);
+ progress_dialog.setSkipButton(true, tr("Abort"));
+ progress_dialog.setWindowTitle(tr("Checking for updates..."));
+ auto ret = progress_dialog.execWithTask(&check_task);
+
+ // If the dialog was skipped / some download error happened
+ if (ret == QDialog::DialogCode::Rejected) {
+ m_aborted = true;
+ QMetaObject::invokeMethod(this, "reject", Qt::QueuedConnection);
+ return;
+ }
+
+ // Add found updates for Modrinth
+ if (m_modrinth_check_task) {
+ auto modrinth_updates = m_modrinth_check_task->getUpdatable();
+ for (auto& updatable : modrinth_updates) {
+ qDebug() << QString("Mod %1 has an update available!").arg(updatable.name);
+
+ appendMod(updatable);
+ m_tasks.insert(updatable.name, updatable.download);
+ }
+ }
+
+ // Add found updated for Flame
+ if (m_flame_check_task) {
+ auto flame_updates = m_flame_check_task->getUpdatable();
+ for (auto& updatable : flame_updates) {
+ qDebug() << QString("Mod %1 has an update available!").arg(updatable.name);
+
+ appendMod(updatable);
+ m_tasks.insert(updatable.name, updatable.download);
+ }
+ }
+
+ // Report failed update checking
+ if (!m_failed_check_update.empty()) {
+ QString text;
+ for (const auto& failed : m_failed_check_update) {
+ const auto& mod = std::get<0>(failed);
+ const auto& reason = std::get<1>(failed);
+ const auto& recover_url = std::get<2>(failed);
+
+ qDebug() << mod->name() << " failed to check for updates!";
+
+ text += tr("Mod name: %1").arg(mod->name()) + "<br>";
+ if (!reason.isEmpty())
+ text += tr("Reason: %1").arg(reason) + "<br>";
+ if (!recover_url.isEmpty())
+ //: %1 is the link to download it manually
+ text += tr("Possible solution: Getting the latest version manually:<br>%1<br>")
+ .arg(QString("<a href='%1'>%1</a>").arg(recover_url.toString()));
+ text += "<br>";
+ }
+
+ ScrollMessageBox message_dialog(m_parent, tr("Failed to check for updates"),
+ tr("Could not check or get the following mods for updates:<br>"
+ "Do you wish to proceed without those mods?"),
+ text);
+ message_dialog.setModal(true);
+ if (message_dialog.exec() == QDialog::Rejected) {
+ m_aborted = true;
+ QMetaObject::invokeMethod(this, "reject", Qt::QueuedConnection);
+ return;
+ }
+ }
+
+ // If there's no mod to be updated
+ if (ui->modTreeWidget->topLevelItemCount() == 0) {
+ m_no_updates = true;
+ } else {
+ // FIXME: Find a more efficient way of doing this!
+
+ // Sort major items in alphabetical order (also sorts the children unfortunately)
+ ui->modTreeWidget->sortItems(0, Qt::SortOrder::AscendingOrder);
+
+ // Re-sort the children
+ auto* item = ui->modTreeWidget->topLevelItem(0);
+ for (int i = 1; item != nullptr; ++i) {
+ item->sortChildren(0, Qt::SortOrder::DescendingOrder);
+ item = ui->modTreeWidget->topLevelItem(i);
+ }
+ }
+
+ if (m_aborted || m_no_updates)
+ QMetaObject::invokeMethod(this, "reject", Qt::QueuedConnection);
+}
+
+// Part 1: Ensure we have a valid metadata
+auto ModUpdateDialog::ensureMetadata() -> bool
+{
+ auto index_dir = indexDir();
+
+ SequentialTask seq(m_parent, tr("Looking for metadata"));
+
+ // A better use of data structures here could remove the need for this QHash
+ QHash<QString, bool> should_try_others;
+ QList<Mod*> modrinth_tmp;
+ QList<Mod*> flame_tmp;
+
+ bool confirm_rest = false;
+ bool try_others_rest = false;
+ bool skip_rest = false;
+ ModPlatform::Provider provider_rest = ModPlatform::Provider::MODRINTH;
+
+ auto addToTmp = [&](Mod* m, ModPlatform::Provider p) {
+ switch (p) {
+ case ModPlatform::Provider::MODRINTH:
+ modrinth_tmp.push_back(m);
+ break;
+ case ModPlatform::Provider::FLAME:
+ flame_tmp.push_back(m);
+ break;
+ }
+ };
+
+ for (auto candidate : m_candidates) {
+ auto* candidate_ptr = candidate.get();
+ if (candidate->status() != ModStatus::NoMetadata) {
+ onMetadataEnsured(candidate_ptr);
+ continue;
+ }
+
+ if (skip_rest)
+ continue;
+
+ if (confirm_rest) {
+ addToTmp(candidate_ptr, provider_rest);
+ should_try_others.insert(candidate->internal_id(), try_others_rest);
+ continue;
+ }
+
+ ChooseProviderDialog chooser(this);
+ chooser.setDescription(tr("The mod '%1' does not have a metadata yet. We need to generate it in order to track relevant "
+ "information on how to update this mod. "
+ "To do this, please select a mod provider which we can use to check for updates for this mod.")
+ .arg(candidate->name()));
+ auto confirmed = chooser.exec() == QDialog::DialogCode::Accepted;
+
+ auto response = chooser.getResponse();
+
+ if (response.skip_all)
+ skip_rest = true;
+ if (response.confirm_all) {
+ confirm_rest = true;
+ provider_rest = response.chosen;
+ try_others_rest = response.try_others;
+ }
+
+ should_try_others.insert(candidate->internal_id(), response.try_others);
+
+ if (confirmed)
+ addToTmp(candidate_ptr, response.chosen);
+ }
+
+ if (!modrinth_tmp.empty()) {
+ auto* modrinth_task = new EnsureMetadataTask(modrinth_tmp, index_dir, ModPlatform::Provider::MODRINTH);
+ connect(modrinth_task, &EnsureMetadataTask::metadataReady, [this](Mod* candidate) { onMetadataEnsured(candidate); });
+ connect(modrinth_task, &EnsureMetadataTask::metadataFailed, [this, &should_try_others](Mod* candidate) {
+ onMetadataFailed(candidate, should_try_others.find(candidate->internal_id()).value(), ModPlatform::Provider::MODRINTH);
+ });
+ seq.addTask(modrinth_task);
+ }
+
+ if (!flame_tmp.empty()) {
+ auto* flame_task = new EnsureMetadataTask(flame_tmp, index_dir, ModPlatform::Provider::FLAME);
+ connect(flame_task, &EnsureMetadataTask::metadataReady, [this](Mod* candidate) { onMetadataEnsured(candidate); });
+ connect(flame_task, &EnsureMetadataTask::metadataFailed, [this, &should_try_others](Mod* candidate) {
+ onMetadataFailed(candidate, should_try_others.find(candidate->internal_id()).value(), ModPlatform::Provider::FLAME);
+ });
+ seq.addTask(flame_task);
+ }
+
+ seq.addTask(m_second_try_metadata);
+
+ ProgressDialog checking_dialog(m_parent);
+ checking_dialog.setSkipButton(true, tr("Abort"));
+ checking_dialog.setWindowTitle(tr("Generating metadata..."));
+ auto ret_metadata = checking_dialog.execWithTask(&seq);
+
+ return (ret_metadata != QDialog::DialogCode::Rejected);
+}
+
+void ModUpdateDialog::onMetadataEnsured(Mod* mod)
+{
+ // When the mod is a folder, for instance
+ if (!mod->metadata())
+ return;
+
+ switch (mod->metadata()->provider) {
+ case ModPlatform::Provider::MODRINTH:
+ m_modrinth_to_update.push_back(mod);
+ break;
+ case ModPlatform::Provider::FLAME:
+ m_flame_to_update.push_back(mod);
+ break;
+ }
+}
+
+ModPlatform::Provider next(ModPlatform::Provider p)
+{
+ switch (p) {
+ case ModPlatform::Provider::MODRINTH:
+ return ModPlatform::Provider::FLAME;
+ case ModPlatform::Provider::FLAME:
+ return ModPlatform::Provider::MODRINTH;
+ }
+
+ return ModPlatform::Provider::FLAME;
+}
+
+void ModUpdateDialog::onMetadataFailed(Mod* mod, bool try_others, ModPlatform::Provider first_choice)
+{
+ if (try_others) {
+ auto index_dir = indexDir();
+
+ auto* task = new EnsureMetadataTask(mod, index_dir, next(first_choice));
+ connect(task, &EnsureMetadataTask::metadataReady, [this](Mod* candidate) { onMetadataEnsured(candidate); });
+ connect(task, &EnsureMetadataTask::metadataFailed, [this](Mod* candidate) { onMetadataFailed(candidate, false); });
+
+ m_second_try_metadata->addTask(task);
+ } else {
+ QString reason{ tr("Couldn't find a valid version on the selected mod provider(s)") };
+
+ m_failed_metadata.append({mod, reason});
+ }
+}
+
+void ModUpdateDialog::appendMod(CheckUpdateTask::UpdatableMod const& info)
+{
+ auto item_top = new QTreeWidgetItem(ui->modTreeWidget);
+ item_top->setCheckState(0, Qt::CheckState::Checked);
+ item_top->setText(0, info.name);
+ item_top->setExpanded(true);
+
+ auto provider_item = new QTreeWidgetItem(item_top);
+ provider_item->setText(0, tr("Provider: %1").arg(ProviderCaps.readableName(info.provider)));
+
+ auto old_version_item = new QTreeWidgetItem(item_top);
+ old_version_item->setText(0, tr("Old version: %1").arg(info.old_version.isEmpty() ? tr("Not installed") : info.old_version));
+
+ auto new_version_item = new QTreeWidgetItem(item_top);
+ new_version_item->setText(0, tr("New version: %1").arg(info.new_version));
+
+ auto changelog_item = new QTreeWidgetItem(item_top);
+ changelog_item->setText(0, tr("Changelog of the latest version"));
+
+ auto changelog = new QTreeWidgetItem(changelog_item);
+ auto changelog_area = new QTextBrowser();
+
+ switch (info.provider) {
+ case ModPlatform::Provider::MODRINTH: {
+ HoeDown h;
+ // HoeDown bug?: \n aren't converted to <br>
+ auto text = h.process(info.changelog.toUtf8());
+
+ // Don't convert if there's an HTML tag right after (Qt rendering weirdness)
+ text.remove(QRegularExpression("(\n+)(?=<)"));
+ text.replace('\n', "<br>");
+
+ changelog_area->setHtml(text);
+ break;
+ }
+ case ModPlatform::Provider::FLAME: {
+ changelog_area->setHtml(info.changelog);
+ break;
+ }
+ }
+
+ changelog_area->setOpenExternalLinks(true);
+ changelog_area->setLineWrapMode(QTextBrowser::LineWrapMode::NoWrap);
+ changelog_area->setVerticalScrollBarPolicy(Qt::ScrollBarPolicy::ScrollBarAsNeeded);
+
+ // HACK: Is there a better way of achieving this?
+ auto font_height = QFontMetrics(changelog_area->font()).height();
+ changelog_area->setMaximumHeight((changelog_area->toPlainText().count(QRegularExpression("\n|<br>")) + 2) * font_height);
+
+ ui->modTreeWidget->setItemWidget(changelog, 0, changelog_area);
+
+ ui->modTreeWidget->addTopLevelItem(item_top);
+}
+
+auto ModUpdateDialog::getTasks() -> const QList<ModDownloadTask*>
+{
+ QList<ModDownloadTask*> list;
+
+ auto* item = ui->modTreeWidget->topLevelItem(0);
+
+ for (int i = 1; item != nullptr; ++i) {
+ if (item->checkState(0) == Qt::CheckState::Checked) {
+ list.push_back(m_tasks.find(item->text(0)).value());
+ }
+
+ item = ui->modTreeWidget->topLevelItem(i);
+ }
+
+ return list;
+}
diff --git a/launcher/ui/dialogs/ModUpdateDialog.h b/launcher/ui/dialogs/ModUpdateDialog.h
new file mode 100644
index 00000000..76aaab36
--- /dev/null
+++ b/launcher/ui/dialogs/ModUpdateDialog.h
@@ -0,0 +1,62 @@
+#pragma once
+
+#include "BaseInstance.h"
+#include "ModDownloadTask.h"
+#include "ReviewMessageBox.h"
+
+#include "minecraft/mod/ModFolderModel.h"
+
+#include "modplatform/CheckUpdateTask.h"
+
+class Mod;
+class ModrinthCheckUpdate;
+class FlameCheckUpdate;
+class ConcurrentTask;
+
+class ModUpdateDialog final : public ReviewMessageBox {
+ Q_OBJECT
+ public:
+ explicit ModUpdateDialog(QWidget* parent,
+ BaseInstance* instance,
+ const std::shared_ptr<ModFolderModel> mod_model,
+ QList<Mod::Ptr>& search_for);
+
+ void checkCandidates();
+
+ void appendMod(const CheckUpdateTask::UpdatableMod& info);
+
+ const QList<ModDownloadTask*> getTasks();
+ auto indexDir() const -> QDir { return m_mod_model->indexDir(); }
+
+ auto noUpdates() const -> bool { return m_no_updates; };
+ auto aborted() const -> bool { return m_aborted; };
+
+ private:
+ auto ensureMetadata() -> bool;
+
+ private slots:
+ void onMetadataEnsured(Mod*);
+ void onMetadataFailed(Mod*, bool try_others = false, ModPlatform::Provider first_choice = ModPlatform::Provider::MODRINTH);
+
+ private:
+ QWidget* m_parent;
+
+ ModrinthCheckUpdate* m_modrinth_check_task = nullptr;
+ FlameCheckUpdate* m_flame_check_task = nullptr;
+
+ const std::shared_ptr<ModFolderModel> m_mod_model;
+
+ QList<Mod::Ptr>& m_candidates;
+ QList<Mod*> m_modrinth_to_update;
+ QList<Mod*> m_flame_to_update;
+
+ ConcurrentTask* m_second_try_metadata;
+ QList<std::tuple<Mod*, QString>> m_failed_metadata;
+ QList<std::tuple<Mod*, QString, QUrl>> m_failed_check_update;
+
+ QHash<QString, ModDownloadTask*> m_tasks;
+ BaseInstance* m_instance;
+
+ bool m_no_updates = false;
+ bool m_aborted = false;
+};
diff --git a/launcher/ui/dialogs/ProgressDialog.cpp b/launcher/ui/dialogs/ProgressDialog.cpp
index e5226016..3c7f53d3 100644
--- a/launcher/ui/dialogs/ProgressDialog.cpp
+++ b/launcher/ui/dialogs/ProgressDialog.cpp
@@ -43,8 +43,8 @@ void ProgressDialog::setSkipButton(bool present, QString label)
void ProgressDialog::on_skipButton_clicked(bool checked)
{
Q_UNUSED(checked);
- task->abort();
- QDialog::reject();
+ if (task->abort())
+ QDialog::reject();
}
ProgressDialog::~ProgressDialog()
@@ -62,24 +62,24 @@ void ProgressDialog::updateSize()
int ProgressDialog::execWithTask(Task* task)
{
this->task = task;
- QDialog::DialogCode result;
if (!task) {
- qDebug() << "Programmer error: progress dialog created with null task.";
- return Accepted;
+ qDebug() << "Programmer error: Progress dialog created with null task.";
+ return QDialog::DialogCode::Accepted;
}
+ QDialog::DialogCode result;
if (handleImmediateResult(result)) {
return result;
}
// Connect signals.
- connect(task, SIGNAL(started()), SLOT(onTaskStarted()));
- connect(task, SIGNAL(failed(QString)), SLOT(onTaskFailed(QString)));
- connect(task, SIGNAL(succeeded()), SLOT(onTaskSucceeded()));
- connect(task, SIGNAL(status(QString)), SLOT(changeStatus(const QString&)));
- connect(task, SIGNAL(stepStatus(QString)), SLOT(changeStatus(const QString&)));
- connect(task, SIGNAL(progress(qint64, qint64)), SLOT(changeProgress(qint64, qint64)));
+ connect(task, &Task::started, this, &ProgressDialog::onTaskStarted);
+ connect(task, &Task::failed, this, &ProgressDialog::onTaskFailed);
+ connect(task, &Task::succeeded, this, &ProgressDialog::onTaskSucceeded);
+ connect(task, &Task::status, this, &ProgressDialog::changeStatus);
+ connect(task, &Task::stepStatus, this, &ProgressDialog::changeStatus);
+ connect(task, &Task::progress, this, &ProgressDialog::changeProgress);
connect(task, &Task::aborted, [this] { onTaskFailed(tr("Aborted by user")); });
@@ -89,19 +89,15 @@ int ProgressDialog::execWithTask(Task* task)
ui->globalProgressBar->setHidden(true);
}
- // if this didn't connect to an already running task, invoke start
+ // It's a good idea to start the task after we entered the dialog's event loop :^)
if (!task->isRunning()) {
- task->start();
- }
- if (task->isRunning()) {
- changeProgress(task->getProgress(), task->getTotalProgress());
- changeStatus(task->getStatus());
- return QDialog::exec();
- } else if (handleImmediateResult(result)) {
- return result;
+ QMetaObject::invokeMethod(task, &Task::start, Qt::QueuedConnection);
} else {
- return QDialog::Rejected;
+ changeStatus(task->getStatus());
+ changeProgress(task->getProgress(), task->getTotalProgress());
}
+
+ return QDialog::exec();
}
// TODO: only provide the unique_ptr overloads
diff --git a/launcher/ui/dialogs/ReviewMessageBox.cpp b/launcher/ui/dialogs/ReviewMessageBox.cpp
index c92234a4..e664e566 100644
--- a/launcher/ui/dialogs/ReviewMessageBox.cpp
+++ b/launcher/ui/dialogs/ReviewMessageBox.cpp
@@ -40,7 +40,7 @@ auto ReviewMessageBox::deselectedMods() -> QStringList
auto* item = ui->modTreeWidget->topLevelItem(0);
- for (int i = 0; item != nullptr; ++i) {
+ for (int i = 1; item != nullptr; ++i) {
if (item->checkState(0) == Qt::CheckState::Unchecked) {
list.append(item->text(0));
}
diff --git a/launcher/ui/dialogs/ScrollMessageBox.ui b/launcher/ui/dialogs/ScrollMessageBox.ui
index 299d2ecc..e684185f 100644
--- a/launcher/ui/dialogs/ScrollMessageBox.ui
+++ b/launcher/ui/dialogs/ScrollMessageBox.ui
@@ -6,7 +6,7 @@
<rect>
<x>0</x>
<y>0</y>
- <width>400</width>
+ <width>500</width>
<height>455</height>
</rect>
</property>
diff --git a/launcher/ui/dialogs/SkinUploadDialog.cpp b/launcher/ui/dialogs/SkinUploadDialog.cpp
index b5b78690..8180ac1f 100644
--- a/launcher/ui/dialogs/SkinUploadDialog.cpp
+++ b/launcher/ui/dialogs/SkinUploadDialog.cpp
@@ -57,68 +57,72 @@ void SkinUploadDialog::on_buttonBox_accepted()
{
QString fileName;
QString input = ui->skinPathTextBox->text();
- QRegularExpression urlPrefixMatcher(QRegularExpression::anchoredPattern("^([a-z]+)://.+$"));
- bool isLocalFile = false;
- // it has an URL prefix -> it is an URL
- if(urlPrefixMatcher.match(input).hasMatch())
- {
- QUrl fileURL = input;
- if(fileURL.isValid())
+ ProgressDialog prog(this);
+ SequentialTask skinUpload;
+
+ if (!input.isEmpty()) {
+ QRegularExpression urlPrefixMatcher(QRegularExpression::anchoredPattern("^([a-z]+)://.+$"));
+ bool isLocalFile = false;
+ // it has an URL prefix -> it is an URL
+ if(urlPrefixMatcher.match(input).hasMatch())
{
- // local?
- if(fileURL.isLocalFile())
+ QUrl fileURL = input;
+ if(fileURL.isValid())
{
- isLocalFile = true;
- fileName = fileURL.toLocalFile();
+ // local?
+ if(fileURL.isLocalFile())
+ {
+ isLocalFile = true;
+ fileName = fileURL.toLocalFile();
+ }
+ else
+ {
+ CustomMessageBox::selectable(
+ this,
+ tr("Skin Upload"),
+ tr("Using remote URLs for setting skins is not implemented yet."),
+ QMessageBox::Warning
+ )->exec();
+ close();
+ return;
+ }
}
else
{
CustomMessageBox::selectable(
this,
tr("Skin Upload"),
- tr("Using remote URLs for setting skins is not implemented yet."),
+ tr("You cannot use an invalid URL for uploading skins."),
QMessageBox::Warning
- )->exec();
+ )->exec();
close();
return;
}
}
else
{
- CustomMessageBox::selectable(
- this,
- tr("Skin Upload"),
- tr("You cannot use an invalid URL for uploading skins."),
- QMessageBox::Warning
- )->exec();
+ // just assume it's a path then
+ isLocalFile = true;
+ fileName = ui->skinPathTextBox->text();
+ }
+ if (isLocalFile && !QFile::exists(fileName))
+ {
+ CustomMessageBox::selectable(this, tr("Skin Upload"), tr("Skin file does not exist!"), QMessageBox::Warning)->exec();
close();
return;
}
+ SkinUpload::Model model = SkinUpload::STEVE;
+ if (ui->steveBtn->isChecked())
+ {
+ model = SkinUpload::STEVE;
+ }
+ else if (ui->alexBtn->isChecked())
+ {
+ model = SkinUpload::ALEX;
+ }
+ skinUpload.addTask(shared_qobject_ptr<SkinUpload>(new SkinUpload(this, m_acct->accessToken(), FS::read(fileName), model)));
}
- else
- {
- // just assume it's a path then
- isLocalFile = true;
- fileName = ui->skinPathTextBox->text();
- }
- if (isLocalFile && !QFile::exists(fileName))
- {
- CustomMessageBox::selectable(this, tr("Skin Upload"), tr("Skin file does not exist!"), QMessageBox::Warning)->exec();
- close();
- return;
- }
- SkinUpload::Model model = SkinUpload::STEVE;
- if (ui->steveBtn->isChecked())
- {
- model = SkinUpload::STEVE;
- }
- else if (ui->alexBtn->isChecked())
- {
- model = SkinUpload::ALEX;
- }
- ProgressDialog prog(this);
- SequentialTask skinUpload;
- skinUpload.addTask(shared_qobject_ptr<SkinUpload>(new SkinUpload(this, m_acct->accessToken(), FS::read(fileName), model)));
+
auto selectedCape = ui->capeCombo->currentData().toString();
if(selectedCape != m_acct->accountData()->minecraftProfile.currentCape) {
skinUpload.addTask(shared_qobject_ptr<CapeChange>(new CapeChange(this, m_acct->accessToken(), selectedCape)));
diff --git a/launcher/ui/dialogs/SkinUploadDialog.ui b/launcher/ui/dialogs/SkinUploadDialog.ui
index f4b0ed0a..c7b16645 100644
--- a/launcher/ui/dialogs/SkinUploadDialog.ui
+++ b/launcher/ui/dialogs/SkinUploadDialog.ui
@@ -21,7 +21,11 @@
</property>
<layout class="QHBoxLayout" name="horizontalLayout">
<item>
- <widget class="QLineEdit" name="skinPathTextBox"/>
+ <widget class="QLineEdit" name="skinPathTextBox">
+ <property name="placeholderText">
+ <string>Leave empty to keep current skin</string>
+ </property>
+ </widget>
</item>
<item>
<widget class="QPushButton" name="skinBrowseBtn">
diff --git a/launcher/ui/pages/instance/ExternalResourcesPage.cpp b/launcher/ui/pages/instance/ExternalResourcesPage.cpp
index d06f412b..69c20309 100644
--- a/launcher/ui/pages/instance/ExternalResourcesPage.cpp
+++ b/launcher/ui/pages/instance/ExternalResourcesPage.cpp
@@ -32,13 +32,13 @@ class SortProxy : public QSortFilterProxyModel {
const auto& mod = model->at(source_row);
- if (mod.name().contains(filterRegularExpression()))
+ if (filterRegularExpression().match(mod.name()).hasMatch())
return true;
- if (mod.description().contains(filterRegularExpression()))
+ if (filterRegularExpression().match(mod.description()).hasMatch())
return true;
for (auto& author : mod.authors()) {
- if (author.contains(filterRegularExpression())) {
+ if (filterRegularExpression().match(author).hasMatch()) {
return true;
}
}
@@ -182,7 +182,7 @@ void ExternalResourcesPage::retranslate()
void ExternalResourcesPage::filterTextChanged(const QString& newContents)
{
m_viewFilter = newContents;
- m_filterModel->setFilterFixedString(m_viewFilter);
+ m_filterModel->setFilterRegularExpression(m_viewFilter);
}
void ExternalResourcesPage::runningStateChanged(bool running)
diff --git a/launcher/ui/pages/instance/ExternalResourcesPage.ui b/launcher/ui/pages/instance/ExternalResourcesPage.ui
index 17bf455a..a13666b2 100644
--- a/launcher/ui/pages/instance/ExternalResourcesPage.ui
+++ b/launcher/ui/pages/instance/ExternalResourcesPage.ui
@@ -147,6 +147,17 @@
<string>Download a new resource</string>
</property>
</action>
+ <action name="actionUpdateItem">
+ <property name="enabled">
+ <bool>false</bool>
+ </property>
+ <property name="text">
+ <string>Check for &amp;Updates</string>
+ </property>
+ <property name="toolTip">
+ <string>Try to check or update all selected resources (all resources if none are selected)</string>
+ </property>
+ </action>
</widget>
<customwidgets>
<customwidget>
diff --git a/launcher/ui/pages/instance/InstanceSettingsPage.cpp b/launcher/ui/pages/instance/InstanceSettingsPage.cpp
index fcc110de..f11cf992 100644
--- a/launcher/ui/pages/instance/InstanceSettingsPage.cpp
+++ b/launcher/ui/pages/instance/InstanceSettingsPage.cpp
@@ -349,7 +349,7 @@ void InstanceSettingsPage::loadSettings()
ui->useDiscreteGpuCheck->setChecked(m_settings->get("UseDiscreteGpu").toBool());
#if !defined(Q_OS_LINUX)
- ui->perfomanceGroupBox->setVisible(false);
+ ui->settingsTabs->setTabVisible(ui->settingsTabs->indexOf(ui->performancePage), false);
#endif
// Miscellanous
diff --git a/launcher/ui/pages/instance/ModFolderPage.cpp b/launcher/ui/pages/instance/ModFolderPage.cpp
index 4432ccc8..14e1f1e5 100644
--- a/launcher/ui/pages/instance/ModFolderPage.cpp
+++ b/launcher/ui/pages/instance/ModFolderPage.cpp
@@ -49,6 +49,7 @@
#include "ui/GuiUtil.h"
#include "ui/dialogs/CustomMessageBox.h"
#include "ui/dialogs/ModDownloadDialog.h"
+#include "ui/dialogs/ModUpdateDialog.h"
#include "DesktopServices.h"
@@ -78,6 +79,23 @@ ModFolderPage::ModFolderPage(BaseInstance* inst, std::shared_ptr<ModFolderModel>
ui->actionsToolbar->insertActionBefore(ui->actionAddItem, ui->actionDownloadItem);
connect(ui->actionDownloadItem, &QAction::triggered, this, &ModFolderPage::installMods);
+
+ ui->actionUpdateItem->setToolTip(tr("Try to check or update all selected mods (all mods if none are selected)"));
+ ui->actionsToolbar->insertActionAfter(ui->actionAddItem, ui->actionUpdateItem);
+ connect(ui->actionUpdateItem, &QAction::triggered, this, &ModFolderPage::updateMods);
+
+ connect(ui->treeView->selectionModel(), &QItemSelectionModel::selectionChanged, this,
+ [this] { ui->actionUpdateItem->setEnabled(ui->treeView->selectionModel()->hasSelection() || !m_model->empty()); });
+
+ connect(mods.get(), &ModFolderModel::rowsInserted, this,
+ [this] { ui->actionUpdateItem->setEnabled(ui->treeView->selectionModel()->hasSelection() || !m_model->empty()); });
+
+ connect(mods.get(), &ModFolderModel::updateFinished, this, [this, mods] {
+ ui->actionUpdateItem->setEnabled(ui->treeView->selectionModel()->hasSelection() || !m_model->empty());
+
+ // Prevent a weird crash when trying to open the mods page twice in a session o.O
+ disconnect(mods.get(), &ModFolderModel::updateFinished, this, 0);
+ });
}
}
@@ -107,7 +125,6 @@ bool CoreModFolderPage::shouldDisplay() const
return false;
if (version->getComponent("net.minecraft")->getReleaseDateTime() < g_VersionFilterData.legacyCutoffDate)
return true;
-
}
return false;
}
@@ -118,7 +135,7 @@ void ModFolderPage::installMods()
return;
if (m_instance->typeName() != "Minecraft")
return; // this is a null instance or a legacy instance
-
+
auto profile = static_cast<MinecraftInstance*>(m_instance)->getPackProfile();
if (profile->getModLoaders() == ModAPI::Unspecified) {
QMessageBox::critical(this, tr("Error"), tr("Please install a mod loader first!"));
@@ -140,7 +157,7 @@ void ModFolderPage::installMods()
QStringList warnings = tasks->warnings();
if (warnings.count())
CustomMessageBox::selectable(this, tr("Warnings"), warnings.join('\n'), QMessageBox::Warning)->show();
-
+
tasks->deleteLater();
});
@@ -155,3 +172,63 @@ void ModFolderPage::installMods()
m_model->update();
}
}
+
+void ModFolderPage::updateMods()
+{
+ auto selection = m_filterModel->mapSelectionToSource(ui->treeView->selectionModel()->selection()).indexes();
+
+ auto mods_list = m_model->selectedMods(selection);
+ bool use_all = mods_list.empty();
+ if (use_all)
+ mods_list = m_model->allMods();
+
+ ModUpdateDialog update_dialog(this, m_instance, m_model, mods_list);
+ update_dialog.checkCandidates();
+
+ if (update_dialog.aborted()) {
+ CustomMessageBox::selectable(this, tr("Aborted"), tr("The mod updater was aborted!"), QMessageBox::Warning)->show();
+ return;
+ }
+ if (update_dialog.noUpdates()) {
+ QString message{ tr("'%1' is up-to-date! :)").arg(mods_list.front()->name()) };
+ if (mods_list.size() > 1) {
+ if (use_all) {
+ message = tr("All mods are up-to-date! :)");
+ } else {
+ message = tr("All selected mods are up-to-date! :)");
+ }
+ }
+ CustomMessageBox::selectable(this, tr("Update checker"), message)
+ ->exec();
+ return;
+ }
+
+ if (update_dialog.exec()) {
+ ConcurrentTask* tasks = new ConcurrentTask(this);
+ connect(tasks, &Task::failed, [this, tasks](QString reason) {
+ CustomMessageBox::selectable(this, tr("Error"), reason, QMessageBox::Critical)->show();
+ tasks->deleteLater();
+ });
+ connect(tasks, &Task::aborted, [this, tasks]() {
+ CustomMessageBox::selectable(this, tr("Aborted"), tr("Download stopped by user."), QMessageBox::Information)->show();
+ tasks->deleteLater();
+ });
+ connect(tasks, &Task::succeeded, [this, tasks]() {
+ QStringList warnings = tasks->warnings();
+ if (warnings.count()) {
+ CustomMessageBox::selectable(this, tr("Warnings"), warnings.join('\n'), QMessageBox::Warning)->show();
+ }
+ tasks->deleteLater();
+ });
+
+ for (auto task : update_dialog.getTasks()) {
+ tasks->addTask(task);
+ }
+
+ ProgressDialog loadDialog(this);
+ loadDialog.setSkipButton(true, tr("Abort"));
+ loadDialog.execWithTask(tasks);
+
+ m_model->update();
+ }
+}
diff --git a/launcher/ui/pages/instance/ModFolderPage.h b/launcher/ui/pages/instance/ModFolderPage.h
index 19caa732..0a7fc9fa 100644
--- a/launcher/ui/pages/instance/ModFolderPage.h
+++ b/launcher/ui/pages/instance/ModFolderPage.h
@@ -56,6 +56,7 @@ class ModFolderPage : public ExternalResourcesPage {
private slots:
void installMods();
+ void updateMods();
};
class CoreModFolderPage : public ModFolderPage {
diff --git a/launcher/ui/pages/modplatform/flame/FlameModPage.cpp b/launcher/ui/pages/modplatform/flame/FlameModPage.cpp
index 10d34218..772fd2e0 100644
--- a/launcher/ui/pages/modplatform/flame/FlameModPage.cpp
+++ b/launcher/ui/pages/modplatform/flame/FlameModPage.cpp
@@ -64,7 +64,7 @@ FlameModPage::FlameModPage(ModDownloadDialog* dialog, BaseInstance* instance)
auto FlameModPage::validateVersion(ModPlatform::IndexedVersion& ver, QString mineVer, ModAPI::ModLoaderTypes loaders) const -> bool
{
Q_UNUSED(loaders);
- return ver.mcVersion.contains(mineVer);
+ return ver.mcVersion.contains(mineVer) && !ver.downloadUrl.isEmpty();
}
// I don't know why, but doing this on the parent class makes it so that
diff --git a/launcher/ui/pages/modplatform/ftb/FtbPage.cpp b/launcher/ui/pages/modplatform/ftb/FtbPage.cpp
index 8a93bc2e..504d7f7b 100644
--- a/launcher/ui/pages/modplatform/ftb/FtbPage.cpp
+++ b/launcher/ui/pages/modplatform/ftb/FtbPage.cpp
@@ -2,6 +2,7 @@
/*
* PolyMC - Minecraft Launcher
* Copyright (c) 2022 Jamie Mansfield <jmansfield@cadixdev.org>
+ * Copyright (C) 2022 Sefa Eyeoglu <contact@scrumplex.net>
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
@@ -126,7 +127,7 @@ void FtbPage::suggestCurrent()
return;
}
- dialog->setSuggestedPack(selected.name + " " + selectedVersion, new ModpacksCH::PackInstallTask(selected, selectedVersion));
+ dialog->setSuggestedPack(selected.name + " " + selectedVersion, new ModpacksCH::PackInstallTask(selected, selectedVersion, this));
for(auto art : selected.art) {
if(art.type == "square") {
QString editedLogoName;
diff --git a/launcher/ui/themes/DarkTheme.cpp b/launcher/ui/themes/DarkTheme.cpp
index 31ecd559..712a9d3e 100644
--- a/launcher/ui/themes/DarkTheme.cpp
+++ b/launcher/ui/themes/DarkTheme.cpp
@@ -31,6 +31,7 @@ QPalette DarkTheme::colorScheme()
darkPalette.setColor(QPalette::Link, QColor(42, 130, 218));
darkPalette.setColor(QPalette::Highlight, QColor(42, 130, 218));
darkPalette.setColor(QPalette::HighlightedText, Qt::black);
+ darkPalette.setColor(QPalette::PlaceholderText, Qt::darkGray);
return fadeInactive(darkPalette, fadeAmount(), fadeColor());
}
diff --git a/launcher/ui/widgets/WideBar.cpp b/launcher/ui/widgets/WideBar.cpp
index 8d5bd12d..79f1e0c9 100644
--- a/launcher/ui/widgets/WideBar.cpp
+++ b/launcher/ui/widgets/WideBar.cpp
@@ -76,13 +76,20 @@ void WideBar::addSeparator()
m_entries.push_back(entry);
}
-void WideBar::insertActionBefore(QAction* before, QAction* action){
- auto iter = std::find_if(m_entries.begin(), m_entries.end(), [before](BarEntry * entry) {
- return entry->wideAction == before;
+auto WideBar::getMatching(QAction* act) -> QList<BarEntry*>::iterator
+{
+ auto iter = std::find_if(m_entries.begin(), m_entries.end(), [act](BarEntry * entry) {
+ return entry->wideAction == act;
});
- if(iter == m_entries.end()) {
+
+ return iter;
+}
+
+void WideBar::insertActionBefore(QAction* before, QAction* action){
+ auto iter = getMatching(before);
+ if(iter == m_entries.end())
return;
- }
+
auto entry = new BarEntry();
entry->qAction = insertWidget((*iter)->qAction, new ActionButton(action, this));
entry->wideAction = action;
@@ -90,14 +97,24 @@ void WideBar::insertActionBefore(QAction* before, QAction* action){
m_entries.insert(iter, entry);
}
+void WideBar::insertActionAfter(QAction* after, QAction* action){
+ auto iter = getMatching(after);
+ if(iter == m_entries.end())
+ return;
+
+ auto entry = new BarEntry();
+ entry->qAction = insertWidget((*(iter+1))->qAction, new ActionButton(action, this));
+ entry->wideAction = action;
+ entry->type = BarEntry::Action;
+ m_entries.insert(iter + 1, entry);
+}
+
void WideBar::insertSpacer(QAction* action)
{
- auto iter = std::find_if(m_entries.begin(), m_entries.end(), [action](BarEntry * entry) {
- return entry->wideAction == action;
- });
- if(iter == m_entries.end()) {
+ auto iter = getMatching(action);
+ if(iter == m_entries.end())
return;
- }
+
QWidget* spacer = new QWidget();
spacer->setSizePolicy(QSizePolicy::Expanding, QSizePolicy::Expanding);
@@ -107,6 +124,18 @@ void WideBar::insertSpacer(QAction* action)
m_entries.insert(iter, entry);
}
+void WideBar::insertSeparator(QAction* before)
+{
+ auto iter = getMatching(before);
+ if(iter == m_entries.end())
+ return;
+
+ auto entry = new BarEntry();
+ entry->qAction = QToolBar::insertSeparator(before);
+ entry->type = BarEntry::Separator;
+ m_entries.insert(iter, entry);
+}
+
QMenu * WideBar::createContextMenu(QWidget *parent, const QString & title)
{
QMenu *contextMenu = new QMenu(title, parent);
diff --git a/launcher/ui/widgets/WideBar.h b/launcher/ui/widgets/WideBar.h
index 2b676a8c..8ff62ef2 100644
--- a/launcher/ui/widgets/WideBar.h
+++ b/launcher/ui/widgets/WideBar.h
@@ -1,27 +1,34 @@
#pragma once
-#include <QToolBar>
#include <QAction>
#include <QMap>
+#include <QToolBar>
class QMenu;
-class WideBar : public QToolBar
-{
+class WideBar : public QToolBar {
Q_OBJECT
-public:
- explicit WideBar(const QString &title, QWidget * parent = nullptr);
- explicit WideBar(QWidget * parent = nullptr);
+ public:
+ explicit WideBar(const QString& title, QWidget* parent = nullptr);
+ explicit WideBar(QWidget* parent = nullptr);
virtual ~WideBar();
- void addAction(QAction *action);
+ void addAction(QAction* action);
void addSeparator();
- void insertSpacer(QAction *action);
- void insertActionBefore(QAction *before, QAction *action);
- QMenu *createContextMenu(QWidget *parent = nullptr, const QString & title = QString());
-private:
+ void insertSpacer(QAction* action);
+ void insertSeparator(QAction* before);
+ void insertActionBefore(QAction* before, QAction* action);
+ void insertActionAfter(QAction* after, QAction* action);
+
+ QMenu* createContextMenu(QWidget* parent = nullptr, const QString& title = QString());
+
+ private:
struct BarEntry;
- QList<BarEntry *> m_entries;
+
+ auto getMatching(QAction* act) -> QList<BarEntry*>::iterator;
+
+ private:
+ QList<BarEntry*> m_entries;
};